65

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?

ATOzTOA
  • 34,814
  • 22
  • 96
  • 117

4 Answers4

127

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()
}
coreyward
  • 77,547
  • 20
  • 137
  • 166
  • The context is giving me functions to be used in the class, not data to be rendered. – ATOzTOA Nov 16 '18 at 23:29
  • @ATOzTOA This works the same with multiple context providers as it does like normal, but I updated my answer with a quick example. – coreyward Nov 16 '18 at 23:59
  • 1
    When `YourComponent` is a class, then `themeContext` becomes `this.props.themeContext`, right? – ATOzTOA Nov 17 '18 at 05:13
  • I use this approach as well if I really need those context separated from each other – John Mar 08 '19 at 07:12
  • 1
    I especially liked the use of `withThemeContext ` HOC :) – STEEL Jul 09 '19 at 10:36
  • imo `const theme = useContext(ThemeContext);const user = useContext(UserContext)` is the cleanest. – artidataio Oct 29 '21 at 08:13
  • 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? – ThunD3eR Dec 23 '21 at 08:52
  • 1
    @ThunD3eR 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. – coreyward Dec 23 '21 at 15:14
  • @coreyward 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. – ThunD3eR Dec 27 '21 at 09:32
  • 1
    @ThunD3eR 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](https://recoiljs.org/)—it's early days but very compelling for certain scenarios! – coreyward Dec 27 '21 at 20:06
24

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/use-force-update@1.0.7';
  import isEqual from 'https://dev.jspm.io/react-fast-compare@3.0.1';
  window.React = React;
  window.ReactDOM = ReactDOM;
  window.useForceUpdate = useForceUpdate.default;
  window.isEqual = isEqual;
</script>
<div id="root"></div>
Wallace Sidhrée
  • 11,221
  • 6
  • 47
  • 58
Dimitar Nestorov
  • 2,411
  • 24
  • 29
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).

Anna B
  • 5,997
  • 5
  • 40
  • 52
  • 29
    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 wrong – sunpietro Apr 30 '19 at 07:19
  • 2
    @sunpietro 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. – rrd Sep 17 '19 at 08:11
  • Your solution is very good, BUT to avoid the render excess, we may use useReducer hook to create one context. – Houssam Badri Jan 27 '20 at 17:07
  • 5
    @HoussemBadri maybe you could post an answer demonstrating your suggestion? – ESR Jan 31 '20 at 15:23
  • Can someone explain why this solution doesn't work? – Dallin Davis Aug 16 '21 at 20:12
  • 1
    @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. – jndietz Sep 24 '22 at 03:34
-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>;
}
jac wida
  • 187
  • 1
  • 1
  • 8