0

I'm making a to-do list. I want my list items to become line-through when I mark the checkbox. Therefore I am trying to make a const called markComplete. I make use of component drilling by sending my props from component 'TodoItem' to 'Todos' to 'App'.

The problem: In markComplete I try to access this because I want to change the state of the list item I checked. Though because I'm using Hooks in this project + the fact that I am kinda new, I struggle to find a solution to the error "Cannot read property 'setState' of undefined". Link to my error

So I believe with Hooks, setState() is not really a thing anymore. I think have to make use of useEffect() , but I'm not so sure. It might have something to do with binding, but I do bind in 'TodoItem' and I do use arrow functions.

Could anyone have a quick look at my code and tell me what I am missing here? Thanks in regard.

App.js

import React, { useState } from 'react';
import Todos from './components/Todos';
import './App.css';

const App = (props) => {
  const [todos, setTodos] = useState(
    [
      {
        id: 1,
        title: 'Learn React',
        completed: true
      },
      {
        id: 2,
        title: 'Eat lunch',
        completed: false
      },
      {
        id: 3,
        title: 'Meet up with friends',
        completed: false
      },
    ]
  );

  const markComplete = (id) => {
    console.log(id);

    this.setState([{todos: this.todos.map(todo => {
      if(todo.id === id) {
        todo.completed =! todo.completed
      }
      return todo;
    })}]);
  }

  return (
    <div className="App">
      {/* Todos.js is being called. Also props are being passed in Todos.js. */}
      <Todos todos={todos} markComplete={markComplete}/>
    </div>
  );


}

export default App;

  // // Log all todos in objects
  // console.log((todos));

  // // Log the 1st todo object
  // console.log((todos[0]));

  // // Log the title of the first todo
  // console.log((todos[0].title));

Todos.js

import React from 'react';
import TodoItem from './TodoItem';
import PropTypes from 'prop-types';

function Todos(props) {
  // console.log(props.todos)

  return (
    <div>
        <h1>My to do list</h1>

        {props.todos.map(todo => { // using props in child component and looping

            return (
                // Outputting all the titles from todo. It is more clean to make a seperate component for this.
                // <li>{todo.title}</li> 

                // Calling a seperate component called ToDoItem in order to return the titles.
                <TodoItem key={todo.id} todo={todo} markComplete={props.markComplete} />
            )
        })}

    </div> 
  );
}

// Defining proptypes for this class. In app.js, we see that Todos component has a prop called 'todos'. That needs to be defined here.
Todos.propTypes = {
  todos: PropTypes.array.isRequired
}

export default Todos;

TodoItem.js

import React, { useCallback } from 'react'
import PropTypes from 'prop-types';

function TodoItem(props) {
    // Change style based on state. 
    // By using useCallback, we can ensure the function App() is only redefined when one of its dependencies changes.
    // In this, case that is the dependencie 'completed'.
    // If you use id / title in the getStyle as well, you have to define these in the useCallback.
    const getStyle = useCallback(() => {  
        return {
            backgroundColor: '#f4f4f4',
            padding: '10px',
            borderBottom: '1px solid #ccc',

            // If-else text decoration based on state
            textDecoration: props.todo.completed ? 
            'line-through' : 'none'
        }
    }, [props.todo.completed]);

    // Destructuring
    const { id, title } = props.todo

    return (
        // Call out a function to change style based on state
        <div style={getStyle()}>
            <p> 
                { /* Start of ladder: passing the state though to Todos.js */ }
                { /* We use bind to see which checkbox is being marked. We use id to make this distinction.  */ }
                <input type="checkbox" onChange={props.markComplete.bind(this, id)}
                /> { ' ' }
                { title }
                {props.todo.title}
            </p>
        </div>
    )
}

// Defining proptypes for this class. In Todos.js, we see that TodoItem component has a prop called 'todo'. That needs to be defined here.
TodoItem.propTypes = {
    todo: PropTypes.object.isRequired
}

// const itemStyle = {
//     backgroundColor: '#f4f4f4'
// }

export default TodoItem
Emile Bergeron
  • 14,368
  • 4
  • 66
  • 111
trinity
  • 13
  • 4
  • Does this answer your question? [How does the "this" keyword work?](https://stackoverflow.com/questions/3127429/how-does-the-this-keyword-work) – Emile Bergeron Apr 30 '20 at 17:42

1 Answers1

2

You are trying to use this.setState in a functional component which is App. You instead need to use setTodos which is the setter for your todos state. Also use the callback approach to update state since your current state is dependent on previous state

const markComplete = (id) => {
    console.log(id);

    setTodos(prevTodos => prevTodos.map(todo => {
      if(todo.id === id) {
        todo.completed =! todo.completed
      }
      return todo;
    }));
}

Read more about useState hook in the documentation here

Shubham Khatri
  • 211,155
  • 45
  • 305
  • 318
  • Just a quick question: if I use the callback approach, the dependency array needs to have 'setTodos' right? – trinity May 01 '20 at 12:20
  • There is no dependency array to state updater function. – Shubham Khatri May 01 '20 at 13:13
  • Oh, then where do I need to implement useCallback? – trinity May 01 '20 at 13:20
  • You don't need to unless you are looking at performance. And by callback approach we mean the functional version of setTodos – Shubham Khatri May 01 '20 at 13:21
  • However if you wanted to ask about useCallback dependency to markComplete. You can pass setTodos or not because it anyways won't change – Shubham Khatri May 01 '20 at 13:22
  • Thanks for your advice. I thought useCallback was the way to fix the problem of updating the state. At the moment, the first checkbox I check updates state, but further checkboxes don't update state. Do you perhaps have any idea why this is? – trinity May 01 '20 at 13:26
  • Could you create a small demo of it and create a new post for it. I am not able to understand you problem fully – Shubham Khatri May 01 '20 at 13:28
  • My questions limit is reached ): – trinity May 01 '20 at 13:35
  • Its weird. I didn't know there is such a policy. You have just asked 6 questions. Anyways not problem. Could you create a small codesandbox and I will have a look at it – Shubham Khatri May 01 '20 at 13:39
  • Awesome! I hope this link works: https://codesandbox.io/s/inspiring-breeze-4w64g – trinity May 01 '20 at 13:46
  • Working demo: https://codesandbox.io/s/vigilant-ritchie-h31pi `setTodos(prevTodos => prevTodos.map(todo => { if (todo.id === id) { return { ...todo, completed: !todo.completed } } return todo; }) );` In the setTodo function you were mutating state which causes the issue – Shubham Khatri May 01 '20 at 14:00