Home > Software engineering >  ReactJS loop after everytime when I invoked onChange
ReactJS loop after everytime when I invoked onChange

Time:10-23

please, I'm learning React with my simple TODO App. Everything now works OK. No errors or warnings (or issues in browser). But please, can you see the console.log('PING!')?

I'm using for list of tasks this loop:

<ul>
  {tasks.map((task, index) => {
     console.log('PING!');
     return (
      <Task id={task.id} name={task.name} isFinished={task.isFinished} key={index}
            onActionButtonClick={handleActionButtonClick}/>
     );
  })}
</ul>

console.log() it's just for show the problem. handleActionButtonClick is used to passing arguments from child Task.

But If I type the first task and hit enter (submit form). Everything is OK. I got PING! message once. But then it's called after every type new character. I'm using onChange event in <input>. Look here:

<input
    id="newTaskInput"
    type="text"
    onChange={handleChange}
    value={newTask}
    placeholder="Add new task here..."
    autoFocus
/>

handleChange functions looks like here:

const handleChange = (e) => {
  if (e.target.value.length === 1) {
     e.target.value = e.target.value.toUpperCase();
  }
  setNewTask(e.target.value);
};

This is called aways when I'm typing a new task (after the first one). Look on the screenshot please:

enter image description here

You can see the first PING! was called after submit Butter item. But then I'm typing a new item called Milk and you can see 4x PING! message in console. But I'm just setting the state called setNewTask which is useState. Look at the definition:

function App() {
  const [newTask, setNewTask] = useState('');
  const [tasks, setTasks] = useState([]);
  const [editTaskId, setEditTaskId] = useState();
  // other code
}

export default App;

I will be glad if someone can explain me why is map function is called everytime after the first submit and how to avoid it. Thanks to any advice

For better understand my logic here is whole code please:

import React, { useState } from 'react';
import Task from './components/Task';
import Header from './components/Header';

function App() {
  const [newTask, setNewTask] = useState('');
  const [tasks, setTasks] = useState([]);
  const [editTaskId, setEditTaskId] = useState();

  const handleChange = (e) => {
    if (e.target.value.length === 1) {
      e.target.value = e.target.value.toUpperCase();
    }
    setNewTask(e.target.value);
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    if (editTaskId > 0) {
      //editing exisitng task
      let newTasksArray = [...tasks];
      let newTask = newTasksArray[editTaskId];
      newTask.name = newTask;
      setTasks(newTasksArray);
    } else {
      //creating new task
      setTasks([
        {
          id: [...tasks].length === 0 ? 0 : Math.max(...tasks.map((task) => task.id))   1,
          name: newTask,
          isFinished: false,
        },
        ...tasks,
      ]);
    }
    setNewTask('');
  };

  const handleActionButtonClick = (buttonValues) => {
    switch (buttonValues.type) {
      case 'edit':
        tasks.map((task) => {
          if (task.id === buttonValues.id) {
            setNewTask(task.name);
            setEditTaskId(task.id);
            return;
          }
        });
        break;
      case 'delete':
        setTasks(tasks.filter((task) => task.id !== buttonValues.id));
        break;
      default:
        //TODO: We shoudn't reverse array. Instead of that we shoud find a right id in a array and change value
        console.log(buttonValues.id);
        let newTasksArray = [...tasks].reverse();
        let newTask = newTasksArray[buttonValues.id];
        //if task if already finished we can return state
        if (newTask.isFinished) newTask.isFinished = false;
        else newTask.isFinished = true;

        setTasks(newTasksArray.reverse());
    }
  };

  return (
    <>
      <Header />
      <div className="container">
        <form onSubmit={handleSubmit}>
          <label htmlFor="newTaskInput" className="sr-only">
            Input for new task
          </label>
<input
    id="newTaskInput"
    type="text"
    onChange={handleChange}
    value={newTask}
    placeholder="Add new task here..."
    autoFocus
/>
</form>
<ul>
    {tasks.map((task, index) => {
        console.log('PING!');
        return (
            <Task
                id={task.id}
                name={task.name}
                isFinished={task.isFinished}
                key={index}
                onActionButtonClick={handleActionButtonClick}
            />
        );
    })}
</ul>
      </div>
    </>
  );
}

export default App;

Best regards folks.

CodePudding user response:

When tasks is empty, there are no elements to iterate over. The callback is never entered into, so console.log('PING!'); doesn't run.

After you've added one item to tasks, there's an element to iterate over, so a single log occurs every time there's a re-render. (Because the <input> is a controlled component, there's a re-render every time a character is typed or deleted.)

If you add multiple items to tasks, on each rerender, there will be a log for each item.

This is the way things are supposed to work in React - every time state changes (such as, when a character is typed), the component containing the state is supposed to re-render; the philosophy in React is for the view to flow from the state, so whenever the state changes, it needs to recalculate the view to see what in the DOM needs to be changed.

If you really need to, you can prevent re-renderings from running the callback again by memoizing the array of Tasks elements with useMemo - but it'd make more sense not to bother in almost all situations.

const tasksToRender = React.useMemo(() => tasks.map((task, index) => {
    console.log('PING!');
    return (
        <div>{task.name}</div>
    );
}), [tasks])

and

<ul>
    {tasksToRender}
</ul>

const { useState } = React;
function App() {
    const [newTask, setNewTask] = useState('');
    const [tasks, setTasks] = useState([]);
    const [editTaskId, setEditTaskId] = useState();

    const handleChange = (e) => {
        if (e.target.value.length === 1) {
            e.target.value = e.target.value.toUpperCase();
        }
        setNewTask(e.target.value);
    };

    const handleSubmit = (e) => {
        e.preventDefault();
        if (editTaskId > 0) {
            //editing exisitng task
            let newTasksArray = [...tasks];
            let newTask = newTasksArray[editTaskId];
            newTask.name = newTask;
            setTasks(newTasksArray);
        } else {
            //creating new task
            setTasks([
                {
                    id: [...tasks].length === 0 ? 0 : Math.max(...tasks.map((task) => task.id))   1,
                    name: newTask,
                    isFinished: false,
                },
                ...tasks,
            ]);
        }
        setNewTask('');
    };

    const handleActionButtonClick = (buttonValues) => {
        switch (buttonValues.type) {
            case 'edit':
                tasks.map((task) => {
                    if (task.id === buttonValues.id) {
                        setNewTask(task.name);
                        setEditTaskId(task.id);
                        return;
                    }
                });
                break;
            case 'delete':
                setTasks(tasks.filter((task) => task.id !== buttonValues.id));
                break;
            default:
                //TODO: We shoudn't reverse array. Instead of that we shoud find a right id in a array and change value
                console.log(buttonValues.id);
                let newTasksArray = [...tasks].reverse();
                let newTask = newTasksArray[buttonValues.id];
                //if task if already finished we can return state
                if (newTask.isFinished) newTask.isFinished = false;
                else newTask.isFinished = true;

                setTasks(newTasksArray.reverse());
        }
    };
    const tasksToRender = React.useMemo(() => tasks.map((task, index) => {
        console.log('PING!');
        return (
            <div>{task.name}</div>
        );
    }), [tasks])
    return (
        <div className="container">
            <form onSubmit={handleSubmit}>
                <label htmlFor="newTaskInput" className="sr-only">
                    Input for new task
                </label>
                <input
                    id="newTaskInput"
                    type="text"
                    onChange={handleChange}
                    value={newTask}
                    placeholder="Add new task here..."
                    autoFocus
                />
            </form>
            <ul>
                {tasksToRender}
            </ul>
        </div>
    );
}

ReactDOM.createRoot(document.querySelector('.react')).render(<App />);
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<div class='react'></div>

CodePudding user response:

If you want to avoid rerendering on every char input :

<input
    id="newTaskInput"
    type="text"
    onSubmit={handleChange}
    value={newTask}
    placeholder="Add new task here..."
    autoFocus
/>

Why ? because on every change happen or char entered you call handleChange function which set a new state and rerender the component, and to avoid this you can add a button to submit or track the change and listen for Enter key to submit the data.

  • Related