По требованию OP это решение использует в основном перехватчики, но useReducer не может обеспечить совместное использование состояний у отдельных провайдеров (насколько я пытался).
Не требуется, чтобы один провайдер находился в корнеприложение и другой редуктор могут использоваться для каждого провайдера.
В нем используется диспетчер статического состояния, но это деталь реализации, чтобы разделить состояние между несколькими компонентами в разных провайдерах контекста, для одного потребуется нечто вроде общей ссылкив состояние, способ его изменения и способ уведомления об этих изменениях.
При использовании фрагмента первая кнопка отображает общее состояние и приращение foo
при нажатии, вторая кнопка показывает тот же общийсостояние и увеличивается bar
при нажатии:
// 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>
В итоге мы создали нечто похожее на redux со статическим состоянием, которое используется несколькими ансамблями провайдеров / потребителей.
Он будет работать одинаково, независимо от того, где и насколько глубоко провайдеры и потребители не будут полагаться на длинные вложенные цепочки обновлений.
Обратите внимание, что в соответствии с запросом OP здесь нет необходимости в провайдерекорень приложения и, следовательно, контекст предоставляется не каждому компоненту, только выбранное вами подмножество.