|
An excerpt from React Quickly, Second Edition by Morten Barklund This excerpt explores using React Context. Read it if you’re a React developer or want to learn more about React. |
Take 25% off React Quickly, Second Edition by entering fccmardan2 into the discount code box at checkout at manning.com.
Let’s jump in and build an application that solves a real-world problem. We’re going to make a user dashboard, i.e. the screen you see after you log in to an application. This dashboard has a welcome message that welcomes you by name and it also has a button in the top left corner that displays your name and links to your settings page. The trick here is that the name is dynamic and will be returned to us by a backend.
The end result is supposed to look like figure 1.
Figure 1 The desired end result for our user dashboard.. The user’s name is displayed twice in this screenshot, which is the core of the issue here.
Let’s break this down into components. We want the top menu to be part of a header, and the central welcome page is just one of many pages that can be displayed by our application. We know we’re going to add more stuff in the future, so let’s add some extra layers in preparation. We will use a component approach as laid out in figure 2.
Figure 2 Our component structure with the necessary placeholders needed for the name. Note how the “name” property is used twice, but still not passed along as a property anywhere.
However, as you can see in figure 2, we did not display how we got the name from the very top of the component tree, where it is passed into the Dashboard component all the way down to the two smaller components at the end that need to display it.
Using only regular React properties we would need to pass the property through every component on its way to the component that needs it. If we did so, it would look like figure 3.
Figure 3 Our component structure if we pass the name to every component that needs to pass it on. There’s a total of five components needing the name property but only two of them actually display it
But note in this component tree, we are passing the name property to both the Header and the Main component. Neither of those components actually need this property by itself. The only reason why we have to pass this property to these two components is so that they can forward the property to yet another component.
Nevertheless, this of course works and we can implement that, so let’s do that in listing 1.
Listing 1 Dashboard with a lot of name props.
const BUTTON_STYLE = { display: 'inline-block', padding: '4px 10px', background: 'transparent', border: '0', }; const HEADER_STYLE = { display: 'flex', justifyContent: 'flex-end', borderBottom: '1px solid', }; function Button({ children }) { return <button style={BUTTON_STYLE}>{children}</button>; } function UserButton({ name }) { return <Button>👤 {name}</Button>; #A } function Header({ name }) { #B return ( <header style={HEADER_STYLE}> <Button>Home</Button> <Button>Groups</Button> <Button>Profile</Button> <UserButton name={name} /> #C </header> ); } function Welcome({ name }) { return <section><h1>Welcome, {name}!</h1></section>; } function Main({ name }) { #B return <main><Welcome name={name} /></main>; #C } function Dashboard({ name }) { return <><Header name={name} /><Main name={name} /></>; }
#A Did you know you can use emojis directly in React? You can!
#B Here we pass a name property to a component that doesn’t actually need to use the property itself
#C The component is only passed the property to be able to pass it on to another component.
npx create-react-app rq10-dashboard-props --template rq10-dashboard-props
This is a reasonable approach, and it works. If you open this up in the browser, you see exactly what we wanted in figure 1.
React Context
Those properties being passed to components, only for them to be passed on to another component, that doesn’t look like good software design. There must be a better way. What if we could have a storage object encapsulating a number of components, which could feed data to all its child components when they asked for it? And they should be able to do so without having any extra properties passed around.
Congratulations, we have just invented React Context. A context does exactly that. It wraps a number of components with a value that all descendant components can access without going through properties at all.
A context in React consists of two parts. It needs a provider that contains the values that you want to pass to any descendant component, and it needs a consumer, that you use in each descendant component that wants access to the provided value.
The context provider is a pretty simple React component. The consumer can be created in two different ways: Either as a component with a function as a child or as a useContext
hook. The former approach is pretty unusual and cumbersome, so we’re not going to use that at all. In modern React applications, you might never see that used at all anyway. It is only useful in class-based codebases, where you can’t use the hook variant.
In essence, using a context looks something like figure 4.
Figure 4 Using a hook with a provider and a consumer using the useContext hook.
We need two pieces of React API here. First of all we need createContext to define the context, which we will store in a variable. This is a variable created outside any component and lives in the same places as other components and can thus be referenced just like any other component.
The other part is the useContext hook. This hook takes a reference to the context and returns the current context value.
Let’s go ahead and add a NameContext to our dashboard application from earlier to the component tree. We do that in figure 5.
Figure 5 The dashboard application component tree with a context surrounding it all.
And that’s literally all it takes. We can go ahead and implement this in listing 2.
Listing 2 Dashboard with context.
import { createContext, useContext } from 'react'; #A const BUTTON_STYLE = { display: 'inline-block', padding: '4px 10px', background: 'transparent', border: '0', }; const HEADER_STYLE = { display: 'flex', justifyContent: 'flex-end', borderBottom: '1px solid', }; const NameContext = createContext(); #B function Button({ children }) { return <button style={BUTTON_STYLE}>{children}</button>; } function UserButton() { #C const name = useContext(NameContext); #D return <Button>👤 {name}</Button>; } function Header() { #C return ( <header style={HEADER_STYLE}> <Button>Home</Button> <Button>Groups</Button> <Button>Profile</Button> <UserButton /> </header> ); } function Welcome() { #C const name = useContext(NameContext); #D return <section><h1>Welcome, {name}!</h1></section>; } function Main() { #C return <main><Welcome /></main>; } function Dashboard({ name }) { return ( <NameContext.Provider value={name}> #E <Header /> <Main /> </NameContext.Provider> ); }
#A We import the two functions from the React package
#B The context is created in the global scope, so we can access it from anywhere.
#C A lot of our components don’t take any properties at all anymore.
#D The two components that need access to the name can just do so by hooking into the context using useContext.
#E In the dashboard component, we make sure to wrap the entire tree in a context provider with the name as the context value.
npx create-react-app rq10-dashboard-context --template rq10-dashboard-context
We get the exact same result as before, but with a much nicer flow of data in our opinion.
Context hooks are stateful
Using a context to store a static value that is used throughout an application is definitely nice, but what is even nicer is that we can store dynamic information there as well. The useContext
hook is stateful, so if the context value changes, the useContext
hook will cause the component using it to re-render automatically.
Let’s imagine that same dashboard, but this time you are an administrator that wants to be able to see what the dashboard looks like for any user in the database. As an administrator, you have a dropdown of users that you can see the dashboard for. We will implement this like figure 6, where the Dashboard component is the same component as before (we just won’t show all its child components to save space).
Figure 6 The admin dashboard allows the user to choose which user to see the dashboard for. The admin dashboard includes a select box and the regular user dashboard.
We will use a simple select element to allow the user to select between the three users in the system: Alice, Bob, and Carol. We can use a simple useState for remembering the selected user and pass that on to the components as needed. Let’s extend the previous example with this new administrator dashboard in listing 3.
Listing 3 Administrator dashboard.
import { useState, createContext, useContext } from 'react'; #A const BUTTON_STYLE = { display: 'inline-block', padding: '4px 10px', background: 'transparent', border: '0', }; const HEADER_STYLE = { display: 'flex', justifyContent: 'flex-end', borderBottom: '1px solid', }; const NameContext = createContext(); function Button({ children }) { return <button style={BUTTON_STYLE}>{children}</button>; } function UserButton() { const name = useContext(NameContext); return <Button>👤 {name}</Button>; } function Header() { return ( <header style={HEADER_STYLE}> <Button>Home</Button> <Button>Groups</Button> <Button>Profile</Button> <UserButton /> </header> ); } function Welcome() { const name = useContext(NameContext); return <section><h1>Welcome, {name}!</h1></section>; } function Main() { return <main><Welcome /></main>; } function Dashboard({ name }) { #B return ( <NameContext.Provider value={name}> <Header /> <Main /> </NameContext.Provider> ); } function AdminDashboard() { const [user, setUser] = useState('Alice'); #C return ( <> <select value={user} onChange={(evt) => setUser(evt.target.value)}> #D <option>Alice</option> <option>Bob</option> <option>Carol</option> </select> <Dashboard name={user} /> #E </> ); }
#A We need to import the useState hook as well
#B Everything inside the Dashboard component is exactly as before
#C We create a simple state, defaulting to Alice
#D We use a controlled select element to choose a user
#E And we pass the currently selected user to the dashboard component.
npx create-react-app rq10-dashboard-admin --template rq10-dashboard-admin
If we try this in the browser, it looks like this figure 7. Go ahead and select a different name from the dropdown, and see the name correctly update in the dashboard in both the menu and the headline.
Figure 7 The admin dashboard displaying the user dashboard for Carol, as we have selected her name in the admin dropdown in the top left.
Memoization
There is still one thing left to do here though. You might be thinking, how can we be sure that the correct components are re-rendering when we change the name in the name context? And that’s a fair question. Because if you were to turn on the debug option in React developer tools, which highlights any component that re-renders, you would see that all the components are re-rendering as in figure 8.
Figure 8 All the components re-render when the user selection changes. You can see that the entire header re-renders, because all the buttons in the header individually re-render—they all have a blue box around them.
And the reason is simple. Any component re-renders when its parent component re-renders. So when the Dashboard component re-renders, its two child components, Header and Main, will also re-render. And when they render, all their children do too, etc. But these two components, Main and Header, don’t actually need to re-render, because nothing has changed for them. We can inform React of this by memoizing these two components. Let’s go ahead and do that in listing 4.
Listing 4 Administrator dashboard with memoization.
import { memo, useState, createContext, useContext } from 'react'; #A const BUTTON_STYLE = { display: 'inline-block', padding: '4px 10px', background: 'transparent', border: '0', }; const HEADER_STYLE = { display: 'flex', justifyContent: 'flex-end', borderBottom: '1px solid', }; const NameContext = createContext(); function Button({ children }) { return <button style={BUTTON_STYLE}>{children}</button>; } function UserButton() { const name = useContext(NameContext); return <Button>👤 {name}</Button>; } const Header = memo(function Header() { #B return ( <header style={HEADER_STYLE}> <Button>Home</Button> <Button>Groups</Button> <Button>Profile</Button> <UserButton /> </header> ); } function Welcome() { const name = useContext(NameContext); return <section><h1>Welcome, {name}!</h1></section>; } const Main = memo(function Main() { #B return <main><Welcome /></main>; } function Dashboard({ name }) { return ( <NameContext.Provider value={name}> <Header /> <Main /> </NameContext.Provider> ); } function AdminDashboard() { const [user, setUser] = useState('Alice'); return ( <> <select value={user} onChange={(evt) => setUser(evt.target.value)}> <option>Alice</option> <option>Bob</option> <option>Carol</option> </select> <Dashboard name={user} /> </> ); }
#A We only change two things. We import the memo function from the React package.
#B And we use it to memoize two components.
npx create-react-app rq10-dashboard-memo --template rq10-dashboard-memo
When we now open this in a browser and change the user with the debugger enabled, we see that only the required components re-render as you can see in figure 9.
Figure 9 This time, only the correct elements are re-rendering. It looks like there’s a blue box around the header, but it’s actually around the entire Dashboard component (which is supposed to re-render). You can see that the header component is not re-rendering, because the other buttons in the header are clearly not re-rendering.
The useContext
hooks are working. They cause their components to re-render without their parent components re-rendering at all—just as a stateful hook is supposed to do!
That’s pretty nifty. The whole context concept is quite powerful if we ponder the implications. Which we’re going to do in a little while, but first we’re going to look at the context API in a bit more detail.
Check out part 2 for more. Thanks for reading.