We are immensely familiar with hooks like useState
, useEffect
and useRef
a lot which allows us to use class-based components features now in functional components. But React hooks have one more weapon in its arsenal which can be an effective tool to optimise a react application: the useReducer
hook.
useReducer - a redux wannabe
The best description and example of the useReducer
hook can be found in the Official React docs. But if I have to explain it in a concise manner:
useReducer allows your react component to have a redux-like state
You just need to provide a reducer function and an initial state value. Your component will get a state
and a dispatch
function which can be used to update that state
.
It seems similar to useState
, and React specifies some deciding factor that can indicate when useReducer
will be better alternative:
- Your component state is complex that involves multiple sub-values, and/or
- The next state value depends upon the current state value.
So a best example of useReducer
can be like this:
const initialTodos = [
{
id: 1,
task: 'Sample Done task #1',
done: true
},
{
id: 2,
task: 'Sample todo task #2',
done: false
}
]
function reducer (state, action) {
switch(action.type) {
case 'new_todo':
return [
...state,
{
id: state[state.length],
task: action.payload.task,
done: false
}
]
case 'edit_todo_task':
const todoIdx = state.find( todo => todo.id===action.payload.id)
return [
...state.slice(0, todoIdx),
{
...state[todoIdx],
task: action.payload.task
},
...state.slice(todoIdx+1)
]
case 'toggle_todo_state':
const todoIdx = state.find( todo => todo.id===action.payload.id)
return [
...state.slice(0, todoIdx),
{
...state[todoIdx],
done: !state[todoIdx].state
},
...state.slice(todoIdx+1)
]
}
}
function TodoApp () {
const [todos, dispatch] = useReducer(initialTodos, reducer)
const handleStatusChange = (todoId) => {
dispatch({
type: 'toggle_todo_state',
payload: { id: todoId}
})
}
const handleTaskUpdate = (todoId, newTaskText) => {
dispatch({
type: 'edit_todo_task',
payload: {
id: todoId,
task: newTaskText
}
})
}
const createNewTodo= (newTodoTask) => {
dispatch({
type: 'new_todo',
payload: { task: newTodoTask }
})
}
return (
<TodoList
todos={todos}
onTodoCreate={createNewTodo}
onStatusChange={handleStatusChange}
onTaskEdit={handleTaskUpdate}
/>
)
}
A common and irritating use case in React Application
When using a complex component state like useReducer
, we are likely to run into a scenario where we have to pass down the state updating function or a callback function (wrapping the state updating function) to the children components. If you have a large application, then it may happen that you have to pass those callback functions through intermediate children components until it reaches the actual descendant component which uses them. This can become unmanageable & suboptimal.
The Solution?
Combine the useReducer
state & dispatch with the Context API.
The Context API
Context API have been a key feature of React. If you feel you need to be familiar with it, you can go through the docs
Both the state and the dispatch function produced by the useReducer
can be fed to separate Context Providers in a parent component. Then any child component, no matter how deep, under the parent, can access them as needed with the use of useContext
or Context Consumer.
Example:
const TodosDispatch = React.createContext(null);
const Todos = React.createContext(null)
function TodoApp() {
const [todos, dispatch] = useReducer(reducer, initialTodos);
return (
<TodosDispatch.Provider value={dispatch}>
<Todos.Provider value={todos} >
<TodoList />
</Todos.Provider>
</TodosDispatch.Provider>
);
}
function TodoList() {
const {todos} = useContext(Todos)
return (
<ul>
{
todos.map(todo => <TodoItem key={todo.id} task={task} isDone={todo.done} />)
}
</ul>
)
}
function AddTodoButton() {
const dispatch = useContext(TodosDispatch);
function handleClick() {
dispatch({
type: 'new_todo', payload: { task: 'hello' }});
}
return (
<button onClick={handleClick}>Add todo</button>
);
}
This combination helps to avoid passing down states or update functions through intermediate components.
Only the components who actually needs the state or the dispatch function can get what they need.
The intermediate components get to handle lesser props as well and can better handle faster component re-rendering decision when memoized.
Benefits
- This
useReducer
anduseContext
combination actually simulates Redux's state management, and is definitely a better light weight alternative to the PubSub library. - If your application is already using an application state, and you require another application state(for whole or part of the application), the combination can be used as a 2nd application state
Caveat
This is not a perfect Redux alternative.
- Redux allows use of custom middlewares for better state management, but this feature is lacking in React's
useRecuder
. - Async tasks can not be used with
useReducer
. - Just like in Redux, there will be huge boilerplate code in the reducer function, and there are no APIs like Redux Tookkit to use for avoiding this.
Top comments (0)