首页 > 解决方案 > 如何使用反应备忘录

问题描述

我正在尝试制作一个简单的任务管理器应用程序,我想在 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.log(taskItemsCopy)
        console.log(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.log('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.log('prev props', prev.task)
    console.log('next props', next.task)
})

标签: javascriptreactjs

解决方案


恐怕您共享的代码存在很多问题,其中只有一些是由于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;
});

虽然这解决了最初的记忆问题,但由于几个原因,代码仍然被破坏。首先是尽管复制了您的taskItemsin 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,因为它具有相同的(隐式)依赖。我提供了一个在技术上可行的解决方案,尽管我认为这是一种 hack,并不真正推荐用于实际使用。它依赖于回调版本,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;
  });
}, []);

现在,我们不检查处理程序属性的相等性并不重要,因为该函数永远不会改变组件的初始渲染。实施这些更改后,我的沙箱分支似乎按预期工作。

更广泛地说,我真的认为在学习框架时应该考虑使用create-react-app编写 React 代码。看到您设置了自定义 webpack,我有点惊讶,而且您似乎没有为 React 提供适当的 linting(在 CRA 中自动捆绑),这会为您突出显示许多这些问题作为警告。具体来说,在Task组件中的许多地方滥用依赖数组,这将使其不稳定且容易出错,即使我建议进行基本修复。


推荐阅读