是否可以在多个组件上使用相同的 <Context.Provider> ?
Posted
技术标签:
【中文标题】是否可以在多个组件上使用相同的 <Context.Provider> ?【英文标题】:Is it possible to use the same <Context.Provider> on multiple components? 【发布时间】:2020-02-16 19:39:06 【问题描述】:我知道我可以用我的 <Context.Provider>
包装 HOC 并在所有子组件中使用它。
我想在两个单独的组件中使用上下文,但它们嵌套在某个很深的地方,并且它们最近的父级位于应用程序根目录中的某个地方。我不想为(几乎)所有组件提供上下文,所以我想知道是否可以只包装这两个组件?
我尝试这样做,但只有第一个组件获取上下文。
App 结构如下所示:
<App>
<A1>
<A2>
<MyContext.Provider>
<Consumer1/>
</MyContext.Provider>
</A2>
</A1>
<B1>
<B2>
<MyContext.Provider>
<Consumer2/>
</MyContext.Provider>
</B2>
</B1>
</App>
编辑:我错误地认为包装根组件会在上下文更改时重新渲染所有子组件。只有消费者会重新渲染,所以包装根组件非常好。
【问题讨论】:
I don't want to provide context to (almost) all components
你的意思是几乎所有组件都不使用上下文,还是说它们确实使用它并且你需要特意确保它们没有任何内容消费?
如果我用<Context.Provider>
包裹根组件,所有子组件将在上下文更改时重新渲染。这是不必要的,因为只有少数子组件需要上下文中的数据。
【参考方案1】:
如果您想要在应用程序的多个部分之间共享一个值,那么您需要以某种形式将该值移动到需要使用该值的那些的共同祖先组件。正如您在 cmets 中提到的,您的问题是性能之一,并且尽量不重新渲染所有内容。拥有两个提供者对此并没有真正的帮助,因为仍然需要一些组件来确保两个提供者都提供相同的价值。因此,该组件最终需要成为两个提供者的共同祖先。
相反,您可以使用 shouldComponentUpdate(用于类组件)或 React.memo(用于功能组件)来阻止重新渲染过程沿组件树向下进行。使用 Context.Consumer 的深层后代仍将重新渲染,因此您可以跳过树的中间部分。这是一个示例(注意在中间组件上使用 React.memo):
const Context = React.createContext(undefined);
const useCountRenders = (name) =>
const count = React.useRef(0);
React.useEffect(() =>
count.current++;
console.log(name, count.current);
);
const App = () =>
const [val, setVal] = React.useState(1);
useCountRenders('App');
React.useEffect(() =>
setTimeout(() =>
console.log('updating app');
setVal(val => val + 1)
, 1000);
, [])
return (
<Context.Provider value=val>
<IntermediateComponent />
</Context.Provider>
);
const IntermediateComponent = React.memo((props) =>
useCountRenders('intermediate');
return (
<div>
<Consumer name="first consumer"/>
<UnrelatedComponent/>
<Consumer name="second consumer"/>
</div>
);
)
const Consumer = (props) =>
useCountRenders(props.name);
return (
<Context.Consumer>
val =>
console.log('running consumer child', props.name);
return <div>consuming val</div>
</Context.Consumer>
)
const UnrelatedComponent = (props) =>
useCountRenders('unrelated');
return props.children || null;
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.10.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.10.2/umd/react-dom.development.js"></script>
<div id="root"></div>
当您运行上述代码时,请检查日志以查看哪些组件重新呈现。在第一次通过时,所有内容都会呈现,但在一秒钟后,当应用程序状态发生变化时,只有应用程序会重新呈现。 IntermediateComponent、UnrelatedComponent 甚至 Consumer 都不会重新渲染。 Context.Consumer 中的函数会重新运行,并且该函数返回的任何内容(在本例中只是一个 div)都会重新呈现。
【讨论】:
我想关于共同祖先的那部分回答了我的问题。【参考方案2】:根据 OP 的要求,此解决方案主要使用钩子,但 useReducer 无法在单独的提供者下实现状态共享(据我尝试)。
它不需要一个 Provider 位于应用的根目录,每个 Provider 可以使用不同的 reducer。
它使用静态状态管理器,但这是一个实现细节,要在不同上下文提供者下的多个组件之间共享状态,需要对状态的共享引用、更改它的方法以及通知这些变化。
当使用 sn-p 时,第一个按钮显示共享状态并在单击时递增 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 的东西,其静态状态由多个 Provider/Consumer 集合共享。
无论提供者和消费者的位置和深度如何,它都可以正常工作,而无需依赖长嵌套的更新链。
请注意,按照 OP 的要求,这里不需要在应用程序的根目录中提供提供程序,因此不会为每个组件提供上下文,只为您选择的子集提供上下文。
【讨论】:
你真的测试过吗?如果我在两个组件上使用单独的<Context.Provider>
,我将获得两个单独的商店。编辑:此评论在编辑您的答案之前针对您的代码。
顺便说一句,我正在使用useReducer
和useContext
钩子。
能否请您提供一个代码给 React hooks 而不是自定义 Provider?
据我所知,useReducer 阻止了不同提供者下的状态共享。不过,这个修改后的解决方案提供了相同类型的 API。以上是关于是否可以在多个组件上使用相同的 <Context.Provider> ?的主要内容,如果未能解决你的问题,请参考以下文章