React multiple contexts
Asked Answered
U

4

69

I am using functions which are passed down through context.

ChildComponent.contextType = SomeContext;

Now I use this.context.someFunction();. This works.

How can I do this if I need functions from two different parent components?

Utmost answered 16/11, 2018 at 22:56 Comment(0)
S
132

You can still use function-as-a-child consumer nodes with the 16.3 Context API, which is what the React documentation suggests doing:

// Theme context, default to light theme
const ThemeContext = React.createContext('light');

// Signed-in user context
const UserContext = React.createContext({
  name: 'Guest',
});

class App extends React.Component {
  render() {
    const {signedInUser, theme} = this.props;

    // App component that provides initial context values
    return (
      <ThemeContext.Provider value={theme}>
        <UserContext.Provider value={signedInUser}>
          <Layout />
        </UserContext.Provider>
      </ThemeContext.Provider>
    );
  }
}

function Layout() {
  return (
    <div>
      <Sidebar />
      <Content />
    </div>
  );
}

// A component may consume multiple contexts
function Content() {
  return (
    <ThemeContext.Consumer>
      {theme => (
        <UserContext.Consumer>
          {user => (
            <ProfilePage user={user} theme={theme} />
          )}
        </UserContext.Consumer>
      )}
    </ThemeContext.Consumer>
  );
}

To use functions in context in your component you'd typically wrap your component in a HOC so the context is passed in as props:

export const withThemeContext = Component => (
  props => (
    <ThemeContext.Consumer>
      {context => <Component themeContext={context} {...props} />}
    </ThemeContext.Consumer>
  )
)

const YourComponent = ({ themeContext, ...props }) => {
  themeContext.someFunction()
  return (<div>Hi Mom!</div>)
}

export default withThemeContext(YourComponent)

If you're running React 16.8+ you can also use hooks to do this more cleanly without using HOCs:

import React, { useContext } from "react"

const YourComponent = props => {
  const theme = useContext(ThemeContext)
  const user = useContext(UserContext)
}

Or, if you consume these contexts a lot, you can even make a custom hook to simplify further:

const useTheme = () => useContext(ThemeContext)
const useUser = () => useContext(UserContext)

const YourComponent = props => {
  const theme = useTheme()
  const user = useUser()
}
Sika answered 16/11, 2018 at 23:7 Comment(10)
The context is giving me functions to be used in the class, not data to be rendered.Utmost
@Utmost This works the same with multiple context providers as it does like normal, but I updated my answer with a quick example.Sika
When YourComponent is a class, then themeContext becomes this.props.themeContext, right?Utmost
I use this approach as well if I really need those context separated from each otherLorin
I especially liked the use of withThemeContext HOC :)Livi
imo const theme = useContext(ThemeContext);const user = useContext(UserContext) is the cleanest.Harwilll
I just saw this. Comming from a approach that uses only one context and I use useReducer for updating states. Isn't it more efficent and dare I see even preferable to have one context with useReducer then nesting multiple ones?Scammony
@Scammony Not necessarily. When you consume context in a child you tell React to re-render that component when the context changes. If you have bits of independent state, combining multiple bits of state into a single context provider can increase the number of components that have to render when the context changes. You really have to weigh the implications for the app/scenario you find yourself in because there is no “One True Path” that is devoid of tradeoffs.Sika
@Sika The reason for me highlighting this is because the above scneario could get quite ambitious to handle. If i have 15 diffirent states I would have 15 contexts that would get nested in order not to rerender. Because of that reason I would argue having one context with useReducer and react.Memo to be more efficient since it would be easier to handle all state in one place and react.Memo would handle the rerendering.Scammony
@Scammony Yeah it's possible, and I encourage you to test it out and see what works for your application. Note though that memoization is not free so you may run into cases where React.Memo degrades performance versus rendering for some components or trees, or that the performance benefits are not worth the challenges of building with it (e.g. inadvertent bugs, issues with hot reloading, etc). Finally, it may be worth considering Recoil—it's early days but very compelling for certain scenarios!Sika
A
27

Another solution is to create a separate context providing the other contexts:

import React, { createContext, memo, useContext } from "react";
import isEqual from "react-fast-compare";

export const MultiContext = createContext(null);
MultiContext.displayName = "MultiContext";

export const MultiContextProvider = memo(
  function({ map, children }) {
    const contextMap = {};
    for (const i in map) {
      contextMap[i] = useContext(map[i]);
    }

    return (
      <MultiContext.Provider value={contextMap}>
        {children}
      </MultiContext.Provider>
    );
  },
  (prevProps, nextProps) => isEqual(prevProps.children, nextProps.children)
);

MultiContextProvider.displayName = "MultiContextProvider";

Example usage:

class DemoConsumer extends React.Component {
  static contextType = MultiContext;

  render() {
    return JSON.stringify({
      someValue: this.context.SomeContext.someValue,
      otherValue: this.context.OtherContext.otherValue,
    });
  }
}

function App() {
  return (
    <MultiContextProvider map={{ SomeContext, OtherContext }}>
      <MultiContextDemoClassConsumer />
    </MultiContextProvider>
  );
}

Demo:

const {
  createContext,
  memo,
  useContext,
  useState,
  useEffect,
} = React;

const MultiContext = createContext(null);
MultiContext.displayName = "MultiContext";

const MultiContextProvider = memo(
  function({ map, children }) {
    console.log("render provider");
    const contextMap = {};
    for (const i in map) {
      contextMap[i] = useContext(map[i]);
    }

    return (
      <MultiContext.Provider value={contextMap}>
        {children}
      </MultiContext.Provider>
    );
  },
  (prevProps, nextProps) => isEqual(prevProps.children, nextProps.children)
);
MultiContextProvider.displayName = "MultiContextProvider";

const initialMinutes = new Date().getMinutes();
const MinutesContext = createContext(initialMinutes);
MinutesContext.displayName = "MinutesContext";

const IncrementContext = createContext(0);
IncrementContext.displayName = "IncrementContext";

class MultiContextDemoClassConsumer extends React.Component {
  static contextType = MultiContext;

  render() {
    return JSON.stringify(this.context);
  }
}

const multiContextMap = { MinutesContext, IncrementContext };
function App() {
  const forceUpdate = useForceUpdate();

  const [minutes, setMinutes] = useState(initialMinutes);
  useEffect(() => {
    const timeoutId = setInterval(() => {
      // console.log('set minutes')
      setMinutes(new Date().getMinutes());
    }, 1000);
    return () => {
      clearInterval(timeoutId);
    };
  }, [setMinutes]);

  const [increment, setIncrement] = useState(0);

  console.log("render app");

  return (
    <MinutesContext.Provider value={minutes}>
      <IncrementContext.Provider value={increment}>
        <MultiContextProvider map={multiContextMap}>
          <MultiContextDemoClassConsumer />
        </MultiContextProvider>
        <button onClick={() => setIncrement(i => i + 1)}>Increment</button>
        <button onClick={forceUpdate}>Force Update</button>
      </IncrementContext.Provider>
    </MinutesContext.Provider>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));
<script type="module">
  import React from 'https://dev.jspm.io/react@16';
  import ReactDOM from 'https://dev.jspm.io/react-dom@16';
  import useForceUpdate from 'https://dev.jspm.io/[email protected]';
  import isEqual from 'https://dev.jspm.io/[email protected]';
  window.React = React;
  window.ReactDOM = ReactDOM;
  window.useForceUpdate = useForceUpdate.default;
  window.isEqual = isEqual;
</script>
<div id="root"></div>
Aubigny answered 26/2, 2020 at 18:53 Comment(0)
T
13

You could also simply merge all your contexts into a single one:

const AppContext = React.createContext({
  user: { name: 'Guest' },
  theme: 'light',
})

ChildComponent.contextType = AppContext;

Done. You simply need to merge the new values if you have a different context in some parts of you app (like a different theme or user).

Toner answered 12/2, 2019 at 21:41 Comment(6)
Reagarding your solution. It does extra re-renders while context value changes in some nested object, doesn't it? Both, user and theme can have nested objects and it causes re-renders everywhere after changing objects (even there were not needed), unless I'm mistaken. Please, correct me if I'm wrongCaution
@Caution if the value of the context changes, any component that utilizes the context will re-render, yes. That's why it makes sense to have a context that changes infrequently at the top of the tree and one that changes a lot closer to where it's used.Chromous
Your solution is very good, BUT to avoid the render excess, we may use useReducer hook to create one context.Nomi
@HoussemBadri maybe you could post an answer demonstrating your suggestion?Cyprinoid
Can someone explain why this solution doesn't work?Shantishantung
@DallinDavis this isn't a good solution because when a context is updated, all subscribers re-render. Keeping everything in one context will mean a lot of unnecessary re-renders.Faline
A
-1

This worked for me.

<AuthProvider>
      <ProvideSide>
        <Component {...pageProps} />
      </ProvideSide>
    </AuthProvider>

all i did was to ensure i pass children in the authprovider and provideside context.

authprovider context function

export function AuthProvider({ children }) {
  const auth = useProvideAuth();

  return <authContext.Provider value={auth}>{children}</authContext.Provider>;
}

provideside contenxt

export function ProvideSide({ children }) {
  const side = useProvideSide();
  return <sideContext.Provider value={side}>{children}</sideContext.Provider>;
}
Antepenult answered 21/12, 2021 at 8:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.