Я пытаюсь создать простое приложение диспетчера задач и хочу реализовать заметку реакции в TaskRow (элемент задачи), но когда я устанавливаю флажок, чтобы завершить задачу, свойства компонентов одинаковы, и я не могу их сравнить, и все задачи повторяются. -рендеринг еще раз, есть предложения? Спасибо
Песочница: https://codesandbox.io/s/interesting-tharp-ziwe3?file=/src/components/Tasks/Tasks.jsx
Компонент задач
import React, { useState, useEffect, useCallback } from 'react'
import TaskRow from "../TaskRow";
function Tasks(props) {
const [taskItems, setTaskItems] = useState([])
useEffect(() => {
setTaskItems(JSON.parse(localStorage.getItem('tasks')) || [])
}, [])
useEffect(() => {
if (!props.newTask) return
newTask({ id: taskItems.length + 1, ...props.newTask })
}, [props.newTask])
const newTask = (task) => {
updateItems([...taskItems, task])
}
const toggleDoneTask = useCallback((id) => {
const taskItemsCopy = [...taskItems]
taskItemsCopy.map((t)=>{
if (t.id === id){
t.done = !t.done
return t
}
return t
})
console.info(taskItemsCopy)
console.info(taskItems)
updateItems(taskItemsCopy)
}, [taskItems])
const updateItems = (tasks) => {
setTaskItems(tasks)
localStorage.setItem('tasks', JSON.stringify(tasks))
}
return (
<React.Fragment>
<h1>learning react </h1>
<table>
<thead>
<tr>
<th>Title</th>
<th>Description</th>
<th>Done</th>
</tr>
</thead>
<tbody>
{
props.show ? taskItems.map((task, i) =>
<TaskRow
task = {task}
key = {task.id}
toggleDoneTask = {()=>toggleDoneTask(task.id)}>
</TaskRow>)
:
taskItems.filter((task) => !task.done)
.map((task) =>
<TaskRow
show = {props.show}
task = {task}
key = {task.id}
toggleDoneTask = {()=>toggleDoneTask(task.id)}></TaskRow>
)
}
</tbody>
</table>
</React.Fragment>
)
}
export default Tasks
Задача элемента (компонент TaskRow)
import React, { memo } from 'react'
function TaskRow(props) {
return (<React.Fragment>
{console.info('render', props.task)}
<Tr show = {props.show} taskDone = {props.task.done}>
<td>
{props.task.title}
</td>
<td>
{props.task.description}
</td>
<td>
<input type = "checkbox"
checked = {props.task.done}
onChange = {props.toggleDoneTask}
/>
</td>
</Tr>
</React.Fragment>)
}
export default memo(TaskRow, (prev,next)=>{
console.info('prev props', prev.task)
console.info('next props', next.task)
})
Боюсь, что с кодом, которым вы поделились, довольно много проблем, только некоторые из них из-за React.memo
. Я начну с этого и проработаю те, что заметил.
Вы предоставили функцию проверки равенства для memo
, но вы не используете ее для проверки чего-либо. Поведение по умолчанию, которое не требует функции тестирования, будет поверхностно сравнивать свойства между предыдущим и следующим рендерингом. Это означает, что он будет обнаруживать различия между примитивными значениями (например, строкой, числом, логическим значением) и ссылками на объекты (например, литералы, массивы, функции), но не будет автоматически глубоко сравнивать эти объекты.
Помните, что memo
разрешает повторную визуализацию только в том случае, если функция проверки на равенство возвращает false. Вы не предоставили возвращаемое значение функции тестирования, что означает, что она возвращает undefined
, что является ложным. Я предоставил простую функцию тестирования для литерала объекта с примитивными значениями, которая будет выполнять необходимую здесь работу. Если у вас есть более сложные объекты для передачи в будущем, я предлагаю использовать комплексную глубокую проверку равенства, такую как та, которая предоставляется библиотекой lodash, или, что еще лучше, вообще не передавать объекты, если вы можете помочь, и вместо этого попытаться придерживаться к примитивным значениям.
export default memo(TaskRow, (prev, next) => {
const prevTaskKeys = Object.keys(prev.task);
const nextTaskKeys = Object.keys(next.task);
const sameLength = prevTaskKeys.length === nextTaskKeys.length;
const sameEntries = prevTaskKeys.every(key => {
return nextTaskKeys.includes(key) && prev.task[key] === next.task[key];
});
return sameLength && sameEntries;
});
Хотя это решает первоначальную проблему с запоминанием, код по-прежнему не работает по нескольким причинам. Во-первых, несмотря на копирование вашего taskItems
в toggleTaskDone
, по тем же причинам, что и описанные выше, ваш массив объектов не копируется глубоко. Вы помещаете объекты в новый массив, но ссылки на эти объекты сохраняются из предыдущего массива. Любые изменения, которые вы вносите в эти объекты, будут напрямую изменять состояние React, вызывая рассинхронизацию значений с остальными вашими эффектами.
Вы можете решить эту проблему, сопоставив копию и распространив объекты. Вам нужно будет сделать это для каждого уровня ссылки на объект в любом объекте состояния, который вы пытаетесь изменить, что является одной из причин, по которой React не рекомендует использовать сложные объекты в useState
(один уровень глубины обычно подходит).
const taskItemsCopy = [...taskItems].map((task) => ({ ...task }));
Боковое примечание: вы ничего не делаете с результатом taskItemsCopy в исходном коде. map
не является мутирующим методом — его вызов без присвоения результата переменной ничего не делает.
Следующая проблема более тонкая и демонстрирует одну из ловушек и потенциальных сложностей при запоминании компонентов. Обратный вызов toggleTaskDone
имеет taskItems
в своем массиве зависимостей. Однако вы передаете его как реквизит в анонимной функции TaskRow
. Это свойство не рассматривается React.memo
— мы специально игнорируем его, потому что мы хотим перерисовывать только изменения в самом объекте задачи. Это означает, что когда задача меняет свой статус done
, все остальные задачи перестают синхронизироваться с новым значением taskItems
— когда они меняют свой статус done
, они будут использовать значение taskItems
, как это было в последний раз. были оказаны.
Встроенные анонимные функции воссоздаются при каждом рендеринге, поэтому они всегда неравны по ссылке. На самом деле вы можете исправить это, изменив способ передачи и выполнения обратного вызова:
// Tasks.jsx
toggleDoneTask = {toggleDoneTask}
// TaskRow.jsx
onChange = {() => props.toggleDoneTask(props.task.id)}
Таким образом, вы сможете проверить изменения ссылки в вашей функции равенства memo
, но, поскольку обратный вызов меняется каждый раз при изменении taskItems
, это сделает мемоизацию совершенно бесполезной!
Так что делать. Здесь реализация остальной части компонента Tasks
начинает нас немного ограничивать. У нас не может быть taskItems
в зависимости от toggleTaskDone
, и мы также не можем вызывать updateItems
, потому что у него такая же (неявная) зависимость. Я предоставил решение, которое технически работает, хотя я бы посчитал это хаком и не рекомендовал бы его для фактического использования. Он основан на версии обратного вызова setState
, которая позволит нам получить доступ к текущему значению taskItems
, не включая его в качестве зависимости.
const toggleDoneTask = useCallback((id) => {
setTaskItems((prevItems) => {
const prevCopy = [...prevItems].map((task) => ({ ...task }));
const newItems = prevCopy.map((t) => {
if (t.id === id) t.done = !t.done;
return t;
});
localStorage.setItem("tasks", JSON.stringify(newItems));
return newItems;
});
}, []);
Теперь не имеет значения, что мы не проверяем свойство обработчика на равенство, потому что функция никогда не меняется по сравнению с первоначальным рендерингом компонента. После внесения этих изменений моя вилка вашей песочницы, кажется, работает так, как ожидалось.
В более широком смысле, я действительно думаю, что вам следует подумать о написании кода React с помощью create-react-app, когда вы изучаете фреймворк. Я был немного удивлен, увидев, что у вас настроен собственный веб-пакет, и у вас, похоже, нет надлежащего линтинга для React (автоматически включенного в CRA), который выделил бы для вас многие из этих проблем в виде предупреждений. В частности, неправильное использование массива зависимостей в ряде мест в компоненте Task
, что сделает его нестабильным и подверженным ошибкам даже с предложенными существенными исправлениями.
Когда вы вводите memo
в свои компоненты, вы берете на себя прямую ответственность за то, какие значения они обновляют и когда. Недавно я ответил на аналогичный вопрос , в котором это рассматривается более подробно. Я также кратко объясню, почему делать это, чтобы обойти объявление ваших зависимостей, — плохая практика. Способ реализовать это «правильно» состоит в том, чтобы переключиться на редуктор шаблон состояния, который делает то же самое, что и мое решение, но более структурированным и удобочитаемым способом.
Конечно, «хак» гораздо более удобочитаем, структурирован и удобочитаем, но почему бы вам не порекомендовать его?
Я говорю, что шаблон редуктора лучше структурирован и, в конечном счете, более удобочитаем, потому что он явно отделяет обратный вызов обработчика от фактической установки состояния. Поскольку useState
на самом деле является специальной реализацией useReducer
под капотом, след функций, вызываемых при использовании обратного вызова useState
, очень похож. В этом случае лучше просто быть «честным» в отношении того, как на самом деле работает ваше состояние, и создать свой собственный редьюсер. Может быть, «взломать» — слишком сильное слово — исходный код, который я разместил, вполне действителен, я просто не считаю его идеальным.
Последний вопрос, и я больше не беспокою вас, хахаха, когда вы говорите: «У нас не может быть taskItems в зависимости от toggleTaskDone, и мы также не можем вызывать updateItems, потому что у него такая же (неявная) зависимость», почему мы не может иметь taskItems в зависимости от toggleTaskDone?, спасибо за всю вашу помощь и извините за мой плохой английский xD
Без проблем! Подумайте об этих шагах: 1) taskItems
находится в массиве зависимостей, 2) всякий раз, когда taskItems
изменяется, обратный вызов воссоздается с новыми значениями, 3) если вы передаете обратный вызов своему запоминаемому компоненту, тогда компонент необходимо перерисовывать каждый раз. время taskItems
изменяется, чтобы получить воссозданный обратный вызов, 4) если вы это сделаете, нет смысла запоминать его вообще, потому что все TaskRow
компоненты будут перерисовываться всякий раз, когда один из них имеет изменение значения.
Здравствуйте, все, что вы мне рассказали, мне очень помогло, но я до сих пор не понимаю, почему вы должны использовать этот «хак», я пытался искать информацию в Интернете, но не нашел ничего, что отвечало бы на мой вопрос. Я знаю, что без "хака" задачи не синхронизируются, но не понимаю почему, спасибо за помощь!