reactjsstaterefreshpage-refreshreact-dnd

When updating a React component rendered in a list, the component for a different item in the list changes


I have a Task Component and in there have a handleCheckboxChange that if I checked the tast after 3 seconds it should go to the done list and if unchecked it should go at the begining of Todo list and it work fine but when I checked or unchecked more than one task it just change the last one position how should I fix that? my Body component:

import React, { useEffect, useState } from 'react'
import TodoContainer from './TodoContainer'

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

const handleDeleteTask = (taskIndex, section) => {
    const updatedTodos = { ...todos };
    updatedTodos[section] = todos[section].filter((_, index) => index !== taskIndex);
    localStorage.setItem("todos", JSON.stringify(updatedTodos));
    setTodos(updatedTodos);
};

const handleAddTask = (newTask, section) => {
    const updatedTodos = { ...todos };
    updatedTodos[section] = [newTask, ...updatedTodos[section]];
    localStorage.setItem("todos", JSON.stringify(updatedTodos));
    setTodos(updatedTodos);
};

useEffect(() => {
    const hasInitialDataLS = localStorage.getItem("hasInitialDataLS");

    if (!hasInitialDataLS) {
        // Pre-save initial information
        const preSavedData = {
            todo: [
                { title: 'Start with meditation, exercise & breakfast for a productive day', check: 0 },
                { title: 'Read to learn something new every day', check: 0 },
                { title: 'Learn something fresh & relevant', check: 0 }
            ],
            doing: [
                { title: 'Engage & question in meetings', check: 0 },
                { title: 'Use time-blocking for effective days', check: 0 }
            ],
            done: [
                { title: 'Use time-blocking for effective days', check: 1 },
                { title: 'Congratulate yourself for incorporating healthier habits into your lifestyle, like regular exercise or mindful eating', check: 1 }
            ]
        };
        localStorage.setItem("todos", JSON.stringify(preSavedData));
        localStorage.setItem("hasInitialDataLS", true);
        setTodos(preSavedData);
    }
    if (hasInitialDataLS) {
        const storeTodos = localStorage.getItem("todos");
        if (storeTodos) {
            setTodos(JSON.parse(storeTodos));
        }
    }
}, []);

useEffect(() => {
    // Update localStorage whenever todos state changes
    localStorage.setItem("todos", JSON.stringify(todos));
}, [todos]); // This effect runs whenever todos change

return (
    <div className="body">
        <TodoContainer
            className="todo"
            title="Todo"
            requirementTasks={todos.todo}
            onDeleteTask={(taskIndex) => handleDeleteTask(taskIndex, 'todo')}
            setTodos={setTodos}
            todos={todos}
            onAddTask={(newTask) => handleAddTask(newTask, 'todo')}
        />
        <TodoContainer
            className="doing"
            title="Doing 💪"
            requirementTasks={todos.doing}
            onDeleteTask={(taskIndex) => handleDeleteTask(taskIndex, 'doing')}
            setTodos={setTodos}
            todos={todos}
            onAddTask={(newTask) => handleAddTask(newTask, 'doing')}
        />
        <TodoContainer
            className="done"
            title="Done 🎉"
            requirementTasks={todos.done}
            onDeleteTask={(taskIndex) => handleDeleteTask(taskIndex, 'done')}
            setTodos={setTodos}
            todos={todos} />
    </div>
)
}

TodoContainer:

import React, { useState, useEffect, useRef } from 'react'
import Task from './Task'
import Button from './Button';
import { useDrop } from 'react-dnd';
import { ItemTypes } from './constants';

export default function TodoContainer({
className,
title,
requirementTasks,
onDeleteTask,
setTodos,
todos,
onAddTask
}) {
const [isDraggingOver, setIsDraggingOver] = useState(false);
const ref = useRef();

const [{ isOver }, drop] = useDrop({
    accept: ItemTypes.TASK,
    drop: (item) => {
        const { index: originalIndex, section: originalSection } = item;
        const newSection = className; // The current section where the drop occurred
        if (originalSection !== newSection) {
            // Handle the task movement from originalSection to newSection
            const updatedOriginalTasks = [...todos[originalSection]];
            const updatedNewTasks = [...todos[newSection]];
            const taskToMove = updatedOriginalTasks.splice(originalIndex, 1)[0];
            if (className === 'done') {
                taskToMove.check = 1;
            } else {
                taskToMove.check = 0; // Reset check status when moving tasks
            }
            updatedNewTasks.unshift(taskToMove);

            setTodos((prevTodos) => ({
                ...prevTodos,
                [originalSection]: updatedOriginalTasks,
                [newSection]: updatedNewTasks,
            }));
        }
    },
    collect: (monitor) => ({
        isOver: !!monitor.isOver(),
    }),
});

useEffect(() => {
    drop(ref); // Pass the ref of the container to the drop function

    const handleDragOver = (event) => {
        event.preventDefault();
        setIsDraggingOver(true);
    };

    const handleDragLeave = () => {
        setIsDraggingOver(false);
    };

    ref.current.addEventListener("dragover", handleDragOver);
    ref.current.addEventListener("dragleave", handleDragLeave);

    return () => {
        ref.current.removeEventListener("dragover", handleDragOver);
        ref.current.removeEventListener("dragleave", handleDragLeave);
    };
}, [drop]);

const tasks = requirementTasks || [];

return (
    <div className={`${className} todo-container  ${isDraggingOver ? "dragging-over" : ""}`} ref={ref}>
        <div className="todo-header">
            <h3>{title}</h3>
            <small>{tasks.length} Tasks</small>
        </div>

        {tasks.map((task, index) => (
            <div className="tasks" key={index}>
                <Task
                    context={task.title}
                    check={task.check}
                    handleDeleteTask={() => onDeleteTask(index)}
                    index={index}
                    setTodos={setTodos}
                    todos={todos}
                    requirementTasks={requirementTasks}
                    onAddTask={onAddTask}
                    section={className} />
            </div>
        ))}
        {className === 'done' ? '' : <Button
            type='add'
            onClickFun={() => { onAddTask({ title: '', check: 0 }); }} />}
    </div>
)
}

Task component

import React, { useState, useRef, useEffect } from 'react'
import { useDrag } from 'react-dnd';
import { ItemTypes } from './constants';
import Button from './Button';

export default function Task({ context, check, handleDeleteTask, index, setTodos, todos, 
requirementTasks, section }) {
const [, drag] = useDrag({
    type: ItemTypes.TASK, // Specify the item type
    item: { index, section }, // Data to be transferred during the drag
});
const [isEditing, setIsEditing] = useState(false)
const [text, setText] = useState(context)
const textareaRef = useRef(null)
const [isChecked, setIsChecked] = useState(check);

const handleSpanClick = () => {
    setIsEditing(true)
}

const handleTextChange = (event) => {
    setText(event.target.value)
    // Update the corresponding task in the state
    const updatedTasks = [...requirementTasks];
    updatedTasks[index].title = event.target.value;
    setTodos({
        ...todos,
        [section]: updatedTasks,
    });
}

const handleBlur = () => {
    setIsEditing(false)
}

const handleCheckboxChange = () => {
    setIsChecked(prevIsChecked => !prevIsChecked); // Use functional update

    // Create a copy of the task to move
    const taskToMove = { ...requirementTasks[index] };

    // Update the task's check property
    taskToMove.check = isChecked ? 0 : 1; // Reverse the check values

    // Create new instances of task arrays
    const updatedSectionTasks = [...todos[section]];
    const updatedTargetTasks = isChecked ? [...todos.todo] : [...todos.done]; // Reverse the target sections

    // Remove task from the current section
    updatedSectionTasks.splice(index, 1);

    // Push the copied task to the target section
    updatedTargetTasks.unshift(taskToMove);

    // Update the state and localStorage
    setTimeout(() => {
        setTodos({
            ...todos,
            [section]: updatedSectionTasks,
            [isChecked ? "todo" : "done"]: updatedTargetTasks // Reverse the target section keys
        });
    }, 3000);
};


useEffect(() => {
    if (isEditing && textareaRef.current) {
        const textarea = textareaRef.current
        const textLength = textarea.value.length
        textarea.setSelectionRange(textLength, textLength)
        textarea.focus()
    }
}, [isEditing])

return (
    <div className={`task ${isEditing ? '' : 'dis-flex'}`} ref={drag}>
        <label className="checkbox-container">
            <input
                type="checkbox"
                checked={isChecked}
                onChange={handleCheckboxChange}
            />
            <span className={`checkmark ${isChecked ? 'checked' : ''}`}></span>
        </label>
        {isEditing ? (
            <textarea
                ref={textareaRef}
                value={text}
                onChange={handleTextChange}
                onBlur={handleBlur}
                className="task-textarea"
            />
        ) : (
            <span onClick={handleSpanClick} className={`${isChecked ? 'text-decoration' : ''}`}>{text ? text : (<input placeholder="new Task" className='new-task'></input>)}</span>
        )}
        <Button
            type='delete'
            onClickFun={() => {
                handleDeleteTask(index);
            }}
        />
    </div>
)
}

Solution

  • but when I checked or unchecked more than one task it just change the last one position

    You seem to be describing what can happen when you render a list without using the key prop.

    React needs to infer what changes happened to the specific item in your list.

    So long as you return the same element type, even if props change, React will not be able to update the corresponding components/DOM nodes in a list if you don't provide a key.

    You probably want to ensure your todo list state is an array of your todo items and then map over them, providing the parent element inside your map with a unique key prop for each todo.

    Example

    return (
        <div className='body'>
          {todos.map((todo) => (
            <TodoContainer
              key={todo.id}
              // your other props...
            />
          ))}
        </div>
    );
    

    Resources

    https://react.dev/learn/rendering-lists

    https://kentcdodds.com/blog/understanding-reacts-key-prop