Skip to content

Toggle Done Status Todo Item on Backend

We need to modify and existing action markItemAsDone that will call our PUT endpoint on backend, get the data and dispatch action to update our store and toggle is_done parameter of that item.

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: string) {
    return async (dispatch: any) => {
        try {
            const toggleDoneItem: any = await fetch('http://localhost:3001/todo/' + item_id, {
                method: 'PUT'
            });

            const body = await toggleDoneItem.json();

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

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

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 MARK_ITEM_DONE_SUCCESS that will dispatch action to reducer with item id so we can find that item in the list and toggle is_done parameter. Also, MARK_ITEM_DONE_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 MARK_ITEM_DONE_ERROR with default "Something went wrong." message in catch block.


Now we have 2 new action types to handle in our reducer MARK_ITEM_DONE_SUCCESS and MARK_ITEM_DONE_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_SUCCESS":
            const doneItemIndex = 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 "MARK_ITEM_DONE_ERROR":
            return {
                ...state,
                error: action.message,
            };
        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;

When we are done updating our action and reducers to handle is_donestatus toggle, it should work right? However you will notice that TodoList throws an error

Argument of type 'number' is not assignable to parameter of type 'string'

This means we are sending a wrong type to the function since we used number as id type for our mock data. Lets update toggleItemDone function in TodoList container just like we did for removeItem function.

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: string) => {
        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;

Now, when you click checkbox of the item "Todo 1" since it is already marked as done, you should see that checkbox gets unchecked and text will not anymore be crossed. That means backend gets updated successfuly and we updated our client application state successfully as well.

React App Todo Toggle Item
React App Todo Toggle Item

We successfully make done toggle for list items and it is connected to backend. The only thing that we might want to improve is to show the error message from todo reducer that we stored.