Alex's Notes

Context (React)

Topic 7 of React Hooks: Tyler McGinnis

In a component architecture, sharing state across multiple components will become an issue.

Typically we lift the state up to the lowest common parent. This works, but in some cases it becomes unmanageable or redundant. Take React Router for example, it needs to pass props to any component in the tree.

Since this is such a common issue React provides a means to pass data through the tree without manually passing props down the hierarchy - context.

Let’s imagine our app supports locales - a user can click a button that will set the locale to English or Spanish and the UI should update everywhere to reflect the locale. What do we need for this?

  1. We need a way to declare the data that we want available throughout our component tree.

  2. We need a way for any component in the component tree that requires that data to be able to subscribe to it.

We can do both of these things with context. Typically you create a new context for each unique piece of data that needs to be available throughout your component tree.

So for locale we could say const LocaleContext = React.createContext(). That LocaleContext object we get back has two properties LocaleContext.Provider and LocaleContext.Consumer that are react components.

Provider allows us to declare the data we want available throughout the tree.

Consumer allows any component in the component tree that needs the data to be able to subscribe to it.

To use a provider we just make sure that all components that need its data are within it, like this:

<MyContext.Provider value={data}>
    <App />
<MyContext.Provider>

Practially, what we do is create a new file for that context, looking like this:

import React from 'react'

const LocaleContext = React.createContext()

export default LocaleContext

Then we can use it as follows:

import LocaleContext from './LocaleContext'

const App = () => {
    const [locale, setLocale] = React.useState('en');

    return (
	<LocaleContext.Provider value={locale}>
	    <Home />
	</LocaleContext>
)}

Now any component in the tree has the option of subscribing to that value, using LocaleContext.Consumer.

To do that React uses a render prop. We have a function, that React will pass as an argument the value that was provided by the context provider, like this:

// Blog.js
import LocaleContext from './LocaleContext'

export default function Blog() {
    return (
	<LocaleContext.Consumer>
	  {(locale) => <Posts locale={locale} />}
	</LocaleContext.Consumer>
)}

We also want a means of updating the value anywhere. For this we can use an object for the Provider value that includes a method to change the state in that high level Provider. But there’s a gotcha, if you just use an object naively like value={locale: 'en', update: function} you’re going to have performance issues. This is because React compares object references to see if anything’s changed, and this will create a new object reference each time. Use state in the parent context to avoid this, rather than an object literal. (ie have a method attached to the state of your parent context, and use that method to update).

defaultValue When a context consumer renders the context value, it will look for the closest context provider up its tree and take that value. What if there isn’t one? It will take the value from the argument provided to React.createContext() when the context was created in the first place. This means you can create a default and then only use a context Provider when you need to override it. Here’s a toy example fro Michael Chan:

const SwearContext = React.createContext('bad words')

function ContextualExclamation() {
    return (
	<SwearContext.Consumer>
	    {(word) => <span> Oh {word}!</span>}
	</SwearContxt.Consumer>
)}

function GrannysHouse() {
    return (
	<SwearContext.Provider value="less bad words">
	    <h1> Granny's House </h1>
	    <ContextualExclamation />
	</SwearContext.Provider>
)}

function FriendsHouse() {
    return (
	<React.Fragment>
	    <h1> Friend's House </h1>
	    <ContextualExclamation />
	</React.Fragment>
)}

function App () {
    return (
	<React.Fragment>
	    <GrannysHouse />
	    <FriendsHouse />
	</React.Fragment>
)}

Hook

Now the render prop syntax is pretty horrible, especially with multiple contexts in play at any one time (they end up nested…)

Fortunately React provides a useContext hook to avoid it.

So we can do something like this:

export default function Nav() {
    const { locale, toggleLocale } = React.useContext(LocaleContext);

    return locale === 'en'
	?  <EnglishNav toggleLocale={toggleLocale} />
	:  <SpanishNav toggleLocale={toggleLocale} />
}

This is the same use case as our Context.Consumer component, but with a more composable, readable API.

Note that context is not the answer to everything. There is nothing wrong with passing props down the tree. It’s common to over-use context when you first learn about it. Avoid this!

Good use cases include theming, locale, auth, if you’re a library and need to get information anywhere.

Links to this note