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 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 article, you are 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 of function components, while the Context API allows you to share state with other components.
The technique you will 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 am currently using Visual Studio Code, which seems to be the most popular code editor right now (especially for JavaScript developers). If you are 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.
There are two types of state that we need to deal with in React projects:
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 re-use and easier ways of sharing state between components. For this tutorial, we will be concerned with the following React hooks:
useState
useReducer
useState
is recommended for handling simple values like numbers or strings. However, when it comes to handling complex data structures, you will need useReducer
hook. For useState
, you only need to have a single setValue()
function for overwriting existing state values.
For useReducer
, you will be handling a state object that contains multiple values with different data types in a tree-like structure. You will need to declare functions that can change one or more of these state values. For data types such as arrays, you will 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 article.
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 will 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. Any child component of the container will be able to access the context object using the useContext
function. Enough chit-chat, let’s see the code in action.
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 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.
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
. 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 have 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 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 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 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 this one:
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 command yarn start
. The browser should start which should render a similar view. Click the buttons to ensure that increment
and decrement
functions are working:
.
Let’s go the next section where we’ll setup an example that is a bit more advanced using the useReducer
hook.
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 will have to use the useReducer
hook.
Create the state context object 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 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 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 = 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 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 ContactView from "./views/contact-view";
//...
<Container>
<h1>React Hooks Context Demo</h1>
{/* <CounterView /> */}
<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.
Go over the code to make sure you understand everything. Read the comments that I’ve included inside the code.
I hope these examples provide an excellent understanding of how you can manage shared application state 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, i.e. 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 the future with the development of a new tool. Obviously, as a developer, you will still need to learn Redux in order to maintain legacy projects. But if you are starting a new project, you will need to ask yourself and your team if using a third-party state management library is really necessary for your case.
You can access the complete project used in this tutorial for this GitHub Repository.
Originally published by Michael Wanyoike at https://www.sitepoint.com
#reactjs #redux #api #javascript #webdev