How to Replace Redux with React Hooks and the React Context API
The most popular way to handle shared application state in React is using a framework such as Redux. Quite recently, the React team introduced several new features which include React Hooks and the React Context API. These two features effectively eliminated a lot of challenges that developers of large React projects have been facing. One of the biggest problems was “prop drilling”, which was common with nested components. The solution was to use a state management library like Redux. This, unfortunately, came with the expense of writing boilerplate code. But now it’s possible to replace Redux with React Hooks and the Context API.
In this tutorial, you’re going to learn a new way of handling state in your React projects, without writing excessive code or installing a bunch of libraries — as is the case with Redux. React hooks allow you to use local state inside function components, while the Context API allows you to share state with other components.
Prerequisites
In order to follow along with this tutorial, you’ll need to be familiar with the following topics:
The technique you’ll learn here is based on patterns that were introduced in Redux. This means you need to have a firm understanding of reducers
and actions
before proceeding. I’m currently using Visual Studio Code, which seems to be the most popular code editor right now (especially for JavaScript developers). If you’re on Windows, I would recommend you install Git Bash. Use the Git Bash terminal to perform all commands provided in this tutorial. Cmder is also a good terminal capable of executing most Linux commands on Windows.
You can access the complete project used in this tutorial at this GitHub Repository.
About the New State Management Technique
There are two types of state we need to deal with in React projects:
- local state
- global state
Local states can only be used within the components where they were defined. Global states can be shared across multiple components. Previously, defining a global state required the installation of a state management framework such as Redux or MobX. With React v16.3.0, the Context API was released, which allows developers to implement global state without installing additional libraries.
As of React v16.8, Hooks have allowed implementation of a number of React features in a component without writing a class. Hooks brought vast benefits to the way React developers write code. This includes code reuse and easier ways of sharing state between components. For this tutorial, we’ll be concerned with the following React hooks:
useState
is recommended for handling simple values like numbers or strings. However, when it comes to handling complex data structures, you’ll need the useReducer
hook. For useState
, you only need to have a single setValue()
function for overwriting existing state values.
For useReducer
, you’ll be handling a state object that contains multiple values with different data types in a tree-like structure. You’ll need to declare functions that can change one or more of these state values. For data types such as arrays, you’ll need to declare multiple immutable functions for handling add, update and delete actions. You’ll see an example of this in a later section of this tutorial.
Once you declare your state using either useState
or useReducer
, you’ll need to lift it up to become global state using React Context. This is done by creating a Context Object using the createContext
function provided by the React library. A context object allows state to be shared among components without using props.
You’ll also need to declare a Context Provider for your context object. This allows a page or a container component to subscribe to your context object for changes to the current context value. Any child component of the container will be able to access the context object using the useContext
function.
Now let’s see the code in action.
Setting Up the Project
We’ll use create-react-app to jump-start our project quickly:
$ npx create-react-app react-hooks-context-app
Next, let’s install Semantic UI React, a React-based CSS framework. This isn’t a requirement; I just like creating nice user interfaces without writing custom CSS:
yarn add semantic-ui-react fomantic-ui-css
Open src/index.js
and insert the following import:
import 'fomantic-ui-css/semantic.min.css';
That’s all we need to do for our project to start using Semantic UI. In the next section, we’ll look at how we can declare a state using the useState
hook and uplifting it to global state.
Counter Example: useState
For this example, we’ll build a simple counter demo consisting of a two-button component and a display component. We’ll introduce a count
state that will be shared globally among the two components. The components will be a child of CounterView
, which will act as the container. The button component will have buttons that will either increment or decrement the value of the count
state.
Let’s start by defining our count
state in a context file called context/counter-context.js
. Create this inside the src
folder and insert the following code:
import React, { useState, createContext } from "react";
// Create Context Object
export const CounterContext = createContext();
// Create a provider for components to consume and subscribe to changes
export const CounterContextProvider = props => {
const [count, setCount] = useState(0);
return (
<CounterContext.Provider value={[count, setCount]}>
{props.children}
</CounterContext.Provider>
);
};
We’ve defined a state called count
and set the default value to 0
. All components that consume the CounterContext.Provider
will have access to the count
state and the setCount
function. Let’s define the component for displaying the count
state in src/components/counter-display.js
:
import React, { useContext } from "react";
import { Statistic } from "semantic-ui-react";
import { CounterContext } from "../context/counter-context";
export default function CounterDisplay() {
const [count] = useContext(CounterContext);
return (
<Statistic>
<Statistic.Value>{count}</Statistic.Value>
<Statistic.Label>Counter</Statistic.Label>
</Statistic>
);
}
Next, let’s define the component that will contain buttons for increasing and decreasing the state
component. Create the file src/components/counter-buttons.js
and insert the following code:
import React, { useContext } from "react";
import { Button } from "semantic-ui-react";
import { CounterContext } from "../context/counter-context";
export default function CounterButtons() {
const [count, setCount] = useContext(CounterContext);
const increment = () => {
setCount(count + 1);
};
const decrement = () => {
setCount(count - 1);
};
return (
<div>
<Button.Group>
<Button color="green" onClick={increment}>
Add
</Button>
<Button color="red" onClick={decrement}>
Minus
</Button>
</Button.Group>
</div>
);
}
As it is, the useContext
function won’t work since we haven’t specified the Provider. Let’s do that now by creating a container in src/views/counter-view.js
. Insert the following code:
import React from "react";
import { Segment } from "semantic-ui-react";
import { CounterContextProvider } from "../context/counter-context";
import CounterDisplay from "../components/counter-display";
import CounterButtons from "../components/counter-buttons";
export default function CounterView() {
return (
<CounterContextProvider>
<h3>Counter</h3>
<Segment textAlign="center">
<CounterDisplay />
<CounterButtons />
</Segment>
</CounterContextProvider>
);
}
Finally, let’s replace the existing code in App.js
with the following:
import React from "react";
import { Container } from "semantic-ui-react";
import CounterView from "./views/counter-view";
export default function App() {
return (
<Container>
<h1>React Hooks Context Demo</h1>
<CounterView />
</Container>
);
}
You can now fire up the create-react-app
server using the yarn start
command. The browser should start and render your counter. Click the buttons to ensure that increment
and decrement
functions are working.
You can also test this code on CodePen.
See the Pen
qBZYyqw by SitePoint (@SitePoint)
on CodePen.
Let’s go the next section, where we’ll set up an example that’s a bit more advanced using the useReducer
hook.
Contacts Example: useReducer
In this example, we’ll build a basic CRUD page for managing contacts. It will be made up of a couple of presentational components and a container. There will also be a context object for managing contacts state. Since our state tree will be a bit more complex than the previous example, we’ll have to use the useReducer
hook.
Create the state context object src/context/contact-context.js
and insert this code:
import React, { useReducer, createContext } from "react";
export const ContactContext = createContext();
const initialState = {
contacts: [
{
id: "098",
name: "Diana Prince",
email: "diana@us.army.mil"
},
{
id: "099",
name: "Bruce Wayne",
email: "bruce@batmail.com"
},
{
id: "100",
name: "Clark Kent",
email: "clark@metropolitan.com"
}
],
loading: false,
error: null
};
const reducer = (state, action) => {
switch (action.type) {
case "ADD_CONTACT":
return {
contacts: [...state.contacts, action.payload]
};
case "DEL_CONTACT":
return {
contacts: state.contacts.filter(
contact => contact.id !== action.payload
)
};
case "START":
return {
loading: true
};
case "COMPLETE":
return {
loading: false
};
default:
throw new Error();
}
};
export const ContactContextProvider = props => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<ContactContext.Provider value={[state, dispatch]}>
{props.children}
</ContactContext.Provider>
);
};
Create the parent component src/views/contact-view.js
and insert this code:
import React from "react";
import { Segment, Header } from "semantic-ui-react";
import ContactForm from "../components/contact-form";
import ContactTable from "../components/contact-table";
import { ContactContextProvider } from "../context/contact-context";
export default function Contacts() {
return (
<ContactContextProvider>
<Segment basic>
<Header as="h3">Contacts</Header>
<ContactForm />
<ContactTable />
</Segment>
</ContactContextProvider>
);
}
Create the presentation component src/components/contact-table.js
and insert this code:
import React, { useState, useContext } from "react";
import { Segment, Table, Button, Icon } from "semantic-ui-react";
import { ContactContext } from "../context/contact-context";
export default function ContactTable() {
// Subscribe to `contacts` state and access dispatch function
const [state, dispatch] = useContext(ContactContext);
// Declare a local state to be used internally by this component
const [selectedId, setSelectedId] = useState();
const delContact = id => {
dispatch({
type: "DEL_CONTACT",
payload: id
});
};
const onRemoveUser = () => {
delContact(selectedId);
setSelectedId(null); // Clear selection
};
const rows = state.contacts.map(contact => (
<Table.Row
key={contact.id}
onClick={() => setSelectedId(contact.id)}
active={contact.id === selectedId}
>
<Table.Cell>{contact.id}</Table.Cell>
<Table.Cell>{contact.name}</Table.Cell>
<Table.Cell>{contact.email}</Table.Cell>
</Table.Row>
));
return (
<Segment>
<Table celled striped selectable>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Id</Table.HeaderCell>
<Table.HeaderCell>Name</Table.HeaderCell>
<Table.HeaderCell>Email</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>{rows}</Table.Body>
<Table.Footer fullWidth>
<Table.Row>
<Table.HeaderCell />
<Table.HeaderCell colSpan="4">
<Button
floated="right"
icon
labelPosition="left"
color="red"
size="small"
disabled={!selectedId}
onClick={onRemoveUser}
>
<Icon name="trash" /> Remove User
</Button>
</Table.HeaderCell>
</Table.Row>
</Table.Footer>
</Table>
</Segment>
);
}
Create the presentation component src/components/contact-form.js
and insert this code:
import React, { useState, useContext } from "react";
import { Segment, Form, Input, Button } from "semantic-ui-react";
import _ from "lodash";
import { ContactContext } from "../context/contact-context";
export default function ContactForm() {
const name = useFormInput("");
const email = useFormInput("");
// eslint-disable-next-line no-unused-vars
const [state, dispatch] = useContext(ContactContext);
const onSubmit = () => {
dispatch({
type: "ADD_CONTACT",
payload: { id: _.uniqueId(10), name: name.value, email: email.value }
});
// Reset Form
name.onReset();
email.onReset();
};
return (
<Segment basic>
<Form onSubmit={onSubmit}>
<Form.Group widths="3">
<Form.Field width={6}>
<Input placeholder="Enter Name" {...name} required />
</Form.Field>
<Form.Field width={6}>
<Input placeholder="Enter Email" {...email} type="email" required />
</Form.Field>
<Form.Field width={4}>
<Button fluid primary>
New Contact
</Button>
</Form.Field>
</Form.Group>
</Form>
</Segment>
);
}
function useFormInput(initialValue) {
const [value, setValue] = useState(initialValue);
const handleChange = e => {
setValue(e.target.value);
};
const handleReset = () => {
setValue("");
};
return {
value,
onChange: handleChange,
onReset: handleReset
};
}
Insert the following code in App.js
accordingly:
import React from "react";
import { Container } from "semantic-ui-react";
import ContactView from "./views/contact-view";
export default function App() {
return (
<Container>
<h1>React Hooks Context Demo</h1>
<ContactView />
</Container>
);
}
After implementing the code, your browser page should refresh. To delete a contact, you need to select a row first then hit the Delete button. To create a new contact, simply fill the form and hit the New Contact button.
You can also test this code on CodePen.
See the Pen
React Hooks Context Demo — Contact List by SitePoint (@SitePoint)
on CodePen.
Go over the code to make sure you understand everything. Read the comments I’ve included inside the code.
Summary
I hope these examples help you well on the road to understanding how you can manage shared application state in a React application without Redux. If you were to rewrite these examples without hooks and the context API, it would have resulted in a lot more code. See how much easier it is to write code without dealing with props?
You may have noticed in the second example that there are a couple of unused state variables — loading
and error
. As a challenge, you can progress this app further to make use of them. For example, you can implement a fake delay, and cause the presentation components to display a loading status. You can also take it much further and access a real remote API. This is where the error
state variable can be useful in displaying error messages.
The only question you may want to ask yourself now: is Redux necessary for future projects? One disadvantage that I’ve seen with this technique is that you can’t use the Redux DevTool extension to debug your application state. However, this might change in future with the development of a new tool. Obviously, as a developer, you’ll still need to learn Redux in order to maintain legacy projects. But if you’re starting a new project, you’ll need to ask yourself and your team if using a third-party state management library is really necessary for your case.
FAQs about React Hooks and the React Context API
React Hooks are functions that allow you to “hook into” React components using state and lifecycle features in functional components. They provide a way to use state and other React features without writing class components. Hooks make code more readable and reusable.
You can use the useState hook to manage component state. It allows you to declare and set state variables in functional components, making it easy to handle component-specific data.
In addition to useState, React provides hooks like useEffect for handling side effects, useContext for accessing context data, and useRef for accessing and interacting with the DOM.
The React Context API is a mechanism for passing data through the component tree without having to pass props manually at every level. React Hooks, specifically useContext, can be used to consume context values within functional components.
Use the Context API when you have data that many components need to access and it would be cumbersome to pass data down as props through each level of the component tree. It’s particularly useful for global application state.
Yes, you can use Hooks like useState or useEffect in combination with the Context API. This allows you to manage local component state while also accessing shared context data.
You can create a new context with the createContext function. To pass data to your application, use the Context.Provider provider component and wrap your component tree with it, passing the data as a prop.
useContext is a hook that allows you to consume context directly in a functional component, making it more concise and straightforward. The Consumer component is used in class components and render props patterns and is less common in modern React applications.
In larger applications, it’s recommended to organize your context data and hooks in separate modules, creating a clear separation of concerns. This makes your codebase more maintainable and easier to test.
Alternatives to the Context API for state management include libraries like Redux, Mobx, Recoil, and Zustand. These libraries offer more advanced state management capabilities for larger and more complex applications.
Yes, you can use React Hooks and the Context API alongside state management libraries like Redux. They can complement each other, with Hooks and Context often being used for local component state, while Redux handles global application state.
To optimize performance, you can use the useMemo and useCallback hooks to memoize values and functions. This helps prevent unnecessary re-renders when context values change.