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

时间:2019-10-20 14:31:51

标签: reactjs react-context

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

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

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

App结构如下:

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

编辑:我错了,认为包装根组件会使上下文更改重新呈现所有子组件。只有使用者会重新渲染,因此包装根组件是完全可以的。

2 个答案:

答案 0 :(得分:1)

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

相反,您可以使用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)都将重新呈现。

答案 1 :(得分:0)

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

它不需要一个提供者位于应用程序的根目录,并且可以为每个提供者使用不同的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的东西,其静态状态由多个提供者/消费者集合共享。

无论提供者和使用者位于何处以及深度有多深,都无需依赖长嵌套的更新链,便可以发挥同样的作用。

请注意,根据OP的要求,此处无需在应用程序的根目录提供提供程序,因此不会将上下文提供给每个组件,仅提供您选择的子集即可。