In my opinion useReducer
is an excellent option to use instead the regular useState
hook for more complex state management. There is also a compelling argument that combining useReducer
with React Context provides a state management solution that drastically reduce (😂) the need to include other libraries like redux.
If you're like me and prefer the additional safety that Typescript provides, there is not a lot of information out there on how to use it with useReducer
.
If you're new to reducers there is a concise summary at the official documentation.
Example App
In the example we will build the compulsory Todo App.
To do this, we need to be able to add, delete and toggle the completed event of Todo
items defined as:
// typings/todo.ts
export interface Todo {
id: string;
task: string;
completed: boolean;
}
Actions
We want the actions to contain information on what type of action we want to do and also to carry an additional payload of data needed to perform the action.
To enable that, we can utilise the following interface as a base.
// typings/action.ts
export interface Action<T, P> {
type: T;
payload: P;
}
Defining the actions for our Todo app, then becomes easy as
// context/todo/action.ts
import { Action, Todo } from '~typings';
export type TodoAction =
| Action<'ADD', Todo>
| Action<'DELETE', { id: string }>
| Action<'TOGGLE', { id: string }>;
Reducer function
In the reducer function Typescript will keep track of all all relations between action types and payload, via the union type TodoAction
that was defined in the previous section.
// context/todo/reducer.ts
import { TodoAction } from './action';
import { Todo } from '~typings';
export const todoReducer = (state: Todo[], action: TodoAction): Todo[] => {
switch (action.type) {
case 'ADD':
return [...state, action.payload];
case 'TOGGLE':
return state.map((todo) => {
if (todo.id !== action.payload.id) {
return todo;
}
return { ...todo, completed: !todo.completed };
});
case 'DELETE':
return [...state.filter((todo) => todo.id !== action.payload.id)];
default:
return state;
}
};
If you only want to use a plain useReducer
adding the following two lines to your component should suffice.
type reducerFunc = (state: Todo[], action: TodoAction) => Todo[];
const [state, dispatch] = useReducer<reducerFunc>(todoReducer, initialContext.todos);
Context
If you instead want use your reducer to manage global state you can wrap it into a Context. This is also what will be used in the examples later on.
Here we create a context with our list of todo items and our dispatch function.
// context/todo/context.tsx
import React, { useContext, useReducer } from 'react';
import { Todo } from '~typings';
import { TodoAction } from './action';
import { todoReducer } from './reducer';
interface TodoContextI {
todos: Todo[];
dispatch: (arg: TodoAction) => void;
}
type reducerFunc = (state: Todo[], action: TodoAction) => Todo[];
const initialContext: TodoContextI = {
todos: [],
dispatch: () => console.error('Context not initialized')
};
const TodoContext = React.createContext<TodoContextI>(initialContext);
interface Props {
children?: React.ReactNode;
}
export const TodoProvider: React.FC<Props> = ({ children }) => {
const [state, dispatch] = useReducer<reducerFunc>(todoReducer, initialContext.todos);
return <TodoContext.Provider value={{ todos: state, dispatch }}>{children}</TodoContext.Provider>;
};
export const useTodos = (): TodoContextI => useContext(TodoContext);
Don't forget to wrap your components within your Context.Provider
// src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { TodoProvider } from '~context/todo';
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
<React.StrictMode>
<TodoProvider>
<App />
</TodoProvider>
</React.StrictMode>
);
Read State
To read the state, there is no difference on how you would use it with a regular Context.
Here we just call the useTodo
function which is just a short hand for useContext(TodoContext)
that we declared with with the context. Then we map over the list and pass each Todo
into a TodoCompoent
.
export const TodoView: React.FC = () => {
const { todos } = useTodos();
return (
<Container>
{todos.map((todo) => (
<TodoComponent {...todo} />
))}
<TodoForm />
</Container>
);
};
Use Dispatch
To use the dispatch
function, we just deconstruct it from the context in similar manner as before. We can then trigger state changes completely typesafe. In the snippet below we trigger TOGGLE
and DELETE
actions to mutate the state.
// components/TodoComponent.tsx
export const TodoComponent: React.FC<Todo> = ({ task, completed, id }) => {
const { dispatch } = useTodos();
const handleCheckBoxClicked = (): void => {
dispatch({ type: 'TOGGLE', payload: { id } });
};
const handleDeleteClicked = (): void => {
dispatch({ type: 'DELETE', payload: { id } });
};
return (
<TodoContainer done={completed}>
<p>{task}</p>
<div>
<button onClick={onDeleteClick}>Delete</button>
<input type="checkbox" checked={completed} onChange={handleCheckBoxClicked} />
</div>
</TodoContainer>
);
};
And in the code below we trigger an action to ADD
a new todo item to our list.
export const TodoForm: React.FC = () => {
const [state, setState] = useState('');
const { dispatch } = useTodos();
const handleInputChange = (e: ChangeEvent<HTMLInputElement>): void => {
setState(e.target.value);
};
const handleSubmit = (): void => {
dispatch({ type: 'ADD', payload: { task: state, completed: false, id: uuidv4() } });
setState('');
};
return (
<Container>
<input type="text" value={state} onChange={handleInputChange} />
<button disabled={!state} onClick={handleSubmit}>
Add
</button>
</Container>
);
};
I hope that this example was of use to you. I think that useReducer
is a good option to use when your state becomes large and/or you need to manipulate the state in a lot of different, but it does come with some additional boilerplate. Most of the time i still prefer to keep things simple with and go with useState
. "I det enkla bor det vackra" - Ernst Kirchsteiger
If you want to have a look at the source you can find it here.
Take Care 👋
Top comments (1)
Awesome! 🙌 Beats Redux IMO