Hi guys, In today's post we are going to be building a todo application with CRUD operations using React, Redux-Toolkit, and Typescript. Once we'd complete building the app, we're going to be hosting it as well on netlify to share it with others. And I'm also going to give a challenge to you guys at the end of the post. So let's start.
1. Create the folder
mkdir todo-crud
cd todo-crud
// this creates typescript app with all necessary configuration
npx create-react-app todo --template typescript
cd todo
npm i react-icons react-redux redux-persist @reduxjs/toolkit uuid
code .
2. Clean up the unecessary folders for our app
Only the following files must stay within the src folder. Once you've deleted all other files, also delete them wherever they'r used
- index.tsx
- App.tsx
- index.css
3. Create the components and redux folder and the files.
src
/components
/AddTodo
AddTodo.tsx
/EditTodo
EditTodo.tsx
/FilterTodo
FilterTodo.tsx
/TodoList
/TodoItem
TodoItem.tsx
TodoList.tsx
/redux
todo.ts
store.ts
4. Populate components folder files with functional components
On each created file within components folder in previous step, type "rafce" or create functional component manually.
// Example
const AddTodo = ()=>{
return <div>AddTodo</div>
}
5. Import all of them to the App file and paste the following code
In this component and in all other components, we're going to be adding appropriate classnames but we're only going to be styling our components once all the logic is written and application works bug free.
import React from "react";
import AddTodo from "./components/AddTodo/AddTodo";
import EditTodo from "./components/EditTodo/EditTodo";
import FilterTodo from "./components/FilterTodo/FilterTodo";
import TodoList from "./components/TodoList/TodoList";
// define the shape of the todo object and export it so that it'd be reused
export interface TodoInterface {
id: string;
task: string;
completed: boolean;
}
const App = () => {
const [editTodo, setEditTodo] = useState<TodoInterface | null>(null);
return (
<main className="app">
<div className="app__wrapper">
<div className="app__header">
<h1 className="app__title">Todo App</h1>
</div>
<div className="app__inputs-box">
// display edit todo when todo is being edited else display add todo form
{editTodo?.id ? <EditTodo /> : <AddTodo />}
<FilterTodo/>
</div>
<TodoList/>
</div>
</main>
);
};
export default App;
6. Type below command
Type npm start it should open a new browser window with the UI we've created so far
npm start
It should display the following UI if everything is working on your side.
7. Create a reducer by pasting the following code in the "src/redux/todo.ts" file
Now we create reducer to handle state of our application
import {createSlice} from "@reduxjs/toolkit";
import {TodoInterface} from "../App";
// shape of todos array
interface TodosListInterface {
todos: TodoInterface[]
}
// initial todos state
const initialState: TodosListInterface = {
todos: []
}
// todo slice with intial state and reducers to mutate state. They perform CRUD and also toggle todo. Redux-Toolkit uses Immutable.js which allows us to mutate state but on the background everything works as immutated state.
export const todoSlice = createSlice({
name: "todo",
initialState,
reducers: {
addTodo: (state, {payload: {task, id, completed}})=>{
state.todos.push({id, task, completed})
},
deleteTodo: (state, {payload: {todoId}})=>{
state.todos = state.todos.filter(todo=> todo.id !== todoId)
},
editTodo: (state, {payload: {editedTodo}})=>{
console.log(editedTodo)
state.todos = state.todos.map(todo => todo.id === editedTodo.id ? editedTodo : todo);
},
toggleTodo: (state, {payload: {todoId}})=>{
state.todos = state.todos.map(todo => todo.id === todoId ? {...todo, completed: !todo.completed} : todo);
},
}
})
// actions for telling reducer what to do with state, they can also include payload for changing state
export const {addTodo, deleteTodo, editTodo, toggleTodo} = todoSlice.actions;
// reducer to change the state
export default todoSlice.reducer;
- Set up store with the reducer and paste the below code
Basically we're using redux-persist to persist the state in localstorage. redux-persist has options like only persisting allowed states and not storing not-allowed states. you can look at it in here
import { configureStore } from '@reduxjs/toolkit'
import { persistStore, persistReducer } from 'redux-persist'
import storage from 'redux-persist/lib/storage'
import todoReducer from "./todo";
const persistConfig = {
key: 'root',
storage,
}
const persistedReducer = persistReducer(persistConfig, todoReducer)
// we are persisting todos on the local storage
export const store = configureStore({
reducer: {
todos: persistedReducer
},
})
const persistor = persistStore(store)
export {persistor};
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
- Open index.tsx file and paste the following code
In this file, we are connecting react and redux and we are providing all the tree of state of objects as global state and persisting the specified state in store.ts file in the local storage
import React from "react";
import ReactDOM from "react-dom/client";
import { PersistGate } from "redux-persist/integration/react";
import { persistor } from "./redux/store";
import "./index.css";
import App from "./App";
import { store } from "./redux/store";
import { Provider } from "react-redux";
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
);
// we are providing state as global and persisting specified state
root.render(
<React.StrictMode>
<Provider store={store}>
<PersistGate persistor={persistor}>
<App />
</PersistGate>
</Provider>
</React.StrictMode>
);
- Populate App.tsx file with the following code
import React, { useState } from "react";
import { useSelector } from "react-redux";
import type { RootState } from "./redux/store";
import AddTodo from "./components/AddTodo/AddTodo";
import EditTodo from "./components/EditTodo/EditTodo";
import FilterTodo from "./components/FilterTodo/FilterTodo";
import TodoList from "./components/TodoList/TodoList";
export interface TodoInterface {
id: string;
task: string;
completed: boolean;
}
const App = () => {
// here we are subsribed to todos state and read it on each time it changes
const todos = useSelector((state: RootState) => state.todos.todos);
// editTodo used to get todo that to be edited
const [editTodo, setEditTodo] = useState<TodoInterface | null>(null);
// todoFilterValue is used to filter out todos on select
const [todoFilterValue, setTodoFilterValue] = useState("all");
// gets filterValue from select and sets it in the state
const getTodoFilterValue = (filterValue: string) =>
setTodoFilterValue(filterValue);
// gets todo that to be edited and sets it in the state
const getEditTodo = (editTodo: TodoInterface) => setEditTodo(editTodo);
return (
<main className="app">
<div className="app_wrapper">
<div className="appheader">
<h1 className="apptitle">Todo App</h1>
</div>
<div className="app_inputs-box">
{editTodo?.id ? (
<EditTodo editTodo={editTodo} setEditTodo={setEditTodo} />
) : (
<AddTodo />
)}
<FilterTodo getTodoFilterValue={getTodoFilterValue} />
</div>
<TodoList
todos={todos}
todoFilterValue={todoFilterValue}
getEditTodo={getEditTodo}
setEditTodo={setEditTodo}
editTodo={editTodo}
/>
</div>
</main>
);
};
export default App;
- Open Todolist component and set types for each property passed to it
import React from "react";
import TodoItem from "./TodoItem/TodoItem";
import { TodoInterface } from "../../App";
type TodoListProps = {
todos: TodoInterface[];
todoFilterValue: string;
getEditTodo: (editTodo: TodoInterface) => void;
setEditTodo: (editTodo: TodoInterface) => void;
editTodo: TodoInterface | null;
};
const TodoList = ({
todos,
todoFilterValue,
editTodo,
getEditTodo,
setEditTodo,
}: TodoListProps) => {
return (
<ul className="todo-list">
{todos
.filter((todo) => (todoFilterValue === "all" ? true : todo.completed))
.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
editTodo={editTodo}
getEditTodo={getEditTodo}
setEditTodo={setEditTodo}
/>
))}
</ul>
);
};
export default TodoList;
- Open TodoItem component and set types for each property passed to it
import React from "react";
import { TodoInterface } from "../../../App";
type TodoItemProps = {
todo: TodoInterface;
editTodo: TodoInterface | null;
getEditTodo: (editTodo: TodoInterface) => void;
setEditTodo: (editTodo: TodoInterface) => void;
};
const TodoItem = ({
todo,
editTodo,
getEditTodo,
setEditTodo,
}: TodoItemProps) => {
return <li>TodoItem</li>
};
export default TodoItem;
- Open FilterTodo component and set types for each property
import React from "react";
type FilterTodoProps = {
getTodoFilterValue: (filterValue: string) => void;
};
const FilterTodo = ({ getTodoFilterValue }: FilterTodoProps) => {
return <div>FilterTodo</div>;
};
export default FilterTodo;
- Open EditTodo component and set types for each property
import React from "react";
import { TodoInterface } from "../../App";
type EditTodoProps = {
editTodo: TodoInterface;
setEditTodo: (editTodo: TodoInterface);
};
const EditTodo = ({ editTodo, setEditTodo }: EditTodoProps) => {
return <div>EditTodo</div>;
};
export default EditTodo;
- Add form to AddTodo component with todo adding logic
import React, { useState } from "react";
import { useDispatch } from "react-redux";
import { v4 as uuidv4 } from "uuid";
import { addTodo } from "../../redux/todo";
const AddTodo = () => {
const dispatch = useDispatch();
const [task, setTask] = useState("");
const [error, setError] = useState("");
/**
this function prevents default behaviour page refresh on form submit and sets error to state if length of characters either less than 5 or greater than 50.
Else if there'r no errors than it dispatches action to the reducer to add new task with unique id. And sets input to empty ""
*/
const handleAddTaskSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (task.trim().length < 5) {
setError("Minimum allowed task length is 5");
} else if (task.trim().length > 50) {
setError("Maximum allowed task length is 50");
} else {
dispatch(addTodo({ task, id: uuidv4(), completed: false }));
setTask("");
}
};
/**
this function removes error from the state if character length is greater than 5 and less than 50
*/
const handleUpdateTodoChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setTask(e.target.value);
if (task.trim().length > 5 && task.trim().length < 50) {
setError("");
}
};
return (
<form onSubmit={handleAddTaskSubmit} className="form">
<div className="form_control">
<input
onChange={handleUpdateTodoChange}
value={task}
type="text"
className="forminput"
placeholder="Add todo..."
/>
{error && <p className="formerror-text">{error}</p>}
</div>
<button className="btn form_btn">Add Todo</button>
</form>
);
};
export default AddTodo;
- Test: Add new todos
If you've been following right and you can add new todos and they persist in the store.
Add Todo sample
17. Add the logic to delete, toggle todo. And also ability to get current todo
We've also included icons to indicate delete and edit todo. Now only focus on handleDeleteTodoClick which dispatches action to delete todo by it's id. Other handlers are used soon. It should be able to delete todo now.
import React from "react";
import { MdModeEditOutline } from "react-icons/md";
import { FaTrashAlt } from "react-icons/fa";
import { useDispatch } from "react-redux";
import { deleteTodo, toggleTodo } from "../../../redux/todo";
import { TodoInterface } from "../../../App";
type TodoItemProps = {
todo: TodoInterface;
editTodo: TodoInterface | null;
getEditTodo: (editTodo: TodoInterface) => void;
setEditTodo: (editTodo: TodoInterface) => void;
};
const TodoItem = ({
todo,
editTodo,
getEditTodo,
setEditTodo,
}: TodoItemProps) => {
const dispatch = useDispatch();
// This event handler toggles checkbox on and off
const handleToggleTodoChange = () =>
dispatch(toggleTodo({ todoId: todo.id }));
/** This event handler deletes current todo on delete button click
It also resets editTodo state if it's deleted
*/
const handleDeleteTodoClick = () => {
dispatch(deleteTodo({ todoId: todo.id }));
if (todo.id === editTodo?.id) {
setEditTodo({ id: "", task: "", completed: false });
}
};
// This event handler gets current todo which is to be edited
const handleGetEditTodoClick = () => getEditTodo(todo);
return (
<li className="todo-list_item">
<label
htmlFor={todo.id}
style={
todo.completed
? { textDecoration: "line-through" }
: { textDecoration: "none" }
}
className="todo-listlabel"
>
<input
onChange={handleToggleTodoChange}
checked={todo.completed ? true : false}
type="checkbox"
id={todo.id}
className="todo-listcheckbox"
/>
{todo.task}
</label>
<div className="todo-listbtns-box">
<button
onClick={handleGetEditTodoClick}
className="todo-listbtn todo-listedit-btn">
<MdModeEditOutline />
</button>
<button
onClick={handleDeleteTodoClick}
className="todo-listbtn todo-list_delete-btn"
>
<FaTrashAlt />
</button>
</div>
</li>
);
};
export default TodoItem;
- Open EditTodo component to add edit todo logic
It's very similar to AddTodo component but we're using useEffect. Once you paste the code test it in the browser. If everything is working right, then it should be able to edit todo now.
import React, { useState, useEffect } from "react";
import { useDispatch } from "react-redux";
import { v4 as uuidv4 } from "uuid";
import { editTodo as updateTodo } from "../../redux/todo";
import { TodoInterface } from "../../App";
type EditTodoProps = {
editTodo: TodoInterface;
};
const EditTodo = ({ editTodo }: EditTodoProps) => {
const dispatch = useDispatch();
const [task, setTask] = useState("");
const [error, setError] = useState("");
// effect hook is going to set new task on each time user click todo edit button
useEffect(() => {
setTask(editTodo.task);
}, [editTodo]);
// This event handler dispatches action to update edited todo and resets editTodo state so that form switches from edit todo to add todo
const handleEditTaskSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (task.trim().length < 5) {
setError("Minimum allowed task length is 5");
} else if (task.trim().length > 50) {
setError("Maximum allowed task length is 50");
} else {
dispatch(updateTodo({ editedTodo: { ...editTodo, task } }));
setEditTodo({ id: "", task: "", completed: false });
setTask("");
}
};
// this event handler removes error if character length greater than 5 and less than 50
const handleUpdateTodoChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setTask(e.target.value);
if (task.trim().length > 5 && task.trim().length < 50) {
setError("");
}
};
console.log(editTodo);
return (
<form onSubmit={handleEditTaskSubmit} className="form">
<div className="form_control">
<input
onChange={handleUpdateTodoChange}
value={task}
type="text"
className="forminput"
placeholder="Edit todo..."
/>
{error && <p className="formerror-text">{error}</p>}
</div>
<button className="btn form_btn">Edit Todo</button>
</form>
);
};
export default EditTodo;
- Add todo filter functionality
Open FilterTodo component and add the following code to enable filter to do functionality. As we've already added logic in TodoList component, we should be able to filter todos based on completed and not completed. Now go and test by adding 2 completed and 2 uncompleted todos, and filter select to all and completed. All should render all 4 and completed should render only 2 items
import React, { useState } from "react";
type FilterTodoProps = {
getTodoFilterValue: (filterValue: string) => void;
};
const FilterTodo = ({ getTodoFilterValue }: FilterTodoProps) => {
const [filterTodoVal, setFilterTodoVal] = useState("all");
// This event handler updates current select option and passes currently selected option value to App component
const handleFilterTodoChanges = (e: React.ChangeEvent<HTMLSelectElement>) => {
setFilterTodoVal(e.target.value);
getTodoFilterValue(e.target.value);
};
return (
<select
onChange={handleFilterTodoChanges}
value={filterTodoVal}
className="filter-todo"
>
<option value="all">All</option>
<option value="completed">Completed</option>
</select>
);
};
export default FilterTodo;
- Apply styling.
Finally we've finished building the application. Now include following code in index.css and after that your application should look like this.
21. Hosting
- Log in to github and create new repository
- In your terminal type following in the "todo" directory
- git add .
- git commit -m "init: initial commit"
- Add remote origin of the repository you've created which looks like this, for instance: git remote add origin https://github.com/YourGithubUsername/created-repository-name.git
- git push -u origin master
- This pushes the react code to Github which you should be able to see in your github account
- Login to Netlify if you've one else create new account which is very straightforward to do
- Do followings
- Click "Add new site" button which opens menu with 3 options
- Chose "Import an existing project" option
- Connect Netlify to your Github account by clicking Github button
- Click above the todo repository that you created
- Type "CI= npm run build" in the Build command input
- Click "Deploy site" button which deploys the website. It's going to take a few minutes. If everything goes well than application is hosted on the internet.
- It's live now. Share it with your friends or even better make it better
22. Bonus: Challenge
- Add pagination with react-paginate. There should be select for displaying 5/10/20/50 todos per page
- Add start date and end date for each todo and it should be styled differently when the task is overdue
- Add todo search option
- If you're mern stack developer add authentication: Register, Login and social sign ins with google, linkedin, facebook so that ea. Todos must not be shared with other authenticated users
Source Code
Summary
So kudos to all of you who've completed the project with me, I'm sure that you've gained some useful knowledge by completing this tutorial. If you're ambitious and have completed the project please share it with me, I'd like to see what you've done to make it better
Top comments (0)