I encountered the following problem: I created a todo app using React & Redux, and it works well until I decided to implement localStorage. When I start my app, I get a quite common error: ‘Uncaught Error: Objects are not valid as a React child (found: object with keys {id, title, isCompleted, isEditing, number})’.
If you meant to render a collection of children, use an array instead.’ However, I couldn’t figure out where I need to make changes to prevent this error. I created a reducer:
import {
ADD_TASK,
REMOVE_TASK,
UPDATE_TASK,
} from "../actionsTypes";
import { v4 as uuidv4 } from "uuid";
export const initialState = {
tasklist: [], //tasklist
};
export function taskReducer(state = initialState, action) {
switch (action.type) {
case ADD_TASK:
return {
...state,
tasklist: [
...state.tasklist,
{
id: uuidv4(),
title: action.payload,
isCompleted: false,
isEditing: false,
number: state.tasklist.length + 1,
},
],
};
case REMOVE_TASK:
return {
...state,
tasklist: state.tasklist.filter((todo) => action.payload !== todo.id),
};
case UPDATE_TASK:
return {
...state,
tasklist: state.tasklist.map((prevTask) =>
prevTask.id === action.payload.id
? { ...prevTask, title: action.payload.newTitle }
: prevTask
),
};
default:
return state;
}
}
I also have my TaskList.jsx, where I tried to insert and extract data from LocalStorage:
import React, { useState, useEffect } from "react";
import InputTask from "../InputTask/InputTask";
import HeaderOfTaskList from "../HeaderOfTaskList/HeaderOfTaskList";
import Task from "../Task/Task";
import { useDispatch, useSelector } from "react-redux";
import { removeTask, addTask } from "../../store/actions";
import InputSearch from "../InputSearch/InputSearch";
import useInput from "../../hooks/useInput";
import "./TaskList.css";
export const TaskList = () => {
const dispatch = useDispatch();
const tasks = useSelector((state) => state.tasklist);
const input = useInput();
const [searchTerm, setSearchTerm] = useState("");
useEffect(() => {
const storedTasks = localStorage.getItem("tasks"); //JSON.stringify(localStorage.getItem("tasks"));
if (storedTasks) {
dispatch(addTask(JSON.parse(storedTasks)));
}
}, [dispatch, tasks]);
useEffect(() => {
localStorage.setItem("tasks", JSON.stringify(tasks));
}, [tasks]);
const filteredTasks = tasks.filter((task) =>
typeof task.title === "string" &&
task.title.toLowerCase().includes(searchTerm.toLowerCase())
);
const isTaskListEmpty = filteredTasks.length === 0;
const handleInputChange = (value) => {
setSearchTerm(value);
};
const handleDelete = (id) => {
dispatch(removeTask(id));
};
const handleAddTask = (tasklist) => {
dispatch(addTask(tasklist));
};
return (
<div>
<InputTask addTask={handleAddTask} />
<InputSearch onInputChange={handleInputChange} />
<HeaderOfTaskList />
{!isTaskListEmpty ? (
<ul>
{filteredTasks
.filter((task) =>
task.title.toLowerCase().includes(input.value.toLowerCase())
)
.map((task) => (
<Task
task={task}
key={task.id}
onDelete={() => handleDelete(task.id)}
/>
))}
</ul>
) : (
<ul>
{tasks.map((task) => (
<Task
task={task}
key={task.id}
onDelete={() => handleDelete(task.id)}
/>
))}
</ul>
)}
</div>
);
};
My Task component is responsible for rendering the list of tasks:
import React, { useState } from "react";
import { useDispatch } from "react-redux";
import Button from "@mui/material/Button";
import DeleteIcon from "@mui/icons-material/Delete";
import EditIcon from "@mui/icons-material/Edit";
import { Checkbox } from "@mui/material";
import { updateTask } from "../../store/actions";
import "./Task.css";
const Task = ({ task, onDelete }) => {
const [isEditing, setIsEditing] = useState(false);
const [inputValue, setInputValue] = useState(task.title);
const [theme, setTheme] = useState("dark");
const dispatch = useDispatch();
const onEdit = () => {
setIsEditing(!isEditing);
};
const onCheckBoxClicked = () => {
if (!task.isCompleted) {
setTheme(theme === "light" ? "dark" : "light");
}
};
const onSaveClicked = () => {
dispatch(updateTask(task.id, inputValue));
setIsEditing(!isEditing);
};
return (
<li className={theme}>
{isEditing ? (
<input
id="edittask"
type="text"
value={inputValue}
onChange={(e) => {
setInputValue(e.target.value);
}}
onKeyDown={(e) => {
if (e.key === "Enter" && isEditing) {
onSaveClicked();
}
}}
/>
) : (
<>
{task.number}
<p>{task.title}</p>
<Checkbox onClick={onCheckBoxClicked} />
</>
)}
{!isEditing ? (
<Button onClick={onEdit} variant="outlined" endIcon={<EditIcon />}>
Edit
</Button>
) : (
<Button onClick={onSaveClicked} variant="outlined">
Save
</Button>
)}
<Button onClick={onDelete} variant="outlined" endIcon={<DeleteIcon />}>
Remove
</Button>
</li>
);
};
export default Task;
import React, { useState } from "react";
import "./InputTask.css";
import Button from "@mui/material/Button";
const InputTask = ({ addTask }) => {
const [value, setValue] = useState("");
const handleChange = () => {
if (value) {
addTask(value);
}
setValue("");
};
const isEnterButtonClicked = (e) => {
if (e.key === "Enter") {
addTask(value);
setValue("");
}
};
return (
<div className="sectioninput">
<input
value={value}
type="text"
id="typetask"
placeholder="What are you gonna do?"
onChange={(e) => setValue(e.target.value)}
onKeyDown={isEnterButtonClicked}
/>
<Button variant="outlined" onClick={handleChange}>
Add Task
</Button>
</div>
);
};
export default InputTask;
import {
ADD_TASK,
REMOVE_TASK,
UPDATE_TASK,
COMPLETE_TASK,
} from "./actionsTypes";
export const addTask = (title) => ({
type: ADD_TASK,
payload: title,
});
export const removeTask = (id) => ({
type: REMOVE_TASK,
payload: id,
});
export const updateTask = (id, title) => ({
type: UPDATE_TASK,
payload: { id, newTitle: title },
});
export const completeTask = (id) => ({
type: COMPLETE_TASK,
payload: id,
});
2
Answers
Create a custom hook of localstorage or I personally prefer localforage then localstorage.
LocalForage uses asynchronous storage, such as IndexedDB or WebSQL, with a simple API that’s similar to localStorage.
You need to do the following steps:
Reducer
to Load Initial State fromlocalStorage
.