React Context Provider Hook Pattern

ST

Seb Toombs

()

2 min read

Two of the hardest to learn, and least understood concepts in React are context, and hooks. Both are incredibly powerful aspects of React, but they can take some time to get your head around.

I'm not going to try and teach the inner workings of either here, but I'm going to show a pattern for using context within a hook that you might find really useful if you're already using context in your app.

TLDR; show me the code

This snippet is a pretty popular 'copy and paste' job, so I'm going to stick it here if you just need to copy it and move on!

import {createContext, useState} from React;

export const SomeContext = createContext();

export const useSomeContext = () => useContext(SomeContext);

export const SomeProvider = ({ children }) => {
  const [someState, setSomeState] = useState();

  return (
    <SomeContext.Provider value={{ someState, setSomeState }}>
      {children}
    </SomeContext.Provider>
  );
};


// And use it like;

const App = () => {
  return <SomeProvider>
    <div>
      <p>This is my app!</p>
      <Main/>
    </div>
  </SomeProvider>;
}

const Main = () => {
  const {someState, setSomeState} = useSomeContext();
  
  return <p>{someState}</p>;
}

React context hook in detail

Wanna know how it works? Read on!

React context is a great way to share uhhh... context... throughout multiple components. Let's say you have a piece of state that needs to be available in multiple components of your app. Usually, it's simple enough to just pass down the state via props (prop drilling) from a parent component to a child.

A contrived prop-drilling example


// A basic example of 2 level deep prop drilling
// Extremely contrived, and not how you'd do it in the 
// Real World(tm) anyway


// Our top level app component passes (for some reason)
// down the active nav menu item as activeNavItem on <Layout/>
function App() {
  return (
    <Layout activeNavItem="learn-react">
      This is the main content of the app!
    </Layout>
  )
}

// The Layout component receives the activeNavItem prop
// and passes it down to the <PrimaryNavigation/> component
function Layout(props) {
  const { activeNavItem, children } = props;
  
  return (
    <div className="layout">
      <header className="header">
        <Brand/>
        <PrimaryNavigation activeNavItem={activeNavItem}/>
      </header>
      <main>{children}</main>
    </div>
  )
}

// Finally, the PrimaryNavigation component receives the activeNavItem
// prop and uses it to add a classname to the active nav link
function PrimaryNavigation(props) {
  const {activeNavItem} = props;
  return (
    <ul className="primaryNav">
      <li><a href="/" className={activeNavItem==='' ? 'isActive' : undefined}>Home</a></li>
      <li><a href="/learn-react" className={activeNavItem==='learn-react' ? 'isActive' : undefined}>Learn React</a></li>
    </ul>
  )
}

In most cases, passing props down one, two, even three levels is fine. In many cases, you can find a way to refactor your code without having to drill props down so many levels (which often has other benefits). In fact, using context instead of prop drilling or component composition is mostly discouraged unless the added complexity is worth it.

In the above code example, I wouldn't actually reach for context. Which makes this a poor example, but press on, we shall.

Now let's say instead of the <App/> component passing the active nav item down through a couple of components, we wanted to share it around via context using the custom hook above (again, I'm not explaining how context or hooks work here, just how to use the above)

Here's how we might refactor the code

import {createContext, useState} from React;

export const NavContext = createContext();

export const useNavContext = () => useContext(NavContext);

export const NavProvider = ({ children, activeNavItem }) => {
  
  // This is our context that actually gets shared
  // Of course we can put whatever we like here
  // Don't want consumers to set state explicitly?
  // Write some accessor methods and send 'em on down
  // etc
  const ctx = {
    activeNavItem
  }

  return (
    <NavContext.Provider value={ctx}>
      {children}
    </NavContext.Provider>
  );
};


function App() {
  
  return (
    // We need to wrap our entire App 
    // (or at least the highest level that ever needs
    // to access the context) in the NavProvider
    // every child down the tree will have
    // access to the context from useNavContext now
    <NavProvider>
      // We don't need to pass the prop to Layout anymore
      <Layout>
        This is the main content of the app!
      </Layout>
    </NavProvider>
  )
}

// The Layout component receives the activeNavItem prop
// and passes it down to the <PrimaryNavigation/> component
function Layout(props) {
  // activeNavItem is no longer passed in as a prop
  const { children } = props;
  
  return (
    <div className="layout">
      <header className="header">
        <Brand/>
        // Remove the prop here as well
        <PrimaryNavigation/>
      </header>
      <main>{children}</main>
    </div>
  )
}

// Finally, the PrimaryNavigation component receives the activeNavItem
// prop and uses it to add a classname to the active nav link
function PrimaryNavigation(props) {
  // Instead of this (previously)
  // const {activeNavItem} = props;
  // We now get activeNavItem from context using our hook
  const {activeNavItem} = useNavContext();
  return (
    <ul className="primaryNav">
      <li><a href="/" className={activeNavItem==='' ? 'isActive' : undefined}>Home</a></li>
      <li><a href="/learn-react" className={activeNavItem==='learn-react' ? 'isActive' : undefined}>Learn React</a></li>
    </ul>
  )
}

Of course, this is still an extremely contrived example, and not really a good use case for context instead of prop-drilling or component composition.

Got a good use case? Or one you'd like to know is a good use case for the context provider hook pattern or not? Hit me up on twitter! (Tweet it, rather than DM so others can learn too)