Skip to content

Deleting Todo Item from Backend

We need to modify and existing action removeItem that will call our DELETE endpoint on backend, get the data and dispatch action to update our store and remove specific item from the list.

export function addItem(title: string) {
    return async (dispatch: any) => {
        try {
            const addTodoItem: any = await fetch('http://localhost:3001/todo', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({
                    title,
                })
            });

            const body = await addTodoItem.json();

            if (addTodoItem.status !== 200) {
                return dispatch({ type: 'ADD_ITEM_ERROR', message: body.message });
            }

            return dispatch({ type: 'ADD_ITEM_SUCCESS', item: body.todo });
        } catch (error) {
            return dispatch({ type: 'ADD_ITEM_ERROR', message: 'Something went wrong.' });
        }
    };
}

export function removeItem(item_id: string) {
    return async (dispatch: any) => {
        try {
            const removeItem: any = await fetch('http://localhost:3001/todo/' + item_id, {
                method: 'DELETE'
            });

            const body = await removeItem.json();

            if (removeItem.status !== 200) {
                return dispatch({ type: 'REMOVE_ITEM_ERROR', message: body.message });
            }

            return dispatch({ type: 'REMOVE_ITEM_SUCCESS', item_id: item_id });
        } catch (error) {
            return dispatch({ type: 'REMOVE_ITEM_ERROR', message: 'Something went wrong.' });
        }
    };
}

export function markItemAsDone(item_id: number) {
    return {
        type: 'MARK_ITEM_DONE',
        item_id
    }
}

export function getTodos() {
    return async (dispatch: any) => {
        try {
            const todoList: any = await fetch('http://localhost:3001/todos', {
                method: 'GET'
            });

            const body = await todoList.json();

            if (todoList.status !== 200) {
                return dispatch({ type: 'GET_TODOS_ERROR', message: body.message });
            }

            return dispatch({ type: 'GET_TODOS_SUCCESS', items: body.data });
        } catch (error) {
            return dispatch({ type: 'GET_TODOS_ERROR', message: 'Something went wrong.' });
        }
    };
}

We will now have 2 action types for removing an item REMOVE_ITEM_SUCCESS that will dispatch action to reducer with item id so we can find that item in the list and delete. Also, REMOVE_ITEM_ERROR that will be called in case something went wrong and pass error message to be stored in reducer.

Like for other actions, we make sure that if anything else is wrong (like typos), we dispatch REMOVE_ITEM_ERROR with default "Something went wrong." message in catch block.


Now we have 2 new action types to handle in our reducer REMOVE_ITEM_SUCCESS and REMOVE_ITEM_ERROR.

export interface Todo {
    id: number;
    title: string;
    is_done: boolean;
}

interface TodoReducerInterface {
    todo_list: Todo[];
    error: "";
}

const INITIAL_STATE: TodoReducerInterface = {
    todo_list: [],
    error: "",
};

const todoReducer = (state = INITIAL_STATE, action: any) => {
    switch (action.type) {
        case "ADD_ITEM_SUCCESS":
            return {
                ...state,
                todo_list: [...state.todo_list, action.item],
            };
        case "ADD_ITEM_ERROR":
            return {
                ...state,
                error: action.message,
            };
        case "REMOVE_ITEM_SUCCESS":
            const removeItemIndex = state.todo_list.findIndex(
                (item: any) => item._id === action.item_id
            );

            if (removeItemIndex < 0) {
                return state;
            }

            return {
                ...state,
                todo_list: [
                    ...state.todo_list.slice(0, removeItemIndex),
                    ...state.todo_list.slice(removeItemIndex + 1),
                ],
            };
        case "REMOVE_ITEM_ERROR":
            return {
                ...state,
                error: action.message,
            };
        case "MARK_ITEM_DONE":
            const doneItemIndex: number = state.todo_list.findIndex(
                (item: any) => item.id === action.item_id
            );

            if (doneItemIndex < 0) {
                return state;
            }

            return {
                ...state,
                todo_list: [
                    ...state.todo_list.slice(0, doneItemIndex),
                    {
                        ...state.todo_list[doneItemIndex],
                        is_done: !state.todo_list[doneItemIndex].is_done,
                    },
                    ...state.todo_list.slice(doneItemIndex + 1),
                ],
            };
        case "GET_TODOS_SUCCESS":
            return {
                ...state,
                todo_list: action.items,
                error: "",
            };
        case "GET_TODOS_ERROR":
            return {
                ...state,
                todo_list: [],
                error: action.message,
            };
        default:
            return state;
    }
};

export default todoReducer;

You should notice that our item_id type changed from number to string when we started working on backend and that your application reports an error because of type issue in TodoList container. Lets modify that.

import { useDispatch, useSelector } from "react-redux";
import ListHeader from "../components/ListHeader";
import List from "../components/List";
import { addItem, getTodos, markItemAsDone, removeItem } from "../store/actions/todo";
import { useEffect, useState } from "react";
import { RootState } from "../store/reducers";

function TodoList() {
    const dispatch = useDispatch();
    const [title, setTitle] = useState<string>("");

    const todo: any = useSelector((state: RootState) => state.todo);

    const addItemToList = () => {
        if (!title) return;

        dispatch(addItem(title));
        setTitle('');
    };

    const removeItemFromList = (item_id: string) => {
        dispatch(removeItem(item_id));
    };

    const toggleItemDone = (item_id: number) => {
        dispatch(markItemAsDone(item_id));
    };

    useEffect(() => {
        dispatch(getTodos());
    }, []);

    return (
        <div className="flex justify-center items-center h-screen">
            <div className="shadow-lg rounded-lg w-[500px]">
                <ListHeader
                    addItemToList={addItemToList}
                    title={title}
                    setTitle={setTitle}
                />
                <List
                    list={todo.todo_list}
                    removeItemFromList={removeItemFromList}
                    toggleItemDone={toggleItemDone}
                />
            </div>
        </div>
    );
}

export default TodoList;

We also need to make sure we also access it as _id instead of id as we did with mock data. For that we need to modify out ListItem component.

import { Todo } from "../store/reducers/todo";
import { motion } from "framer-motion";

interface ListItemInterface {
    item: Todo;
    removeItemFromList: Function;
    toggleItemDone: Function;
}

function ListItem(props: ListItemInterface) {
    return (
        <motion.li
            animate={{ opacity: 1, y: '0' }}
            exit={{ opacity: 0 }}
            transition={{ duration: 0.5 }}
            className="flex items-center p-2 opacity-0 -translate-y-1/2"
        >
            <input
                onChange={() => props.toggleItemDone(props.item._id)}
                type="checkbox"
                className="h-4 w-4 mr-2 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"
                checked={props.item.is_done}
            />
            <p
                className={
                    "w-full" + (props.item.is_done ? " line-through" : "")
                }
            >
                {props.item.title}
            </p>
            <button
                onClick={() => props.removeItemFromList(props.item._id)}
                type="button"
                className="w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-2 py-0.5 bg-white text-base font-medium text-gray-700 hover:bg-gray-100 focus:outline-none focus:ring-1 focus:ring-offset-2 focus:ring-red-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
            >
                Remove
            </button>
        </motion.li>
    );
}

export default ListItem;

We changed it but we will notice some erros. That is because we did not update our Todo interface. We need to do that and change id parameter to _id.

export interface Todo {
    _id: string;
    title: string;
    is_done: boolean;
}

interface TodoReducerInterface {
    todo_list: Todo[];
    error: "";
}

const INITIAL_STATE: TodoReducerInterface = {
    todo_list: [],
    error: "",
};

const todoReducer = (state = INITIAL_STATE, action: any) => {
    switch (action.type) {
        case "ADD_ITEM_SUCCESS":
            return {
                ...state,
                todo_list: [...state.todo_list, action.item],
            };
        case "ADD_ITEM_ERROR":
            return {
                ...state,
                error: action.message,
            };
        case "REMOVE_ITEM_SUCCESS":
            const removeItemIndex = state.todo_list.findIndex(
                (item: any) => item._id === action.item_id
            );

            if (removeItemIndex < 0) {
                return state;
            }

            return {
                ...state,
                todo_list: [
                    ...state.todo_list.slice(0, removeItemIndex),
                    ...state.todo_list.slice(removeItemIndex + 1),
                ],
            };
        case "REMOVE_ITEM_ERROR":
            return {
                ...state,
                error: action.message,
            };
        case "MARK_ITEM_DONE":
            const doneItemIndex: number = state.todo_list.findIndex(
                (item: any) => item.id === action.item_id
            );

            if (doneItemIndex < 0) {
                return state;
            }

            return {
                ...state,
                todo_list: [
                    ...state.todo_list.slice(0, doneItemIndex),
                    {
                        ...state.todo_list[doneItemIndex],
                        is_done: !state.todo_list[doneItemIndex].is_done,
                    },
                    ...state.todo_list.slice(doneItemIndex + 1),
                ],
            };
        case "GET_TODOS_SUCCESS":
            return {
                ...state,
                todo_list: action.items,
                error: "",
            };
        case "GET_TODOS_ERROR":
            return {
                ...state,
                todo_list: [],
                error: action.message,
            };
        default:
            return state;
    }
};

export default todoReducer;

Good, but you should notice our application is still not compiling. That is because the key parameter for each item in the list still points to id instead of _id and Typescript shows an error since id parameter does not exist on our Todo interface anymore.

Lets update List component

import { AnimatePresence } from "framer-motion";
import { Todo } from "../store/reducers/todo";
import ListItem from "./ListItem";

interface ListInterface {
    list: Todo[];
    removeItemFromList: Function;
    toggleItemDone: Function;
}

function List(props: ListInterface) {
    return (
        <ul className="p-8 pt-0 divide-y">
            {props.list.length > 0 && (
                <AnimatePresence>
                    {props.list.map((todo_item: Todo) => (
                        <ListItem
                            key={todo_item._id}
                            item={todo_item}
                            removeItemFromList={props.removeItemFromList}
                            toggleItemDone={props.toggleItemDone}
                        />
                    ))}
                </AnimatePresence>
            )}
            {props.list.length < 1 && (
                <p>Your todo list is empty, please add some todo.</p>
            )}
        </ul>
    );
}

export default List;

Now, we can try to delete an existing todo item through application with title "Todo 2". Click "Remove" button of "Todo 2".

If you did everything as you should, the item will be remove from the database and will disappear from the list.

React App Todo Remove Item
React App Todo Remove Item

We successfully removed an item from the database and removed it from the list. In the next part we will make users able to toggle is_done parameter of an existing item from the database with provided id and automatically check the item on our client application and cross the text.


Last update: March 28, 2022