For this tutorial, you should have a fair understanding of hooks. Still, before we begin, I’ll briefly discuss what they are and the hooks we’ll be using in this article.
According to the React Docs:
“Hooks are a new addition in React 16.8. They let you use state and other React features without writing a class.”
That is basically what a React hook is. It allows us to use state, refs and other React features in our functional components.
Let us discuss the two hooks we will encounter in this article.
The useState
Hook
The useState hook allows us to use state in our functional components. A useState
hook takes the initial value of our state as the only argument, and it returns an array of two elements. The first element is our state variable and the second element is a function in which we can use the update the value of the state variable.
Let’s take a look at the following example:
import React, {useState} from "react";
function SampleComponent(){
const [count, setCount] = useState(0);
}
Here, count
is our state variable and its initial value is 0
while setCount
is a function which we can use to update the value of count.
The useContext
Hook
I will discuss this later in the article but this hook basically allows us to consume the value of a context. What this actually means will become more apparent later in the article.
Yarn Workspaces
Yarn workspaces let you organize your project codebase using a monolithic repository (monorepo). React is a good example of an open-source project that is monorepo and uses Yarn workspaces to achieve that purpose. Learn more ?
Why Do We Need The Context API?
We want to build a “theme toggler” component which toggles between light mode and dark mode for our React app. Every component has to have access to the current theme mode so they can be styled accordingly.
Normally, we would provide the current theme mode to all the components through props and update the current theme using state
:
import React from "react";
import ReactDOM from "react-dom";
function App() {
return (
<div>
<Text theme= "blue" />
<h1>{theme}</h1>
</div>
);
}
function Text({theme}) {
return(
<h1 style = {{
color: `${theme}`
}}>{theme}</h1>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
In the code sample above, we created a Text Component which renders an h1
element. The color of the h1
element depends on the current theme mode. Currently, the theme is blue. We can toggle between blue
and red
themes by using state
.
We will create a state called “theme” using the useState
hook. The useState
hook will return the current value of the theme and a function which we can use to update the theme.
So, let us create our theme state:
const [theme, setTheme] = React.useState("blue");
We will also add a button element to our App
component. This button will be used to toggle the themes and it needs a click event handler. So, let us write the click event handler like so:
const onClickHandler = () => {
setTheme();
}
Now, we want to set the new theme to Red
if the current theme is Blue
, and vice versa. Instead of using an if
statement, a more convenient way to do this is with the help of the ternary operator in JavaScript.
setTheme( theme === "red"? "blue": "red");
So now, we have written our onClick
handler. Let’s add this button element to the App
component:
<button onClick = {onClickHandler}>Change theme</button>
Let us also change the value of the theme props of the Text component to the theme state.
<Text theme={theme}/>
Now, we should have this:
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function App() {
const[theme, setTheme] = React.useState("red");
const onClickHandler = () => {
setTheme( theme === "red"? "blue": "red");
}
return (
<div>
<Text theme={theme}/>
<button onClick = {onClickHandler}>Change theme</button>
</div>
);
}
function Text({theme}) {
return(
<h1 style = {{
color: `${theme}`
}}>{theme}</h1>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
We can now toggle between our two themes. However, if this was a much larger application, it would be difficult to use the theme in deeply nested components and the code becomes unwieldy.
Introducing The Context API
Let me introduce the Context API. According to the React documentation:
“Context provides a way to pass data through the component tree without having to pass props down manually at every level.”
For a more in-depth definition, it provides a way for you to make particular data available to all components throughout the component tree no matter how deeply nested that component may be.
Let us look at this example:
const App = () => {
return(
<ParentComponent theme = "light"/>
);
}
const ParentComponent = (props) => (
<Child theme = {props.theme} />
)
const Child = (props) => (
<Grandchild theme = {props.theme} />
)
const Grandchild = (props) => (
<p>Theme: {props.theme}</p>
)
In the example above, we specified the application theme using a props in the ParentComponent
called theme
. We had to pass that props to all components down the component tree to get it where it is needed which is the GrandChild
component. The ChildComponent
had nothing to do with the theme props but was just used as an intermediary.
Now, imagine the GrandChild
component was more deeply nested than it was in the top example. We would have to pass the theme props the same way we did here which would be cumbersome. This is the problem that Context
solves. With Context
, every component in the component tree has access to whatever data we decide to put in our context.
Let’s Get Started With Context
It’s time to replicate the theme toggling button we built at the beginning of the article with the Context API. This time, our theme toggler will be a separate component. We will build a ThemeToggler
component which switches the theme of our React app using Context
.
First, let us initialize our React app. (I prefer using create-react-app
but you can use whatever method you prefer.)
Once you have initialized your React project, create a file called ThemeContext.js in your /src
folder. You can also create a folder called /context
and place your ThemeContext file in there if you want.
Now, let us move on.
Creating Your Context API
We will create our theme context in our ThemeContext.js file.
To create a context, we use React.createContext
which creates a context object. You can pass in anything as an argument to React.createContext
. In this case, we are going to pass in a string which is the current theme mode. So now our current theme mode is the “light” theme mode.
import React from "react";
const ThemeContext = React.createContext("light");
export default ThemeContext;
To make this context available to all our React components, we have to use a Provider. What is a Provider? According to the React documentation, every context object comes with a Provider React component that allows consuming components to subscribe to context changes. It is the provider that allows the context to be consumed by other components. That said, let us create our provider.
Go to your App.js file. In order to create our provider, we have to import our ThemeContext
.
Once the ThemeContext
has been imported, we have to enclose the contents of our App
component in ThemeContext.Provider
tags and give the ThemeContext.Provider
component a props called value
which will contain the data we want to make available to our component tree.
function App() {
const theme = "light";
return (
<ThemeContext.Provider value = {themeHook}>
<div>
</div>
</ThemeContext.Provider>
);
}
So now the value of “light” is available to all our components (which we will write soon).
Creating Our Theme File
Now, we will create our theme file that will contain the different color values for both our light and dark themes. Create a file in your /src
folder called Colors.js.
In Colors.js, we will create an object called AppTheme
. This object will contain the colors for our themes. Once you are done, export the AppTheme
object like so:
const AppTheme = {
light: {
textColor: "#000",
backgroundColor: "#fff"
},
dark: {
textColor: "#fff",
backgroundColor: "#333"
}
}
export default AppTheme;
Now it’s time to start creating our different React components.
Creating Our React Components
Let’s create the following components:
Header
ThemeToggler
MainWithClass
Header.jsx
import React from "react";
import ThemeToggler from "./ThemeToggler";
const headerStyles = {
padding: "1rem",
display: "flex",
justifyContent: "space-between",
alignItems: "center"
}
const Header = () => {
return(
<header style = {headerStyles}>
<h1>Context API</h1>
<ThemeToggler />
</header>
);
}
export default Header;
ThemeToggler.jsx
(For now, we will just return an empty div
.)
import React from "react";
import ThemeContext from "../Context/ThemeContext";
const themeTogglerStyle = {
cursor: "pointer"
}
const ThemeToggler = () => {
return(
<div style = {themeTogglerStyle}>
</div>
);
}
export default ThemeToggler;
Consuming Context With Class-Based Components
Here, we will use the value of our ThemeContext
. As you may already know, we have two methods of writing components in React: through functions or classes. The process of use context in both methods is different so we will create two components to serve as the main section of our application: MainWithClass
and MainWithFunction
.
Let us start with MainWithClass
.
MainWithClass.jsx
We will have to import our ThemeContext
and AppTheme
. Once that is done, we will write a class that returns our JSX from a render method. Now we have to consume our context. There are two methods to do this with class-based components:
- The first method is through
Class.contextType
.
To use this method, we assign the context object from our ThemeContext
to contextType
property of our class. After that, we will be able to access the context value using this.context
. You can also reference this in any of the lifecycle methods and even the render method.
import React, { Component } from "react";
import ThemeContext from "../Context/ThemeContext";
import AppTheme from "../Colors";
class Main extends Component{
constructor(){
super();
}
static contextType = ThemeContext;
render(){
const currentTheme = AppTheme[this.context];
return(
<main></main>
);
}
}
After assigning ThemeContext
to the contextType
property of our class, I saved the current theme object in the currentTheme
variable.
Now, we will grab the colors from the currentTheme
variable and use them to style some markup.
render() {
const currentTheme = AppTheme[this.context];
return (
<main style={{
padding: "1rem",
backgroundColor: `${currentTheme.backgroundColor}`,
color: `${currentTheme.textColor}`,
}}>
<h1>Heading 1</h1>
<p>This is a paragraph</p>
<button> This is a button</button>
</main>
That’s it! This method, however, limits you to consuming only one context.
- The second method is
ThemeContext.Consumer
that involves the use of a Consumer. Each context object also comes with a Consumer React component which can be used in a class-based component. The consumer component takes a child as a function and that function returns a React node. The current context value is passed to that function as an argument.
Now, let us replace the code in our MainWithClass
component with this:
class Main extends Component {
constructor() {
super();
this.state = {
}
}
render(){
return(
<ThemeContext.Consumer>
{
(theme) => {
const currentTheme = AppTheme[theme];
return(
<main style = {{
padding: "1rem",
backgroundColor: `${currentTheme.backgroundColor}`,
color: `${currentTheme.textColor}`,
}}>
<h1>Heading 1</h1>
<p>This is a paragraph</p>
<button> This is a button</button>
</main>
)
}
}
</ThemeContext.Consumer>
);
}
}
As you can see, we used the current value of our ThemeContext
which we aliased as “theme” and we grabbed the color values for that theme mode and assigned it to the variable currentTheme
. With this method, you can use multiple Consumers.
Those are the two methods of consuming context with class-based components.
Consuming Context With Functional Components
Consuming context with functional components is easier and less tedious than doing so with class-based components. To consume context in a functional component, we will use a hook called useContext
.
Here is what consuming our ThemeContext
with a functional component would look like:
const Main = () => {
const theme = useContext(ThemeContext);
const currentTheme = AppTheme[theme];
return(
<main style = {{
padding: "1rem",
backgroundColor: `${currentTheme.backgroundColor}`,
color: `${currentTheme.textColor}`,
}}>
<h1>Heading 1</h1>
<p>This is a paragraph</p>
<button> This is a button</button>
</main>
);
}
export default Main;
As you can see, all we had to do was use our useContext
hook with our ThemeContext
passed in as an argument.
Note: You have to use these different components in the App.js file in order to see the results.
Updating Our Theme With The ThemeToggler
Component
Now we are going to work on our ThemeToggler
component. We need to be able to switch between the light and dark themes. To do this, we are going to need to edit our ThemeContext.js. Our React.createContext
will now take an object resembling the result of a useState
hook as an argument.
const ThemeContext = React.createContext(["light", () => {}]);
We passed an array to the React.createContext
function. The first element in the array is the current theme mode and the second element is the function that would be used to update the theme. As I said, this just resembles the result of a useState
hook but it is not exactly the result of a useState
hook.
Now we will edit our App.js file. We need to change the value passed to the provider to a useState
hook. Now the value of our Theme Context is a useState
hook whose default value is “light”.
function App() {
const themeHook = useState("light");
return (
<ThemeContext.Provider value = {themeHook}>
<div>
<Header />
<Main />
</div>
</ThemeContext.Provider>
);
}
Writing Our ThemeToggler
Component
Let us now actually write our ThemeToggler
component:
import React,{useContext} from "react";
import ThemeContext from "../Context/ThemeContext";
const themeTogglerStyle = {
cursor: "pointer"
}
const ThemeToggler = () => {
const[themeMode, setThemeMode] = useContext(ThemeContext);
return(
<div style = {themeTogglerStyle} onClick = {() => {setThemeMode(themeMode === "light"? "dark": "light")}}>
<span title = "switch theme">
{themeMode === "light" ? "🌙" : "☀️"}
</span>
</div>
);
}
export default ThemeToggler;
Since the value of our theme context is now a hook whenever we call useContext
on it, it will return an array. Using destructuring, we were able to grab the elements from the array. We then wrote an onClick
event handler for our ThemeToggler
. With that code, whenever the theme toggler is clicked, it will switch the theme of our application.
Now we will edit the different versions of our Main
component.
Editing Our MainWithClass
Component
- The version of the
MainWithClass
component that uses the Class.contextType
method:
import React, { Component } from "react";
import ThemeContext from "../Context/ThemeContext";
import AppTheme from "../Colors";
class Main extends Component{
constructor(){
super();
}
static contextType = ThemeContext;
render(){
const currentTheme = AppTheme[this.context[0]];
return(
<main style={{
padding: "1rem",
backgroundColor: `${currentTheme.backgroundColor}`,
color: `${currentTheme.textColor}`,
}}>
<h1>Heading 1</h1>
<p>This is a paragraph</p>
<button> This is a button</button>
</main>
);
}
}
- The version of the
MainWithClass
component that uses the ThemeContext.Consumer
method:
import React, { Component } from "react";
import ThemeContext from "../Context/ThemeContext";
import AppTheme from "../Colors";
class Main extends Component {
constructor() {
super();
this.state = {}
}
render() {
return (
<ThemeContext.Consumer>
{
([theme]) => {
const currentTheme = AppTheme[theme];
return(
<main style = {{
padding: "1rem",
backgroundColor: `${currentTheme.backgroundColor}`,
color: `${currentTheme.textColor}`,
}}>
<h1>Heading 1</h1>
<p>This is a paragraph</p>
<button> This is a button</button>
</main>
)
}
}
</ThemeContext.Consumer>
);
}
}
export default Main;
Editing Our MainWithFunction
Component
The MainWithFunction
Component should be edited as the following:
import React, { useContext } from "react";
import ThemeContext from "../Context/ThemeContext";
import AppTheme from "../Colors";
const Main = () => {
const theme = useContext(ThemeContext)[0];
const currentTheme = AppTheme[theme];
return(
<main style = {{
padding: "1rem",
backgroundColor: `${currentTheme.backgroundColor}`,
color: `${currentTheme.textColor}`,
}}>
<h1>Heading 1</h1>
<p>This is a paragraph</p>
<button> This is a button</button>
</main>
);
}
export default Main;
Conclusion
That’s it! We have succeeded in implementing two theme modes for our React app using the Context API.
In the process, we have learned:
- What the Context API is and the problem it solves;
- When to use the Context API;
- Creating
Context
and consuming it in both functional and class-based components.
Further Reading on SmashingMag:
(dm, il)