A simple example of React useContext and useReducer Hooks available in React that can be used as a replacement for Redux.
Instead of using Redux as state management. We can use the inbuilt hooks that are available in React itself. Eventually, you can replace or move the project that is dependent on Redux to the inbuilt hooks.
The useContext hook is used to create common data that can be accessed throughout the component hierarchy without passing the props down manually to each level. Context defined will be available to all the child components without involving “props”. React Context is a powerful state management feature in React. Instead of passing the props down through each component, React Context allows you to broadcast props to the components below.
The useReducer hook is used for complex state manipulations and state transitions. … useReducer is a React hook function that accepts a reducer function, and an initial state. const [state, dispatch] = useReducer(reducer, initialState);This hook function returns an array with 2 values.
I am using the usual use case of the Todo List example for easy understanding.
Step 1: Initial State and Actions
//Initial State and Actions
const initialState = {
todoList: []
};
const actions = {
ADD_TODO_ITEM: "ADD_TODO_ITEM",
REMOVE_TODO_ITEM: "REMOVE_TODO_ITEM",
TOGGLE_COMPLETED: "TOGGLE_COMPLETED"
};
Step 2: Reducers to Handle Actions
//Reducer to Handle Actions
const reducer = (state, action) => {
switch (action.type) {
case actions.ADD_TODO_ITEM:
return {
todoList: [
...state.todoList,
{
id: new Date().valueOf(),
label: action.todoItemLabel,
completed: false
}
]
};
case actions.REMOVE_TODO_ITEM: {
const filteredTodoItem = state.todoList.filter(
(todoItem) => todoItem.id !== action.todoItemId
);
return { todoList: filteredTodoItem };
}
case actions.TOGGLE_COMPLETED: {
const updatedTodoList = state.todoList.map((todoItem) =>
todoItem.id === action.todoItemId
? { ...todoItem, completed: !todoItem.completed }
: todoItem
);
return { todoList: updatedTodoList };
}
default:
return state;
}
};
Breakdown of the Code: We use the usual Switch Case Statements to Evaluate the Actions.
- First Case ADD_TODO_ITEM -action spread the existing list and add a new todo item to the list with id(unique-ish), label(user-entered value), and completed flag.
- Second Case REMOVE_TODO_ITEM -action filter out the to-do item that needs to be removed based on the id.
- Third Case TOGGLE_COMPLETED - action loop through all the to-do items and toggle the completed flag based on the id.
Step 3: Create the Context and Provider to Dispatch the Actions.
//Context and Provider
const TodoListContext = React.createContext();
const Provider = ({ children }) => {
const [state, dispatch] = React.useReducer(reducer, initialState);
const value = {
todoList: state.todoList,
addTodoItem: (todoItemLabel) => {
dispatch({ type: actions.ADD_TODO_ITEM, todoItemLabel });
},
removeTodoItem: (todoItemId) => {
dispatch({ type: actions.REMOVE_TODO_ITEM, todoItemId });
},
markAsCompleted: (todoItemId) => {
dispatch({ type: actions.TOGGLE_COMPLETED, todoItemId });
}
};
return (
<TodoListContext.Provider value={value}>
{children}
</TodoListContext.Provider>
);
};
In this step, we create the TodoListContext and a Provider function that returns the TodoListContext’s Provider.
Here is the Breakdown of the Code.
- Here we pass the reducer function and theinitialState to the useReducer hook. This will return state and dispatch. The state will have the initialState. And the dispatch is used to trigger our actions, just like in redux.
- In the value object, we have todoList state, and three functions addTodoItem, removeTodoItem, and markAsCompleted which trigger ADD_TODO_ITEM, REMOVE_TODO_ITEM, and TOGGLE_COMPLETED actions respectively.
- We pass the value object as a prop to the TodoListContext's Provider, so that we can access it using useContext.
Step 4: Create the Two Components that will use the store.
AddTodo & TodoList
// AddTodo Component with Input field and Add Button
const AddTodo = () => {
const [inputValue, setInputValue] = React.useState("");
const { addTodoItem } = React.useContext(TodoListContext);
return (
<>
<input
type="text"
value={inputValue}
placeholder={"Type and add todo item"}
onChange={(e) => setInputValue(e.target.value)}
/>
<button
onClick={() => {
addTodoItem(inputValue);
setInputValue("");
}}
>
Add
</button>
</>
);
};
In thisAddTodocomponent, we use the useContext to subscribe to our TodoListContext and getting addTodoItem dispatch function.
//TodoList Component to show the list
const TodoList = () => {
const { todoList, removeTodoItem, markAsCompleted } = React.useContext(
TodoListContext
);
return (
<ul>
{todoList.map((todoItem) => (
<li
className={`todoItem ${todoItem.completed ? "completed" : ""}`}
key={todoItem.id}
onClick={() => markAsCompleted(todoItem.id)}
>
{todoItem.label}
<button
className="delete"
onClick={() => removeTodoItem(todoItem.id)}
>
X
</button>
</li>
))}
</ul>
);
};
In TodoList component, we are using useContext to subscribe to the TodoListContext and getting the todoList state, removeTodoItemand andmarkAsCompleted dispatch functions. We are mapping through todoList and rendering the to-do items and a remove(X) button next to them. On clicking on an item we are marking it as complete and when clicking on X the button we are removing it from the list.
Step 5: Final step, wrapping the above two components to the Provider.
//Final Wrapper
export default function App() {
return (
<Provider>
<AddTodo />
<TodoList />
</Provider>
);
}
Here’s the working sample version of the code and the preview. You can Open the Sandbox for the full version of the code.
Top comments (10)
While this is a bare state management mechanism, it is not a replacement for redux. Here all components subscribed to the context will rerender when any value in the context changes.
When you use redux, only the components subscribed to that particular state rerenders on state change - even though there is a single global store.
Good point. Is there a way to use something to prevent the subscribed components to rerender if not needed? Thinking about something like memo where it does shallow comparison on props object?
What if the context api is used with reducer? more complex but interesting thing to do.
Well, nice article.
Now, I'm missing the part with async actions...
Some other article I read suggested for better performance to divide the Store context and the Dispatch event context as whenever the context changes all components rerenders. But these dispatch methods shouldn't change over runtime...
@ IDURAR , we use react context api for all UI parts , and we keep our data layer inside redux .
Here Article about : 🚀 Mastering Advanced Complex React useContext with useReducer ⭐ (Redux like Style) ⭐ : dev.to/idurar/mastering-advanced-c...
That's awesome. I myself don't really like Redux and I recently used a similar solution in one of my applications. When using Websocket, this is a game changer.
Thanks. Yes, React is evolving pretty good by reducing the dependencies on other libraries. Will post a new featured article about the latest changes very soon.
Thanks a lot for sharing 💖
Good read.
Question: what is the cons and pros between using useReducer with Context API vs using simple useState hook?
useState is not recommended for storing object state