Donnie Berry
Donnie Berry

Reputation: 289

"TypeError: props.todos.map is not a function" can't figure out what the cause is?

I am trying to create my first Todo list with React.js. I am trying to change the state from

const [todos, setTodos] = useState([])

To:

const [todos, setTodos] = useState({
    todo: [],
    isCompleted: false,
  })

Just to try and add in a isCompleted state. However, when I change it, I get an error when running my application from a previously working map. The error is in the title.

Could somebody tell me what is wrong?

Code:

TodosApp.js

import React, { useState } from "react"
import Todos from "./Todos"

const TodoApp = () => {
  const [todos, setTodos] = useState({
    todo: [],
    isCompleted: false,
  })
  const [input, setInput] = useState("")

  const handleCurrentInput = (e) => {
    setInput(e.target.value)
  }
  const handleSubmit = (e) => {
    e.preventDefault()
    console.log(input)
    setInput("")
    setTodos({
      ...todos,
      task: input,
      isCompleted: false,
    })
  }

  const handleDelete = ({ index }) => {
    const newTodos = [...todos]
    newTodos.splice(index, 1)
    setTodos(newTodos)
  }

  return (
    <div id="todoForm">
      <div class="container">
        <div class="todo_form">
          <div class="todo_input">
            <form onSubmit={handleSubmit}>
              <input
                type="text"
                id="input_todo"
                onChange={handleCurrentInput}
                value={input}
              />
            </form>
            <Todos todos={todos} handleDelete={handleDelete} />
          </div>
        </div>
      </div>
    </div>
  )
}

export default TodoApp

Todos.js

import React, { useState } from "react"

const Todos = (props) => {
  return (
    <ul>
      {props.todos.map((todo, index) => {
        return (
          <li key={todo}>
            {todo}
            <button onClick={() => props.handleDelete({ index })}>
              Delete
            </button>
          </li>
        )
      })}
    </ul>
  )
}

export default Todos

Upvotes: 0

Views: 444

Answers (5)

Imran Rafiq Rather
Imran Rafiq Rather

Reputation: 8118

isCompleted should be associated with each todo item. So, you should use todos as array and store objects within that array. Each object will have isCompleted and a task property along with a unique Id as well.

const [todos, setTodos] = useState([]);

And your submit input would look like:

 const handleSubmit = (e) => {
    e.preventDefault();
    const todo = {
      task: input,
      id: new Date().getTime().toString(), 
      isCompleted: false
    };
    const updatedTodos = [...todos, todo];

    setTodos(updatedTodos);

    console.log(updatedTodos);
    setInput("");
  };

Note: To generate unique Ids you can use uuid library. I have generated unique ids here using id: new Date().getTime().toString().

FULL WORKING CODE SANDBOX LINK: https://codesandbox.io/s/todosissue-2mc26?file=/src/TodoApp.js

Have modified handleDelete function as well :)

Upvotes: 0

Ketan Ramteke
Ketan Ramteke

Reputation: 10675

Working App: Stackblitz

enter image description here

import React, { useState, useEffect } from "react";

const TodoApp = () => {
  /* initialize todos with array 
  instead of an object               👇*/
  const [todos, setTodos] = useState([]);
  const [input, setInput] = useState("");

  const handleCurrentInput = e => {
    setInput(e.target.value);
  };
  const handleSubmit = e => {
    e.preventDefault();
    console.log(input);
    /* update the state by appending an object having 
       key todo and isCompleted to copy of our main state, 
       todos.👇
     */
    setTodos([...todos, { todo: input, isCompleted: false }]);
    setInput("");
  };

  const handleDelete = ({ index }) => {
    const newTodos = [...todos];
    newTodos.splice(index, 1);
    setTodos(newTodos);
  };

  useEffect(() => {
    console.log(JSON.stringify(todos));
  }, [todos]);

  return (
    <div id="todoForm">
      <div class="container">
        <div class="todo_form">
          <div class="todo_input">
            <form onSubmit={handleSubmit}>
              <input
                type="text"
                id="input_todo"
                onChange={handleCurrentInput}
                value={input}
              />
            </form>
            <Todos todos={todos} handleDelete={handleDelete} />
          </div>
        </div>
      </div>
    </div>
  );
};

export default TodoApp;

const Todos = props => {
  return (
    <>
      <ul>
        {props.todos.map((todo, index) => {
          return (
            <li key={index}>
              {/**use null propogation to avoid accessing the null todo value which will not exist in first render. */}
              {todo?.todo}
              <button onClick={() => props.handleDelete({ index })}>
                Delete
              </button>
            </li>
          );
        })}
      </ul>
    </>
  );
};

Upvotes: 0

hjrshng
hjrshng

Reputation: 1826

It's because you don't set to state the right way, todos got overwritten with the wrong value. You should write:

    // handleSubmit
    setTodos(s => {
      ...s,
      task: input,
      isCompleted: false,
    });

and

    // handleDelete
    const newTodos = [...todos]
    newTodos.splice(index, 1)
    setTodos(s => ({ ...s, todos: newTodos }))

Upvotes: 0

Nguyễn Văn Phong
Nguyễn Văn Phong

Reputation: 14228

You need to focus on each todo item including 2 props task, isCompleted instead of isCompleted of todos.

const [todos, setTodos] = useState([]);
var newTodo = {
                task: 'React JS',
                isCompleted: false
             };
setTodos([...todos, newTodo]);

Then your todos's structure like below:

[
    {
        task: 'Study React JS',
        isCompleted: false
    },
    {
        task: 'Study React Redux',
        isCompleted: false
    },
];

Upvotes: 2

lehnerchristian
lehnerchristian

Reputation: 1267

Your state is an object containing an array of todos. This is what you're passing to your Todos component.

So you have two options:

  1. Either pass todos.todos as a prop or
  2. (Better way) Rethink your state. isCompleted seems that it should be part of each todo, because each todo should be completed not the list itself. A list is completed if every todo isCompleted So your state would be const [todos, setTodos] = useState([])

I hope it's clear what I mean. Typing this from the phone is not so easy :-)

Upvotes: 1

Related Questions