DEV Community

Jaxongir
Jaxongir

Posted on • Updated on

React Todo CRUD App with Redux and Typescript

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 .
Enter fullscreen mode Exit fullscreen mode

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

  1. index.tsx
  2. App.tsx
  3. 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
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

6. Type below command

Type npm start it should open a new browser window with the UI we've created so far

npm start
Enter fullscreen mode Exit fullscreen mode

It should display the following UI if everything is working on your side.
Image description

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;
Enter fullscreen mode Exit fullscreen mode

8. 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
Enter fullscreen mode Exit fullscreen mode

9. 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>
);
Enter fullscreen mode Exit fullscreen mode

10. 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="app__header">
          <h1 className="app__title">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;
Enter fullscreen mode Exit fullscreen mode

11. 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;

Enter fullscreen mode Exit fullscreen mode

12. 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;

Enter fullscreen mode Exit fullscreen mode

13. 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;

Enter fullscreen mode Exit fullscreen mode

14. 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;
Enter fullscreen mode Exit fullscreen mode

15. 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="form__input"
          placeholder="Add todo..."
        />
        {error && <p className="form__error-text">{error}</p>}
      </div>
      <button className="btn form__btn">Add Todo</button>
    </form>
  );
};

export default AddTodo;

Enter fullscreen mode Exit fullscreen mode

16. 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
Image description

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-list__label"
      >
        <input
          onChange={handleToggleTodoChange}
          checked={todo.completed ? true : false}
          type="checkbox"
          id={todo.id}
          className="todo-list__checkbox"
        />
        {todo.task}
      </label>
      <div className="todo-list__btns-box">
        <button 
onClick={handleGetEditTodoClick}
className="todo-list__btn todo-list__edit-btn">
<MdModeEditOutline />
</button>
        <button
          onClick={handleDeleteTodoClick}
          className="todo-list__btn todo-list__delete-btn"
        >
<FaTrashAlt />
</button>
      </div>
    </li>
  );
};

export default TodoItem;

Enter fullscreen mode Exit fullscreen mode

18. 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="form__input"
          placeholder="Edit todo..."
        />
        {error && <p className="form__error-text">{error}</p>}
      </div>
      <button className="btn form__btn">Edit Todo</button>
    </form>
  );
};

export default EditTodo;

Enter fullscreen mode Exit fullscreen mode

19. 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;

Enter fullscreen mode Exit fullscreen mode

20. 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.
Image description

21. Hosting

  1. Log in to github and create new repository
  2. In your terminal type following in the "todo" directory
    1. git add .
    2. git commit -m "init: initial commit"
    3. 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
    4. git push -u origin master
  3. This pushes the react code to Github which you should be able to see in your github account
  4. Login to Netlify if you've one else create new account which is very straightforward to do
  5. Do followings
    1. Click "Add new site" button which opens menu with 3 options
    2. Chose "Import an existing project" option
    3. Connect Netlify to your Github account by clicking Github button
    4. Click above the todo repository that you created
    5. Type "CI= npm run build" in the Build command input
    6. 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.
    7. It's live now. Share it with your friends or even better make it better

22. Bonus: Challenge

  1. Add pagination with react-paginate. There should be select for displaying 5/10/20/50 todos per page
  2. Add start date and end date for each todo and it should be styled differently when the task is overdue
  3. Add todo search option
  4. 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)