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