skip to Main Content

I have components DoneTodos and Todo. First one uses the second one and should return rendered list of Todos which are ‘done’ ( In todos i have this key-value, it’s false by default ). Here they are:

Todo.jsx:

export default function Todo({todo}) {
    function handleCheck() {
        todo.done = true;
        console.log(todo);
    }

    return (
        <>
            <input type="checkbox" onChange={handleCheck}/>
            <span>
                {todo.text}
            </span>
        </>
    );
}

DoneTodos,jsx:

import Todo from "./Todo.jsx";
import {useState} from "react";

// TODO: Button doesn't work at all. Do smth about it

export default function DoneTodos({todos}){
    const [state, setState] = useState(false);
    let displayTodos = [];

    function handleDoneTodos() {
        for(let i = 0; i < todos.length; i++) {
            if (todos[i].done === true) {
                displayTodos.push(todos[i]);
            }
        }
        setState(true);
    }

    if (state !== false) {
        return (
            <>
                <ul>
                    {
                        displayTodos.map((todo, key) => (
                            <li key={key}>
                                <Todo todo={todo}></Todo>
                            </li>
                        ))
                    }
                </ul>
            </>
        );
    }
    return (
        <button onClick={handleDoneTodos}>
            Show done tasks
        </button>
    );
}


and App.jsx where DoneTodos is implemented:

import './App.css'
import CreateTodos from "./components/CreateTodos.jsx";
import TodoList from "./components/TaskList.jsx";
import {useState} from "react";
import DoneTodos from "./components/DoneTodos.jsx";

export default function App() {
    const [todos, setTodos] = useState([]);

    return (
       <>
           <CreateTodos todos={todos} setTodos={setTodos} />
           <TodoList todos={todos} />
           <DoneTodos todos={todos} />
       </>
    )
}

I am new to React so maybe I’m not getting something. Help me please.

I have tried to create function handleDoneTodos without using state in it but it seems to me to be impossible.

EDIT: Let me show my CreateTodos file for full understanding

CreateTodos.jsx:

export default function CreateTodos({todos, setTodos}) {
    let onEnterClick = event => {
        if (event.key === 'Enter') {
            setTodos([
                ...todos,
                {
                    text: event.target.value,
                    done: false
                } // save todo to todos list
            ]);
            event.target.value = ''; // reset input
        }
    }

    return (
        <p>
            <input type="text" placeholder="Enter your task here!" onKeyDown={onEnterClick} />
        </p>

    );
}

3

Answers


  1. Let me give you an idea. You have to handle how you did it for ‘Create todo’ option using useState function.

    But at the same time, you also need an unique identifier for each todo you create. With that identifier, we would easily modify the todo’s state.

    Consider the below example,

    export default function App() {
      const [todos, setTodos] = useState([]);
    
      const handleCreateTodo = (todo) => {
        setTodos((prev) => {
          const count = prev.length;
          if (count > 0) {
            const lastId = prev[count - 1];
            return [...prev, { id: lastId + 1, done: false, text: todo }];
          } else {
            return [{ id: 1, text: todo, done: false }];
          }
        });
      };
    
      const handleMarkAsDone = (id) => {
        setTodos((prev) => {
          const items = { ...prev };
          const idx = items.find((item) => item.id === id);
          if (idx !== -1) {
            items[idx].done = true;
          }
          return items;
        });
      };
    
      return (
        <>
          <CreateTodos onCreate={handleCreateTodo} />
          <TodoList todos={todos} onComplete={handleMarkAsDone} />
          <DoneTodos
            todos={todos.filter((todo) => todo.done)}
            onComplete={handleMarkAsDone}
          />
        </>
      );
    }
    

    Now where every you need to update the todo in your child component, just call onComplete(2) or onCreate("Buy coffee powder at evening").

    Login or Signup to reply.
  2. I have tried to create function handleDoneTodos without using state in
    it but it seems to me to be impossible.

    It’s entirely possible since you do actually update the state state which triggers a component rerender. The issue here is the order of operations. The handleDoneTodos mutates the local displayTodos variable then enqueues a state state update. This triggers a component rerender and displayTodos is redeclared an empty array on the next render cycle.

    The good news here is that what you want to render is completely derived "state", so you only need to map the done todos under the correct condition. Here you can simply filter the todos inline when mapping to the list items.

    Example:

    export default function DoneTodos({ todos }) {
      const [showDone, setShowDone] = useState(false);
    
      function toggleShowDone() {
        setShowDone(show => !show);
      }
    
      return (
        <>
          <button onClick={toggleShowDone}>
            Toggle done tasks
          </button>
          <ul>
            {todos
              .filter(todo => showDone ? todo.done : true)
              .map((todo) => (
                <li key={todo.id}>
                  <Todo todo={todo} />
                </li>
              ))
            }
          </ul>
        </>
      );
    }
    

    Other issues I see in the code are state/prop/object mutations. Not mutating references is one of the React Commandments. You should issue a state update to correctly update from the previous state and provide new object references for the new state values.

    export default function Todo({ todo }) {
      function handleCheck() {
        todo.done = true; // <-- mutation, don't do this!
        console.log(todo);
      }
    
      return (
        <>
          <input type="checkbox" onChange={handleCheck}/>
          <span>{todo.text}</span>
        </>
      );
    }
    

    I suggest a bit of a refactor of your code to:

    • Provide GUIDs for the todos
    • Use state updates to correctly change the done state.
    • Centralize control over the todos state and how it can be updated.

    App

    import { nanoid } from "nanoid";
    
    function App() {
      const [todos, setTodos] = useState([]);
    
      const addTodo = (todo) => { // <-- control adding todos
        setTodos((todos) =>
          todos.concat({
            text: todo,
            done: false,
            id: nanoid() // <-- add GUID
          })
        );
      };
    
      const toggleDone = (todoId) => { // <-- control toggling done state
        setTodos((todos) =>
          todos.map((todo) =>
            todo.id === todoId
              ? {
                  ...todo,
                  done: !todo.done
                }
              : todo
          )
        );
      };
    
      return (
        <div className="App">
          <CreateTodos addTodo={addTodo} />
          <TodoList todos={todos} />
          <DoneTodos todos={todos} toggleDone={toggleDone} />
        </div>
      );
    }
    

    CreateTodos (Example since you didn’t include your code)

    const CreateTodos = ({ addTodo }) => {
      const submitTodo = (e) => {
        e.preventDefault();
        addTodo(e.target.todo.value);
        e.target.reset();
      };
    
      return (
        <form onSubmit={submitTodo}>
          <h1>Add Todo</h1>
          <label>
            Text
            <input type="text" name="todo" />
          </label>
          <button type="submit">Add</button>
        </form>
      );
    };
    

    Todo

    function Todo({ todo, toggleDone }) {
      function handleCheck() {
        toggleDone(todo.id);
      }
    
      return (
        <>
          <input type="checkbox" onChange={handleCheck} value={todo.done} />
          <span>{todo.text}</span>
        </>
      );
    }
    

    Demo

    Edit component-in-react-js-works-incorrectly

    Login or Signup to reply.
  3. React Functional Component: Understanding State and Re-renders

    Drew’s answer already contains the necessary corrections, but let’s dive deeper into why your initial code failed to display the list of done todos and gain a better understanding of how state works in React.

    React State and Re-renders

    In React, when you define state using the useState hook, React employs closures to ensure the persistence of state values between re-renders. This ensures that your state values do not get cleared during re-renders. A re-render is essentially React running the component function again to make necessary DOM updates.

    Two fundamental ways to trigger a re-render in React are:

    1. Changes in props.
    2. Changes in state by calling setState.

    When you call setState, here’s a simplified explanation of what happens:

    • React takes note of the new state value but doesn’t immediately change the state.
    • React schedules the state update to be applied but doesn’t do so immediately.
    • It registers that it needs to run the component function at least one more time after the current instance.
    • React batches state updates, so if you call setState multiple times within the same component, all the updates will be enqueued and actually applied during the next render cycle.

    Here’s an example:

    import React from "react";
    
    export default function Example() {
       const [state, setState] = useState(0);
    
       function handleClick() => {
        // a call is Made to setState here
        setState(1);
        // the developer then tries to check if the value of state by logging it here
        console.log(state)
    
       // the above line would log 0 and not 1, because in this current render cycle, state is still 0. And by the time state becomes 1 in the next render cycle, the user would need to click the button again to log it out as 1.
        
    }
    
       return(
       <button onClick={handleClick}>
          Open your console!
      </button>
    );
    
    }
    

    In the above example, the console.log will log 0, not 1, because during the current render cycle, the state is still 0. The state becomes 1 in the next render cycle after the user clicks the button again.

    Why the Original Code Didn’t Work

    Let’s analyze your original code:

    import Todo from "./Todo.jsx";
    import { useState } from "react";
    
    export default function DoneTodos({todos}){
       // here you set the state to false this state would persist through re-renders.
        const [state, setState] = useState(false);
        // you created an empty array and assigned it to the variable displayTodos
        let displayTodos = [];
    
        function handleDoneTodos() {
    // this for loop updates displaytodos
            for(let i = 0; i < todos.length; i++) {
                if (todos[i].done === true) {
                    displayTodos.push(todos[i]);
                }
            }
    // if you were to console.log `displayTodos` at this point, it's not an  // empty array, it contains all the doneTodos.
    
    //here you updated the state to true. 
            setState(true);
        }
    
    // Remember however, that at the time this list is being rendered, 
    // irrespective of the value of state, displayTodos has just been 
    // recreated as an empty array, and would always be empty.
        if (state !== false) {
            // On re-render, 'displayTodos' gets recreated as an empty array
            return (
                <>
                    <ul>
                        {displayTodos.map((todo, key) => (
                            <li key={key}>
                                <Todo todo={todo}></Todo>
                            </li>
                        ))}
                    </ul>
                </>
            );
        }
        return (
            <button onClick={handleDoneTodos}>
                Show done tasks
            </button>
        );
    }
    

    The issue in your original code is that displayTodos is an array created outside of the component’s state. This means it gets recreated as an empty array on every render. So, when you trigger a re-render by calling setState(true), displayTodos is reset to an empty array, causing the list to be empty.

    A Better Solution

    As Drew mentioned, a more efficient solution is to use the filter method to conditionally render the done todos.

    export default function DoneTodos({ todos }) {
      const [showDone, setShowDone] = useState(false);
    
      function toggleShowDone() {
        setShowDone((prevShowDone) => !prevShowDone);
      }
    
      return (
        <>
          <button onClick={toggleShowDone}>
            Toggle done tasks
          </button>
          <ul>
            {todos
              .filter((todo) => (showDone ? todo.done : true))
              .map((todo) => (
                <li key={todo.id}>
                  <Todo todo={todo} />
                </li>
              ))}
          </ul>
        </>
      );
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search