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:
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.