As requested by the OP this solution uses mostly hooks but useReducer cannot achieve state sharing under separate providers (as far as I've tried).
It does not require one Provider to be at the root of the app and different reducer can be used for each Provider.
It uses a static state manager but that's an implementation detail, to share the state between several component under different context proviver, one will need something like a shared reference to the state, a way to change it and a way to notify of these changes.
When using the snippet the first button shows the shared state and increment foo
when clicked, the second button shows the same shared state and increments bar
when clicked:
// The context
const MyContext = React.createContext();
// the shared static state
class ProviderState {
static items = [];
static register(item) {
ProviderState.items.push(item);
}
static unregister(item) {
const idx = ProviderState.items.indexOf(item);
if (idx !== -1) {
ProviderState.items.splice(idx, 1);
}
}
static notify(newState) {
ProviderState.state = newState;
ProviderState.items.forEach(item => item.setState(newState));
}
static state = { foo: 0, bar: 0 };
}
// the state provider which registers to (listens to) the shared state
const Provider = ({ reducer, children }) => {
const [state, setState] = React.useState(ProviderState.state);
React.useEffect(
() => {
const entry = { reducer, setState };
ProviderState.register(entry);
return () => {
ProviderState.unregister(entry);
};
},
[]
);
return (
<MyContext.Provider
value={{
state,
dispatch: action => {
const newState = reducer(ProviderState.state, action);
if (newState !== ProviderState.state) {
ProviderState.notify(newState);
}
}
}}
>
{children}
</MyContext.Provider>
);
}
// several consumers
const Consumer1 = () => {
const { state, dispatch } = React.useContext(MyContext);
// console.log('render1');
return <button onClick={() => dispatch({ type: 'inc_foo' })}>foo {state.foo} bar {state.bar}!</button>;
};
const Consumer2 = () => {
const { state, dispatch } = React.useContext(MyContext);
// console.log('render2');
return <button onClick={() => dispatch({ type: 'inc_bar' })}>bar {state.bar} foo {state.foo}!</button>;
};
const reducer = (state, action) => {
console.log('reducing action:', action);
switch(action.type) {
case 'inc_foo':
return {
...state,
foo: state.foo + 1,
};
case 'inc_bar':
return {
...state,
bar: state.bar + 1,
};
default:
return state;
}
}
// here the providers are used on the same level but any depth would work
class App extends React.Component {
render() {
console.log('render app');
return (
<div>
<Provider reducer={reducer}>
<Consumer1 />
</Provider>
<Provider reducer={reducer}>
<Consumer2 />
</Provider>
<h2>I'm not rerendering my children</h2>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>
In the end we have re created something a little like redux with a static state shared by several Provider/Consumer ensembles.
It will work the same no matter where and how deep the providers and consumers are without relying on long nested update chains.
Note that as the OP requested, here there is no need for a provider at the root of the app and therefore the context is not provided to every component, just the subset you choose.
I don't want to provide context to (almost) all components
Do you mean that almost all components don't consume the context, or you mean they do consume it and you need to deliberately make sure there's nothing for them to consume? – Decency<Context.Provider>
, all child component will re-render on context change. That's unnecessary as only few child components needs that data in context. – Windpipe