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

2

12

I know that I can wrap HOC with my <Context.Provider> and consume it in all child components.

I would like to consume context in two separate components but they are nested somewhere deeply and closest parent of them is somewhere in the app root. I don't want to provide context to (almost) all components, so I was wondering is it possible to wrap only those two components?

I tried to do it but only first component gets context.

The App structure looks like this:

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

EDIT: I was wrong thinking that wrapping root component will make re-render all child components on context change. Only consumers will rerender so it's perfectly fine to wrap root component.

Windpipe answered 20/10, 2019 at 14:31 Comment(3)
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
If I wrap root component with <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
I had this same question and I was wrapping my components with Context Providers multiple times. Turns out, if one of the components updated the state in the Context; the updated state did not reflect in the other component. After wrapping with Provider at the top parent level, it fixes that issue.Frazil
D
7

If you want to have a single value which is shared between multiple parts of the application, then in some form you will need to move that value up to the common ancestor component of the ones that need to consume the value. As you mentioned in the comments, your issue is one of performance and trying not to rerender everything. Having two providers doesn't really help with this, because there will still need to be some component making sure both providers are providing the same value. So that component will end up needing to be a common ancestor of the two providers.

Instead, you can use shouldComponentUpdate (for class components) or React.memo (for functional components) to stop the rerendering process from working its way down the component tree. Deep descendants which are using Context.Consumer will still rerender, and so you can skip over the middle parts of your tree. Here's an example (note the use of React.memo on the intermediate component):

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>

When you run the above code, check the logs to see which components rerender. On the first pass, everything renders, but then after a second when the app state changes, only app rerenders. IntermediateComponent, UnrelatedComponent, and even Consumer don't rerender. The function inside the Context.Consumer does rerun, and any thing returned by that function (in this case just a div) will rerender.

Decency answered 20/10, 2019 at 15:12 Comment(1)
I guess that part about common ancestor answers my question.Windpipe
F
2

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&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>

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.

Faunie answered 20/10, 2019 at 14:44 Comment(4)
Did you actually test it? I'm getting two separate stores if I use separate <Context.Provider> on two components. EDIT: this comment targets your code before editing your answer.Windpipe
And by the way, I am using useReducer and useContext hooks.Windpipe
Could you please provide a code to React hooks instead of custom Provider?Windpipe
As far as I know, useReducer prevents state sharing under different providers. This modified solution offers the same kind of API though.Faunie

© 2022 - 2024 — McMap. All rights reserved.