DEV Community

Cover image for Building Todo app with Zustand in React
vigneshiyergithub
vigneshiyergithub

Posted on

11 3 1 1 1

Building Todo app with Zustand in React

What is this post about ?

Hello fellow humanoids. Today we will try to implement a basic todo app with Zustand in React Js. This post won't be focused much on the styling rather the bare minimum logic required.

Check out the app here : Todo App - Zustand

Todo App Zustand

Content

  • What is Zustand ?
  • Zustand store structure
  • Todo App - Adding Todos
  • Todo App - Adding todo items

Lets go deep dive into each one and explore how it was implemented.

What is Zustand ?

A small, fast and scalable bearbones state-management solution using simplified flux principles. Has a comfy api based on hooks, isn't boilerplatey or opinionated.

Don't disregard it because it's cute. It has quite the claws, lots of time was spent to deal with common pitfalls, like the dreaded zombie child problem, react concurrency, and context loss between mixed renderers. It may be the one state-manager in the React space that gets all of these right.

Check it out-Zustand

Zustand store

import create from "zustand";
import { v4 as uuidv4 } from "uuid";
const setAddTodo = (set) => (todo) =>
set((state) => ({ todos: [...state.todos, todo] }));
const setEditTodo = (set) => (id, todo) =>
set((state) => {
const todos = state.todos.map((_t) => {
if (_t.id === id) {
console.log("Editing todo", todo);
return { ...todo, v: todo?.v ?? 0 + 1 };
}
return _t;
});
return { todos };
});
const setCreateNewTodo = (set) => () => {
const newKey = uuidv4();
set((state) => ({
todos: [
...state.todos.map((t) => ({ ...t, isSelected: false })),
{
id: newKey,
name: `Default Title - ${state.todos.length + 1}`,
items: [],
isSelected: true,
},
],
}));
};
const setSelectCurrentTodo = (set) => (todoId) => {
set(({ todos }) => ({
todos: todos.map((t) => {
if (t.id === todoId) {
const newTodo = { ...t, isSelected: true };
return newTodo;
}
return { ...t, isSelected: false };
}),
}));
};
const setAddNewTodoItem = (set) => (tId, value) => {
set((state) => {
const todos = state.todos.map((t) => {
if (t.id === tId) {
return {
...t,
items: [
...t.items,
{
id: uuidv4(),
value,
completed: false,
},
],
};
}
return t;
});
return {
todos,
};
});
};
const setCompleteTask = (set) => (tId, ItemId, value) => {
set((state) => {
return {
todos: state.todos.map((_t) => {
if (tId === _t.id) {
return {
..._t,
items: _t.items.map((_i) => {
if (_i.id === ItemId) {
return { ..._i, completed: value };
}
return _i;
}),
};
}
return _t;
}),
};
});
};
const setDeleteTodo = (set) => (tId) => {
set((state) => {
return {
todos: state.todos.filter((_t) => _t.id !== tId),
};
});
};
const getLocalStorage = (key) => JSON.parse(window.localStorage.getItem(key));
export const useStore = create((set) => ({
todos: getLocalStorage("todos") ?? [],
addTodo: setAddTodo(set),
editTodo: setEditTodo(set),
deleteTodo: setDeleteTodo(set),
createNewTodo: setCreateNewTodo(set),
selectCurrentTodo: setSelectCurrentTodo(set),
addNewTodoItem: setAddNewTodoItem(set),
completeTask: setCompleteTask(set),
}));

Todo App - Adding Todos

We would add new Todo using

Add New Todo

button

import { useToasts } from "react-toast-notifications";
import { ActiveTodos } from "./ActiveTodos";
import "./App.css";
import { CurrentTodoSection } from "./CurrentTodoSection";
import { useStore } from "./store";
const ADD_TODO_CONTENT = "Added new todo!";
export const DELETE_TODO_CONTENT = "Deleted todo!";
function App() {
const createNew = useStore((state) => state.createNewTodo);
const { addToast } = useToasts();
const addNewTodo = () => {
createNew();
addToast(ADD_TODO_CONTENT, { appearance: "success", autoDismiss: true });
};
return (
<div className="App">
<header>Todo App with Zustand</header>
<div className="content-container">
<div className="content-sidebar">
<div className="content-sidebar-item">
<button className="btn-add-todo" onClick={addNewTodo}>
Add New Todo
</button>
</div>
<div className="content-sidebar-item content-sidebar-title">
Active Todos
</div>
<ActiveTodos />
</div>
<div className="content-main">
<CurrentTodoSection />
</div>
</div>
</div>
);
}
export default App;
view raw Todo Add New.js hosted with ❤ by GitHub

Todo App - Adding todo items

Let's create a basic comment component with basic utility functions

import { useEffect } from "react";
import { useToasts } from "react-toast-notifications";
import crossIcon from "./assets/cancel.png";
import { useStore } from "./store";
import { DELETE_TODO_CONTENT } from "./App";
export const ActiveTodos = () => {
const todos = useStore(
(state) => state.todos,
(oldV, newV) => JSON.stringify(oldV) === JSON.stringify(newV)
);
const { addToast } = useToasts();
const deleteTodo = useStore((state) => state.deleteTodo);
const setCurrentTodo = useStore((state) => state.selectCurrentTodo);
const setLocalStorage = (key, value) => window.localStorage.setItem(key, JSON.stringify(value));
useEffect(() => {
return () => {
console.log("Writing todos... local storage");
setLocalStorage("todos", todos);
};
});
const onDelete = (tId) => {
deleteTodo(tId);
addToast(DELETE_TODO_CONTENT, { appearance: "warning", autoDismiss: true });
};
const perCent = (t) => {
const completedTodos = t.items.filter((i) => i.completed).length;
const totalTodos = t.items.length;
return totalTodos > 0
? `${parseInt((completedTodos / totalTodos) * 100)}`
: 0;
};
const getStyle = (t) => {
const per = perCent(t);
console.log("per", per);
if (per > 0) {
return {
background: `linear-gradient(
to right,
darkcyan 0%,
darkcyan ${per}%,
darkcyan ${per}%,
lightcyan 100%
)`,
};
}
return {};
};
return (
<>
{todos.map((_t) => (
<div
key={_t.id}
className={`content-sidebar-item todo-sidebar-item ${_t.isSelected && "content-sidebar-item-selected"}`}
onClick={() => setCurrentTodo(_t.id)}
style={getStyle(_t)}
>
<div>{_t.name}</div>
<div>
<img
src={crossIcon}
className="todo-item-delete"
alt="todo-delete-item-icon"
onClick={() => onDelete(_t.id)} />
</div>
</div>
))}
</>
);
};
view raw Active-todo.js hosted with ❤ by GitHub
import { useState } from "react";
import { useStore } from "./store";
import { EditableText } from "./EditableText";
export const CurrentTodoSection = () => {
const todos = useStore((state) => state.todos);
const addNewTodoItem = useStore((state) => state.addNewTodoItem);
const editTodo = useStore((state) => state.editTodo);
const setCompleteTask = useStore((state) => state.completeTask);
const [inputText, setInputText] = useState("");
const currentTodo = todos.filter((t) => t.isSelected)?.[0] ?? {};
const onEditFieldTodoTitle = (text) => {
editTodo(currentTodo.id, { ...currentTodo, name: text });
};
const onEditTodoItem = (tId, itemId, value) => {
console.log("Editing Item task", tId, itemId, value);
editTodo(tId, {
...currentTodo,
items: currentTodo.items.map((_i) => {
if (_i.id === itemId) {
return { ..._i, value };
}
return _i;
}),
});
};
return Object.keys(currentTodo).length > 0 ? (
<div className="todo-main">
<div className="todo-title">
<EditableText
value={currentTodo.name}
onEditField={onEditFieldTodoTitle} />
</div>
<div className="todo-playground">
<div className="todo-input-area">
<input
type="text"
value={inputText}
onChange={(e) => setInputText(e.target.value)} />
<button
onClick={() => {
addNewTodoItem(currentTodo.id, inputText);
setInputText("");
}}
>
Insert
</button>
</div>
<div className="todo-list">
{currentTodo.items.map((_i) => {
return (
<div className="todo-list-item" key={_i.id}>
<input
type="checkbox"
id={_i.id}
name={_i.id}
checked={_i.completed}
onChange={() => {
currentTodo.id &&
setCompleteTask(currentTodo.id, _i.id, !_i.completed);
}} />
<label for={_i.id}>
<EditableText
value={_i.value}
onEditField={(text) => onEditTodoItem(currentTodo.id, _i.id, text)} />
</label>
</div>
);
})}
</div>
</div>
</div>
) : null;
};
import { useEffect, useState } from "react";
import { useToasts } from "react-toast-notifications";
import editIcon from "./assets/edit.png";
export const EditableText = (props) => {
const [showEdit, setShowEdit] = useState(false);
const [onEdit, setOnEdit] = useState(false);
const [inputText, setInputText] = useState(props.value);
const { addToast } = useToasts();
useEffect(() => {
setInputText(props.value);
}, [props.value]);
const onMouseEnter = () => {
setShowEdit(true);
};
const onMouseLeave = () => setShowEdit(false);
const onAdd = () => {
props.onEditField(inputText);
setOnEdit(false);
setShowEdit(false);
addToast("Todo item edited", {
appearance: "info",
autoDismiss: true,
});
};
return (
<div
className="editable-text"
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
{!onEdit ? (
<>
<div className="editable-text-child">{props.value}</div>
{showEdit && (
<img
src={editIcon}
className="editable-text-edit"
alt="edit-icon"
onClick={(e) => {
e.stopPropagation();
setOnEdit(true);
}} />
)}
</>
) : (
<>
<input
className="editable-text-input"
value={inputText}
onChange={(e) => setInputText(e.target.value)} />
<button onClick={onAdd}>Save</button>
</>
)}
</div>
);
};

Conclusion

This app was made as part of learning new components which are used in real life applications.
Stay safe and lend a hand to another :)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay