是否可以在多个组件上使用相同的 <Context.Provider>?

Is it possible to use the same <Context.Provider> on multiple components?

我知道我可以用我的 <Context.Provider> 包装 HOC 并在所有子组件中使用它。

我想在两个单独的组件中使用上下文,但它们嵌套得很深,而且它们最近的父组件位于应用程序根目录中的某个位置。我不想为(几乎)所有组件提供上下文,所以我想知道是否可以只包装这两个组件?

我尝试这样做,但只有第一个组件获取上下文。

App 结构如下所示:

<App>
    <A1>
        <A2>
            <MyContext.Provider>
                <Consumer1/>
            </MyContext.Provider>
        </A2>
    </A1>
    <B1>
        <B2>
            <MyContext.Provider>
                <Consumer2/>
            </MyContext.Provider>
        </B2>
    </B1>
</App>

编辑:我错误地认为包装根组件会在上下文更改时重新呈现所有子组件。只有消费者会重新渲染,所以包装根组件是完全没问题的。

根据 OP 的要求,此解决方案主要使用钩子,但 useReducer 无法在单独的提供程序下实现状态共享(据我尝试)。

它不需要一个 Provider 位于应用程序的根目录,并且可以为每个 Provider 使用不同的 reducer。

它使用静态状态管理器,但这是一个实现细节,要在不同上下文提供者下的多个组件之间共享状态,需要一些类似对状态的共享引用的东西,一种更改它的方法以及一种通知这些更改。

使用代码片段时,第一个按钮显示共享状态并在单击时递增 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&apos;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 的要求,这里不需要在应用程序的根目录提供提供程序,因此上下文不会提供给每个组件,只提供您选择的子集。

如果您想要在应用程序的多个部分之间共享一个值,那么您需要以某种形式将该值向上移动到需要使用该值的组件的公共祖先组件。正如您在评论中提到的,您的问题是性能问题之一,并且尽量不重新渲染所有内容。拥有两个提供者并不能真正帮助解决这个问题,因为仍然需要一些组件来确保两个提供者提供相同的价值。因此该组件最终将需要成为两个提供者的共同祖先。

相反,您可以使用 shouldComponentUpdate(对于 class 组件)或 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)都将重新呈现。