📓 Help Queue: Creating a Context and Provider
With our Help Queue project set up and our updates planned, we're ready to implement context. In this lesson, we'll do three things:
- Create a context for our light and dark theme.
- Learn about context providers and consumers.
- Create a context provider.
- Implement a state management tool to change the value of the context.
Implementing Context​
Let's create a context for our theme. Start by creating a new folder called context
within the src
folder, and within that a file called theme-context.js
.
Within theme-context.js
, we'll add our CSS style object that we created earlier as well as some new code:
import React from 'react';
export const themes = {
light: {
backgroundColor: "AntiqueWhite",
textColor: "DarkSlateGrey",
buttonBackground: "Lavender",
inputBackground: "Gainsboro"
},
dark: {
backgroundColor: "DarkSlateGrey",
textColor: "AntiqueWhite",
buttonBackground: "#232b3c",
inputBackground: "#45516d"
}
};
export const ThemeContext = React.createContext();
First we've imported React from 'react'
, and then we've declared our CSS style object and saved it to the themes
variable. Notice that we're exporting themes
so that we can use it where we need to in our app.
Then, we create a new context with React's creatContext()
method:
export const ThemeContext = React.createContext();
The convention is to name context objects in Upper Camel Case, calling it whatever is representative of the data that the context will hold. We've called our context ThemeContext
, because it holds theme data.
It's also common convention to include the data relevant to the context within the same file, which is why we're including the CSS object within theme-context.js
. However, it would be fine to save this information in the component that manages the theme state.
And with that we've created our context! But it's not that useful yet: we haven't associated a value with our ThemeContext
and we haven't put it to use in our app. Next, let's learn about the tools that ThemeContext
exposes: provider and consumer components.
Context Providers and Consumers​
Context uses provider and consumer components to share data between components. In fact, the ThemeContext
we created earlier is an object with two properties: a provider and a consumer:
export const ThemeContext = React.createContext();
console.log(ThemeContext.Provider);
console.log(ThemeContext.Consumer);
Since React components are just functions, we can console.log
these values, like in the above code snippet. However, we won't get helpful information when look inside of the ThemeContext.Provider
and ThemeContext.Consumer
variables. The takeaway here is that these two components are generated from the createContext()
method, and they are the mechanism by which context works:
- Provider components provide data to a section of the (or entire) component tree
- Consumer components enable individual components to use the data that the provider component exposes.
Provider and consumer components wrap around the components that they are modifying to give and gain access to the context. Using the Help Queue as an example, if we wanted to provide ThemeContext
data to our component tree, we might wrap App.js
in <ThemeContext.Provider>
, and any component that consumes that data would be wrapped with <ThemeContext.Consumer>
:
As demonstrated in the component tree above, we can use as many consumer components as we need in our component tree. However, we'll use just one provider component that is lifted to the lowest common ancestor to all of the consumer components that consume the provider's data.
Remember that data flows down in React, and this is true for context provider and consumer components as well. This means that if we can only use a consumer component lower in the component tree from a provider component. Using the example above, we purposefully place our provider component around App.js
so that the four child and grandchild components of App.js
can access the context data.
Take note that the above component tree is for demonstration purposes only. It's very close to how we'll implement a provider and consumers, but we'll use slightly different tools and locate the provider component within App.js
, but not wrapped around <App>
. This will become clear soon.
Ultimately, it's important to understand that provider and consumer components enact a subscription-based relationship: consumer components are subscribed to the corresponding provider component, and any time the value of the provider component changes, all of the components that consume the providers data will be re-rendered. Using the example component diagram above, this would mean anytime the value of the provider component changes in App.js
, the following four components will re-render: TicketControl.js
, TicketDetail.js
, ReusableForm.js
, and ToggleTheme.js
. Pretty neat!
Next, let's implement a provider.
Adding a Provider Component​
Since we've determined that App.js
is our lowest common ancestor to all of the components that will need access to the ThemeContext
data, let's add a provider there. First, we'll import ThemeContext
and our CSS themes
object from theme-context.js
:
import { ThemeContext, themes } from "../context/theme-context";
Next, let's wrap our component tree with the ThemeContext.Provider
component. In the following code notice that we're replacing the <React.Fragment>
components with the <ThemeContext.Provider>
components:
import React from "react";
import Header from "./Header";
import TicketControl from "./TicketControl";
import ToggleTheme from "./ToggleTheme";
import { ThemeContext, themes } from "../context/theme-context";
function App(){
return (
<ThemeContext.Provider value={themes.light}>
<Header />
<ToggleTheme/>
<TicketControl />
</ThemeContext.Provider>
);
}
export default App;
Notice that we've added a prop called value
to our <ThemeContext.Provider>
component. This is how we designate a value for our context provider. The prop must always be called value
.
We've set the value of the value
prop to themes.light
. That will be our starting theme. However, this theme is static! If we want the value of our theme to change, we'll need to implement a state management tool. Let's do that next.
Adding State​
We'll use the useState()
hook to manage our state. Let's start with an import and declare a new state variable:
import React, { useState } from "react";
import Header from "./Header";
import TicketControl from "./TicketControl";
import ToggleTheme from "./ToggleTheme";
import { ThemeContext, themes } from "../context/theme-context";
function App(){
const [theme, setTheme] = useState(themes.light);
return (
<ThemeContext.Provider value={themes.light}>
<Header />
<ToggleTheme/>
<TicketControl />
</ThemeContext.Provider>
);
}
export default App;
Our state variable is called theme
and it is set to the light theme in our CSS themes
object.
The next update we need to make is to set the value
prop of our <ThemeContext.Provider>
to the value of our state variable theme
:
...
function App(){
const [theme, setTheme] = useState(themes.light);
return (
<ThemeContext.Provider value={theme}> {/* new code! */}
...
</ThemeContext.Provider>
);
}
export default App;
Now the value of our provider component is directly tied to our state variable. That means we can call setTheme()
to change the current theme for our provider and its consumers. Next, let's wire up our button in the ToggleTheme
component to do just that!
Wiring Up the ToggleTheme
Button​
The first thing we'll need to do is pass a callback function down to the ToggleTheme
component so that it can invoke a change in state in App.js
. Remember that callback functions enable us to pass data up from a child component to a parent component, while maintaining React's unidirectional data flow.
We'll create a toggleTheme()
function in App.js
that calls the setTheme()
state updater function:
...
function App(){
const [theme, setTheme] = useState(themes.light);
function toggleTheme() {
setTheme(theme =>
theme.textColor === "AntiqueWhite" ? themes.light : themes.dark
);
}
return (
...
);
}
export default App;
Notice that we pass a function to the setTheme()
function call:
theme => theme.textColor === "AntiqueWhite" ? themes.light : themes.dark
This is an arrow function that makes use of all of the available shortcuts: omitting parens with one parameter and an implicit return. We could otherwise write this arrow function like so:
(theme) => {
return theme.textColor === "AntiqueWhite" ? themes.light : themes.dark
}
We're passing in a function to the setTheme()
function in order to access the current value of the theme
state variable.
The ternary operator checks to see if we're on the light theme by checking the value of the textColor
property; if we're on the light theme, then it updates state to the dark theme, and vice versa.
Note that the ternary operator could be re-written like so:
(theme) => {
if (theme.textColor === "AntiqueWhite") {
return themes.light
} else {
return themes.dark
}
}
Next, we'll need to pass the toggleTheme()
function to the ToggleTheme
component:
...
function App(){
const [theme, setTheme] = useState(themes.light);
function toggleTheme() {
setTheme(theme =>
theme.textColor === "AntiqueWhite" ? themes.light : themes.dark
);
}
return (
<ThemeContext.Provider value={theme}>
<Header />
<ToggleTheme toggleTheme={toggleTheme}/> {/* new code! */}
<TicketControl />
</ThemeContext.Provider>
);
}
export default App;
Next let's update the ToggleTheme
component to accept props, list prop types, and use the toggleTheme()
function.
import React from "react";
import PropTypes from "prop-types";
function ToggleTheme(props) {
const { toggleTheme } = props;
return (
<React.Fragment>
<button onClick={toggleTheme}>
Toggle Theme
</button>
<hr/>
</React.Fragment>
);
}
ToggleTheme.propTypes = {
toggleTheme: PropTypes.func
}
export default ToggleTheme;
At this point if we run our project, we'll be able to click our toggle theme button and change the value of the theme
state variable. But we won't be able to tell that anything is working. That's because we're not actually using the theme
state anywhere! Let's fix this issue by adding some styles so that we can see the new toggle theme functionality in action.
Setting the Background and Text Color​
The first styling we'll add is updating the <body>
tag's background color and text color to match the current theme. We'll first show you the two new lines of code, and then explain it. Here's what we'll add to App.js
:
...
function App(){
const [theme, setTheme] = useState(themes.light);
document.body.style.backgroundColor = theme.backgroundColor; // new code!
document.body.style.color = theme.textColor; // new code!
function toggleTheme() {
setTheme(theme =>
theme.textColor === "AntiqueWhite" ? themes.light : themes.dark
);
}
return (
<ThemeContext.Provider value={theme}>
<Header />
<ToggleTheme toggleTheme={toggleTheme}/>
<TicketControl />
</ThemeContext.Provider>
);
}
export default App;
Let's break down the new code we've added. We've used dot notation to access the nested properties of the document
object:
- The
document
object represents the Document Object Model (DOM) for our Help Queue app. The DOM is a Web API. - The
document.body
property represents the<body>
tags in the DOM. - The
document.body.style
property represents the<body>
tags'style
attribute, which sets inline HTML styles. - The
style.backgroundColor
andstyle.color
represent the CSS propertiesbackground-color
andcolor
respectively. We can set the value of these properties to change the background and text colors for the body tags.
So this JavaScript:
document.body.style.backgroundColor = theme.backgroundColor;
document.body.style.color = theme.textColor;
Is the same as this CSS:
body {
background-color: "blue";
color: "white";
}
However, the JavaScript is dynamic in nature. Instead of hardcoding values like in the example CSS above ("blue"
and "white"
), we're able to set the values of the background color and text color to the current theme. Very cool!
Go ahead and test this out now: run your project and press the "toggle theme" button; you'll see the background and text color change from a light theme to a dark theme. At this point we still need to update the background colors of our button and input elements to complete the functionality. We'll do that in the next lesson when we learn how to create context consumers for function and class components.