A Step-by-Step Guide to Managing State with React 18 and Context API

Jean F Beaulieu
7 min readMar 7, 2023

--

Vancouver Island, BC, Canada

Abstract

State management helps developers manage their application’s data more effectively by providing them with the ability to store, retrieve, and modify state information in an organized manner. It also allows developers to cache data so that they don’t have to make multiple requests each time they need it. With the right state management strategy, developers can create more efficient applications that can handle complex tasks without sacrificing performance or scalability.

What is the Context API and How Does it Work?

The React Context API is a powerful tool for managing state in React applications. It allows developers to access data from any component in the application, without having to pass props down through multiple levels of components. This makes it easier to manage complex state and keep track of changes. The Context API also provides a way to share data between components without having to use Redux, Flux or MobX. This makes it an ideal choice for developers who want an easy-to-use solution for state management in their React apps.

Step 1: Setting up the React Project with Create React App

Create React App is a great tool for setting up a React project quickly and efficiently. It allows developers to focus on the code without worrying about complicated configurations and setup. With Create React App, developers can create a project with just one command, install all the dependencies with ease, and get started coding right away.

Refer to one of my previous articles for the steps to create a new React app using Create React App

Step 2: Understanding the Basics of the Context API

The Context API is a powerful tool for developers to help them create more efficient and modular applications. It allows developers to store data in an easy-to-access way and pass it around components without having to manually pass the data down the component tree. Understanding the basics of the Context API can help developers create more robust and scalable applications.

The Context API provides a way for developers to store global state within their application, which makes it easier for them to access and update data across different components. It also helps with code organization, as all of the related data is stored in one place instead of being scattered across multiple components. With this understanding, developers can use the Context API to simplify their codebase while ensuring that all relevant data is accessible from any component within their application.

Here is a basic example of how to use the Context API coming from the React official website:

// Context lets us pass a value deep into the component tree
// without explicitly threading it through every component.
// Create a context for the current theme (with "light" as the default).
const ThemeContext = React.createContext('light');

class App extends React.Component {
render() {
// Use a Provider to pass the current theme to the tree below.
// Any component can read it, no matter how deep it is.
// In this example, we're passing "dark" as the current value.
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
}

// A component in the middle doesn't have to
// pass the theme down explicitly anymore.
function Toolbar() {
return (
<div>
<ThemedButton />
</div>
);
}

class ThemedButton extends React.Component {
// Assign a contextType to read the current theme context.
// React will find the closest theme Provider above and use its value.
// In this example, the current theme is "dark".
static contextType = ThemeContext;
render() {
return <Button theme={this.context} />;
}
}

Step 3: Creating a Custom Provider Component

Creating a custom provider component is a great way to extend the functionality of your application and make it more flexible. It allows you to create components that can be used in different parts of the application and provide custom functionality for each one. With this, you can easily add new features or modify existing ones without having to rewrite the entire codebase. By creating a custom provider component, you can also reduce development time and cost, as well as increase scalability and maintainability of your application.

Here are all of the important stuff. We are going to create a new Provider that will be used to return not one but two contexts created with the Context API 🤓 The Provider component will contain a reducer that will be used to make changes to the state. A reducer needs two things, a state and an action. It returns a new state with the applied changes coming from the action and depending on the action type. And now the real science happens ⚗️🧪🔬 where we create two different contexts using the react hook createContext then later use them with the react hook useContext. We are creating a reducer inside of our Provider using the react hook useReducer. This will return to us two things, a state and a dispatch callback method that we will then assign to each of our two newly created context Providers:

import React, { createContext, useReducer, useContext } from "react";
import { AuthenticationReducer } from "../../reducers/AuthenticationReducer";

export interface IAction {
type: AuthenticationActionTypes;
payload?: any;
}

export interface IUserData {
userName: string;
userEmail: string;
}

export interface IState {
isAuthenticated: boolean;
userData: IUserData;
}

type Dispatch = (action: IAction) => void;

// separate contexts for the state and dispatch
const AuthenticationStateContext = createContext<IState | undefined>(undefined);
const AuthenticationDispatchContext = createContext<Dispatch | undefined>(undefined);

// custom hook for the state
export const useAuthenticationStateContext = () => {
const context = useContext(AuthenticationStateContext);
if (context === undefined) {
throw new Error("useAuthenticationStateContext must be used within a AuthenticationStateContext.Provider");
}
return context;
};

// custom hook for the dispatch
export const useAuthenticationDispatchContext = () => {
const context = useContext(AuthenticationDispatchContext);
if (context === undefined) {
throw new Error("useAuthenticationDispatchContext must be used within a AuthenticationDispatchContext.Provider");
}
return context;
};

export const AuthenticationProvider = ({ children }: Props) => {
const [state, dispatch] = useReducer(AuthenticationReducer, initialState);

return (
<AuthenticationStateContext.Provider value={state}>
<AuthenticationDispatchContext.Provider value={dispatch}>{children}</AuthenticationDispatchContext.Provider>
</AuthenticationStateContext.Provider>
);
};

Step 4: Implementing Reducers and Actions for State Management

Reducers and actions are essential components of state management. They are used to store and modify the application’s state in a predictable way. By implementing reducers and actions, developers can ensure that their applications remain responsive and efficient even when dealing with large amounts of data.

Reducers define how the application’s state should be modified in response to various actions. Actions, on the other hand, are dispatched by components to trigger changes in the application’s state. Implementing reducers and actions correctly is essential for ensuring that your application remains performant and bug-free. 🐞

Here is an example of a simple reducer used for state management in our react application. Only one action is shown, which is the “set logged employee” action. Since we know the type of action, we know how to create a new state from the action payload:

import { IState, IAction, AuthenticationActionTypes } from "../components/providers/AuthenticationProvider";

export const AuthenticationReducer = (state: IState, action: IAction): IState => {
switch (action.type) {
case AuthenticationActionTypes.SET_LOGGED_EMPLOYEE: {
console.log("Action: SET_LOGGED_EMPLOYEE");
return {
...state,
action.payload
};
}
}
};

Step 5: Connecting Components to the Provider Component

Connecting components to the provider component is an important step in developing a react application. It allows for the integration of different components into one unified system. This process involves connecting each component to the provider component so that they can communicate with each other and share data. The provider component acts as a bridge between all of the components, allowing them to interact with each other and access shared resources. By connecting components to the provider component, developers can create a more efficient and reliable system that is easier to maintain and update over time. ⏳

Here is an example react component in the form of a simple login form. There is a login button and a logout button, with the two corresponding actions being dispatched when the buttons are clicked. 🤯

import { ActionTypes } from "../../layout/Main/Contracts";
import { useAuthenticationStateContext, useAuthenticationDispatchContext, AuthenticationActionTypes } from "../../providers/AuthenticationProvider";

type Props = {};

const AuthenticationComponent: FC<Props> = () => {
const authenticationState = useAuthenticationStateContext();
const authenticationDispatch = useAuthenticationDispatchContext();

const handleLoginButtonClick = () => {
authenticationDispatch({
type: AuthenticationActionTypes.SET_LOGGED_EMPLOYEE,
payload: {
isAuthenticated: true,
userData: {
name: "Test User",
email: "test@1234.com",
},
},
});
};

const handleLogoutButtonClick = () => {
authenticationDispatch({
type: AuthenticationActionTypes.SET_LOGGED_EMPLOYEE,
payload: {
isAuthenticated: false,
userData: null
},
});
};

return (
<div className="container">
<div className="row">
<div className="col-12">
<button className="btn btn-primary" onClick={handleLoginButtonClick}>
Login
</button>
<button className="btn btn-primary" onClick={handleLogoutButtonClick}>
Logout
</button>
</div>
</div>
<div className="row">
<div className="col-12">
<p>Is authenticated: {authenticationState.isAuthenticated ? "true" : "false"}</p>
<p>Logged user name: {authenticationState.userData?.name}</p>
<p>Logged user email: {authenticationState.userData?.email}</p>
</div>
</div>
</div>
);
};

export default AuthenticationComponent;

To use the newly created provider, simply add the component to your App.ts file:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import AuthenticationProvider from "../components/providers/AuthenticationProvider";

ReactDOM.render(
<>
<AuthenticationProvider>
<App />
</AuthenticationProvider>
</>
);

Conclusion

React has been a great solution for web development for many years now. With the introduction of the Context API and robust state management, React is becoming even more powerful and easier to use. As developers continue to explore new ways of using React, we can expect its future to be even brighter. The Context API provides a great way to manage state in an application, while also making it easy for developers to create complex user interfaces with minimal effort. With its robust state management and easy-to-use features, React is sure to remain a popular choice among web developers in the years ahead. 🚀

--

--