skip to Main Content

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


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

    Login or Signup to reply.
  2. You need to do the following steps:

    1. Adjust the Reducer to Load Initial State from localStorage.
    export const initialState = {
      tasklist: JSON.parse(localStorage.getItem('tasks')) || [],
    };
    
    1. Remove Unnecessary useEffect in TaskList.
    useEffect(() => {
      const storedTasks = localStorage.getItem("tasks");  block
      if (storedTasks) {
        dispatch(addTask(JSON.parse(storedTasks)));
      }
    }, [dispatch, tasks]);
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search