Functional components have undoubtedly made React a powerful tool to create web interfaces.
In this article we will look at how we can create our own state management system like Redux with the help of Context API and useReducer hook in React.js
Before diving into the code, let us see what Context API and useReducer hook are?
Context API
Context API provides a way to share the state with different components in a component tree without manually passing the props through every level.
In React, you can create context by using createContext
method. It requires initial state as an argument.
for example:
import { createContext } from 'react'
const initialState = { count: 0 }
const context = createContext(initialState)
useReducer Hook
If you are familiar with state management systems like Redux you should be familiar with Reducers. Reducers are pure functions that takes previous state and an action and returns the new state
useReducer
hook can imported from react and it takes two parameters, Reducer function and initial state, and it returns the dispatch function and new state
for example:
import { useReducer } from 'react'
const ReducerFunction = (prevState, action) => {
// this function holds all the logic of how you want your state to be updated
}
const [state, dispatch] = useReducer(ReducerFunction, initialState)
Now that we know, what Context API and the useReducer hook is, let us use both and build a simple todo app.
First let's create a new react app, we will use Vite to create a new react app
Run the following command:
yarn create vite todo-app --template react
Notice that I am using
yarn
as package installer, if you prefer package installer other thanyarn
make sure to read the docs for creating react app using vite.
After creating react app. Run the following commands:
cd todo-app
yarn
This will install all the dependencies.
After that, run yarn dev
to start a local server
I am going to create two folders inside src
folder, components
folder and context
folder.
In the components
folder create a file named AddTodo.jsx
Inside AddTodo.jsx
, create a simple input field and button to add todo
for example:
import { useState } from 'react'
const AddTodo = () => {
const [todo, setTodo] = useState("")
return (
<div>
<input
type="text"
value={todo}
onChange={(e) => setTodo(e.target.value) }
/>
<button>ADD TODO</button>
</div>
)
}
export default AddTodo
Notice that I have also created a state to handle input field.
Now, create a new file named TodosContext.jsx
inside the context
folder
Inside TodosContext.jsx
we will first create the context for our todos as follows:
import { createContext } from 'react'
const initialState = { todos: [] }
export const TodosContext = createContext(initialState)
Next we will create a provider component that will provide the states to all the components wrapped around it
for example in the same TodosContext.jsx
file:
// Prev code
const TodosContextProvider = ({children}) => {
return (
<TodosContext.Provider
value={{
todos: [] // empty for now, here the new state from useReducer hook will come
}}
>
{children}
</TodosContext.Provider>
);
}
export default TodosContextProvider
Inside your App.jsx
file update the code to the following:
import AddTodo from "./components/AddTodo";
import TodosContextProvider from "./context/TodosContext";
import "./styles.css";
export default function App() {
return (
<TodosContextProvider>
<div className="App">
<AddTodo />
</div>
</TodosContextProvider>
);
}
Now create a new file name TodosReducer.js
inside the context
folder.
In this file we will create the Reducer function that will be responsible for updating the state based on the action provided to it
Write the following code in TodosReducer.js
file
export const TodosReducer = (prevState, action) => {
switch (action.type) {
case "ADD_TODO": {
return {
...prevState,
todos: [...prevState.todos, action.payload]
};
}
default: {
return prevState;
}
}
};
Notice that action
object have two properties action.type
that defines the action you want to perform on the state and action.payload
that is optional and depends on how you want your state to be updated. In our case we are appending action.payload
at the end of todos array, So this will be our new todo.
Now, head back to the TodosContext.jsx
file and import the TodosReducer
function and useReducer hook:
import { useReducer } from 'react'
import { TodosReducer } from './TodosReducer.js'
and inside the TodosContextProvider
component update the following code:
// prev code
const TodosContextProvider = ({ children }) => {
const [state, dispatch] = useReducer(TodosReducer, initialState)
const addTodo = (todo) => {
dispatch({ type: "ADD_TODO", payload: todo })
}
return (
<TodosContext.Provider
value={{
todos: state.todos,
addTodo
}}
>
{children}
</TodosContext.Provider>
);
}
export default TodosContextProvider
Create a new file name Todos.jsx
inside components folder and add the following code:
import { useContext } from "react";
import { TodosContext } from "../context/TodosContext";
const Todos = () => {
const { todos } = useContext(TodosContext);
return (
<ul>
{todos.map((todo, i) => {
return (
<div key={i}
<li>{todo}</li>
</div>
);
})}
</ul>
);
};
export default Todos;
Notice that we are using useContext
hook from react, giving it the context we created earlier and it will return the todos state and addTodo function back to us. But in Todos.jsx
we only need todos
state so we destructured it from the returned object and map it as list items.
Now head back to the AddTodo.jsx
file and add the following code:
import { useState, useContext } from 'react'
import { TodosContext } from '../context/TodosContext'
const AddTodo = () => {
const [todo, setTodo] = useState("")
const { addTodo } = useContext(TodosContext)
return (
<div>
<input
type="text"
value={todo}
onChange={(e) => setTodo(e.target.value) }
/>
<button onClick={() => addTodo(todo)}>ADD TODO</button>
</div>
)
}
export default AddTodo
In this file we have used the context to get the addTodo method that will take a todo and will trigger a dispatch function with the action type of "ADD_TODO" which will then update the todos
state and the new added todo will be reflected in the Todos.jsx
file.
So that is how you use the Context API and useReducer hook to manage the states. Although it was a simple example but imagine a large application with hundreds of states. It will be very hard to manage all those state by using useState hook and passing those states through props to other components.
With the help of Context API and useReducer hook the state management in React became much easier without the need of external libraries like Redux.
Below is the codesandbox for code above:
Top comments (0)