A simple React Todo List with Zustand, Material/UI and persisting data with Zustand Midleware
The final project is here: https://github.com/leandroaps/react-todo-app-zustand
Creating the project base
To start, create the project react-todo-app-zustand using the comand below on your terminal:
yarn create vite@latest react-todo-app-zustand --template react
- Choose React
- Select TypeScript
Go to the folder of your project and run yarn to install all the dependencies
cd react-todo-app-zustand && yarn
After that, I creates some folder to keep the things organized:
- components
- store
- theme (only if you want to use Material/UI)
- types
Material/UI
Install the Material/UI using the command:
yarn add @mui/material @emotion/react @emotion/styled
Create a index.tsx file inside the folder theme, with the content, this will handle a small theme for Material/UI.
import { red } from '@mui/material/colors';
import { createTheme } from '@mui/material/styles';
const theme = createTheme({
palette: {
primary: {
main: '#556cd6'
},
secondary: {
main: '#19857b'
},
error: {
main: red.A400
}
}
});
export default theme;
Adjust our main.tsx file, adding the theme and the CssBaseline, use the content below:
import { CssBaseline } from '@mui/material';
import { ThemeProvider } from '@mui/material/styles';
import * as React from 'react';
import * as ReactDOM from 'react-dom/client';
import App from './App';
import theme from './theme/';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<ThemeProvider theme={theme}>
<CssBaseline />
<App />
</ThemeProvider>
</React.StrictMode>
);
Zustand
Install zustand using
yarn add zustand
Inside the store folder, create a index.tsx file with the code, this will handle our store data, with the persist method from zustand/midleware
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { IToDo } from '../types';
const store = create()(
devtools(
persist(
(set) => ({
todos: [],
addTodo: (todo: IToDo) => {
set((state: { todos: IToDo[] }) => ({
todos: [...state.todos, { id: Date.now(), text: todo, completed: false }]
}));
},
removeTodo: (id: number) => {
set((state: { todos: IToDo[] }) => ({
todos: state.todos.filter((todo: IToDo) => todo.id !== id)
}));
},
toggleTodo: (id: number) => {
set((state: { todos: IToDo[] }) => ({
todos: state.todos.map((todo: IToDo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
}));
}
}),
{ name: 'bearStore' }
)
)
);
export default store;
TypeScript
Adds some types to our project, inside the folder types, create a index.tsx file with the content:
export interface IToDo {
id: number;
completed: boolean;
text: string;
}
To Do List
For the main component, create a folder named TodoList inside the folder components and inside that folder, create our TodoList.tsx file with the content:
import { Button, Input, List, ListItem, Typography } from '@mui/material';
import { memo, useState } from 'react';
import store from '../../store';
import { IToDo } from '../../types/';
const TodoList = () => {
const { todos, addTodo, removeTodo, toggleTodo } = store();
const [newTodo, setNewTodo] = useState('');
const handleAddTodo = () => {
if (newTodo.trim()) {
addTodo(newTodo);
setNewTodo(''); // Clear the input after adding
}
};
return (
<div>
<Typography variant="h1" component="h1" sx={{ mb: 2 }}>
Todo List
</Typography>
<Input
type="text"
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
placeholder="Add a new todo"
/>
<Button onClick={handleAddTodo}>Add</Button>
<List>
{todos.map((todo: IToDo) => (
<ListItem key={todo.id}>
<Typography
style={{
textDecoration: todo.completed ? 'line-through' : 'none',
cursor: 'pointer'
}}
onClick={() => toggleTodo(todo.id)}
>
{todo.text}
</Typography>
<Button onClick={() => removeTodo(todo.id)}>Delete</Button>
</ListItem>
))}
</List>
</div>
);
};
export default memo(TodoList);
App
Connect the TodoList component to our using the content:
import { Container } from '@mui/material';
import { memo } from 'react';
import TodoList from './components/TodoList/TodoList';
function App() {
return (
<Container>
<TodoList />
</Container>
);
}
export default memo(App);
I added some memo to our code to keep the store memoized, this will improve performance and the loading of the components avoiding re-renders.