Tutorial - ToDo App with useReducer

This tutorial shows example code with useReducer.

src/App.js

import React from 'react';
import { Provider } from './store';
import TodoList from './TodoList';
const App = () => (
<Provider>
<TodoList />
</Provider>
);
export default App;

This is the root component. It wraps TodoList with Provider.

src/store.js

import { useReducer } from 'react';
import { createContainer } from 'react-tracked';
const initialState = {
todos: [
{ id: 1, title: 'Wash dishes' },
{ id: 2, title: 'Study JS' },
{ id: 3, title: 'Buy ticket' },
],
query: '',
};
let nextId = 4;
const reducer = (state, action) => {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [...state.todos, { id: nextId++, title: action.title }],
};
case 'DELETE_TODO':
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.id),
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.id
? { ...todo, completed: !todo.completed }
: todo,
),
};
case 'SET_QUERY':
return {
...state,
query: action.query,
};
default:
return state;
}
};
const useValue = () => useReducer(reducer, initialState);
export const {
Provider,
useTrackedState,
useUpdate: useDispatch,
} = createContainer(useValue);

The store is created by useReducer. useUpdate is renamed to useDispatch for exporting.

src/TodoList.js

import React from 'react';
import { useDispatch, useTrackedState } from './store';
import TodoItem from './TodoItem';
import NewTodo from './NewTodo';
const TodoList = () => {
const dispatch = useDispatch();
const state = useTrackedState();
const setQuery = event => {
dispatch({ type: 'SET_QUERY', query: event.target.value });
};
return (
<div>
<ul>
{state.todos.map(({ id, title, completed }) => (
<TodoItem key={id} id={id} title={title} completed={completed} />
))}
<NewTodo />
</ul>
<div>
Highlight Query for incomplete items:
<input value={state.query} onChange={setQuery} />
</div>
</div>
);
};
export default TodoList;

This component is to show the list of TodoItems, NewTodo to create a new item, and a text field for highlight query. This query is only effective against incomplete items.

src/TodoItem.js

import React from 'react';
import { useDispatch, useTrackedState } from './store';
import { useFlasher } from './utils';
const renderHighlight = (title, query) => {
if (!query) return title;
const index = title.indexOf(query);
if (index === -1) return title;
return (
<>
{title.slice(0, index)}
<b>{query}</b>
{title.slice(index + query.length)}
</>
);
};
const TodoItem = ({ id, title, completed }) => {
const dispatch = useDispatch();
const state = useTrackedState();
const delTodo = () => {
dispatch({ type: 'DELETE_TODO', id });
};
return (
<li ref={useFlasher()}>
<input
type="checkbox"
checked={!!completed}
onChange={() => dispatch({ type: 'TOGGLE_TODO', id })}
/>
<span
style={{
textDecoration: completed ? 'line-through' : 'none',
}}
>
{completed ? title : renderHighlight(title, state.query)}
</span>
<button onClick={delTodo}>Delete</button>
</li>
);
};
export default React.memo(TodoItem);

This is the TodoItem component. We prefer primitive props for memoized components.

If you want to use object props for memoized components, you need to notify the objects by trackMemo. See example/09 for the usage.

src/NewTodo.js

import React, { useState } from 'react';
import { useDispatch } from './store';
import { useFlasher } from './utils';
const NewTodo = () => {
const dispatch = useDispatch();
const [text, setText] = useState('');
const addTodo = () => {
dispatch({ type: 'ADD_TODO', title: text });
setText('');
};
return (
<li ref={useFlasher()}>
<input
value={text}
placeholder="Enter title..."
onChange={e => setText(e.target.value)}
/>
<button onClick={addTodo}>Add</button>
</li>
);
};
export default React.memo(NewTodo);

This is the NewTodo component to create a new item. It uses a local state for the text field.

src/utils.js

import { useRef, useEffect } from 'react';
export const useFlasher = () => {
const ref = useRef(null);
useEffect(() => {
if (!ref.current) return;
ref.current.setAttribute(
'style',
'box-shadow: 0 0 2px 1px red; transition: box-shadow 100ms ease-out;',
);
setTimeout(() => {
if (!ref.current) return;
ref.current.setAttribute('style', '');
}, 300);
});
return ref;
};

This is a utility function to show which components render.

CodeSandbox

You can try working example.