【问题标题】:React.useMemo is re-rendering all items in arrayReact.useMemo 正在重新渲染数组中的所有项目
【发布时间】:2022-02-13 21:49:14
【问题描述】:

我有一个在 useState 中存储一组水果的反应组件。我有一个过滤水果的记忆函数(visibleFruits)。我将 visibleFruits 映射到 dom。

问题是,当我检查一个水果时,所有可见的水果都会重新渲染。

我希望只有选中的那个会重新渲染,因为它是唯一一个正在改变的。

有没有办法让我使用这种模式,但防止所有人在检查时重新渲染?

在现实生活中,visibleFruits useMemo 中有一个复杂的功能。所以我不能简单地将过滤器附加到地图之前。

编辑,这里是更新编辑:


const Example = () => {
    const [fruits, setFruits] = React.useState([
        { id: 1, name: 'apple', visible: true, selected: false },
        { id: 2, name: 'banana', visible: false, selected: false },
        { id: 3, name: 'orange', visible: true, selected: false }
    ])

    const visibleFruits = React.useMemo(() => {
        return fruits.filter((f) => f.visible)
    }, [fruits])

    const handleCheck = (bool, id) => {
        setFruits((prev) => {
            return prev.map((f) => {
                if (f.id === id) {
                    f.selected = bool
                }
                return f
            })
        })
    }

    return (
        <div>
            {visibleFruits.map((fruit) => {
                return <FruitOption fruit={fruit} handleCheck={handleCheck} />
            })}
        </div>
    )
}

const FruitOption = ({ fruit, handleCheck }) => {
    console.log('** THIS RENDERS TWICE EVERY TIME USER SELECTS A FRUIT **')
    return (
        <div key={fruit.id}>
            <input
                checked={fruit.selected}
                onChange={(e) => handleCheck(e.target.checked, fruit.id)}
                type='checkbox'
            />
            <label>{fruit.name}</label>
        </div>
    )
}

export default Example

【问题讨论】:

    标签: javascript reactjs react-hooks react-usememo


    【解决方案1】:

    首先,handleCheck 函数存在问题(但这与您要询问的内容无关)。您的代码直接修改 fruit 对象 (f.selected = bool),但您是 not allowed to do that with React state,状态中的对象不得直接修改,渲染可能如果您违反该规则,则不正确。相反,您需要复制对象并修改副本(就像您使用数组一样):

    const handleCheck = (bool, id) => {
        setFruits((prev) => {
            return prev.map((f) => {
                if (f.id === id) {
                    return {...f, selected: bool}; // ***
                }
                return f;
            });
        });
    };
    

    但这不是你要问的,只是要解决的其他问题。 :-)

    您看到console.loghandleCheck 之后执行两次的原因是组件必须重新渲染(用于更改),并且有两个可见的结果,因此您看到两次调用您的FruitOption组件功能。这有两个原因:

    1. handleChange 每次调用你的Example 组件函数时都会改变,所以FruitOption 每次都会看到一个新的道具;和

    2. FruitOption 在其 props 不变时不会避免重新渲染,因此即使您修复了 #1,您仍然会看到两个 console.log 调用;和

    另外,FruitOption 元素上没有 key,这可能会导致呈现问题。渲染数组中的元素时始终包含有意义的键。 (不要只使用索引,it's problematic;但你的水果对象有一个id,这是完美的。)

    修复它:

    1. 记住handleChange,这样它就不会每次都重新创建,可能是通过useCallback,并且

    2. 使用React.memo,这样如果FruitOption 的道具没有改变,就不会被调用(请参阅此答案的末尾以获取class 等效组件),并且

    3. Example中的FruitOption元素添加一个有意义的key

    将这些和上面的handleChange 修复并将它们放在一起:

    const Example = () => {
        const [fruits, setFruits] = React.useState([
            { id: 1, name: 'apple', visible: true, selected: false },
            { id: 2, name: 'banana', visible: false, selected: false },
            { id: 3, name: 'orange', visible: true, selected: false }
        ]);
    
        const visibleFruits = React.useMemo(() => {
            return fruits.filter((f) => f.visible);
        }, [fruits]);
    
        const handleCheck = React.useCallback(
            (bool, id) => {
                setFruits((prev) => {
                    return prev.map((f) => {
                        if (f.id === id) {
                            return {...f, selected: bool}; // ***
                        }
                        return f;
                    });
                });
            },
            []  // *** No dependencies since all it uses is `setFruits`, which is
                // stable for the lifetime of the component
        );
    
        return (
            <div>
                {visibleFruits.map((fruit) => {
                    // *** Note the key
                    return <FruitOption key={fruit.id} fruit={fruit} handleCheck={handleCheck} />
                })}
            </div>
        );
    }
    
    // *** `React.memo` will compare the props and skip the call if they're the same, reusing
    // the previous call's result.
    const FruitOption = React.memo(({ fruit, handleCheck }) => {
        console.log(`Rendering fruit ${fruit.id}`);
        return (
            <div key={fruit.id}>
                <input
                    checked={fruit.selected}
                    onChange={(e) => handleCheck(e.target.checked, fruit.id)}
                    type='checkbox'
                />
                <label>{fruit.name}</label>
            </div>
        );
    });
    
    ReactDOM.render(<Example />, document.getElementById("root"));
    <div id="root"></div>
    
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>

    如您所见,所有这些都到位后,只有更改后的水果被重新渲染。

    回复React.memo:对于需求比较复杂的组件,可以提供一个函数作为第二个参数,判断两组props是否“相同”用于渲染目的。默认情况下,React.memo 只进行浅层相等比较,这通常就足够了。


    最后:对于class 组件,不提供相等回调的React.memo 的等价物是扩展PureComponent 而不是Component。如果你想让你的 props 检查更细粒度,你可以实现 shouldComponentUpdate 代替。

    【讨论】:

    • @Martin - 那里说你不需要的答案是不正确的。
    • @T.J.Crowder 哇,用 React.memo 包装组件是我一年或 2 年来一直缺少的东西。是时候回去重构一大堆代码了。非常感谢,这真是太棒了
    • @Jared - 不客气! :-)(FWIW,如果你需要它们,类组件等价物是PureComponentshouldComponentUpdate。)
    • @Jared 好吧,您的代码的第一个版本没有使用memo,您编辑的版本使用了。这对是否需要克隆数组项的问题产生了重大影响,因为现在对象身份是相关的。在第一个版本中情况并非如此。
    • @Jared - FWIW,Martin 和我选择了这个over here。 AFAIKT,Martin 基本上是在说您可以摆脱直接修改状态中的对象,尽管文档说不这样做,但有一些警告。也许你可以,但是 A) 据我所知,它是不正确的,并且随着 React 添加了高级功能,我不能保证它不会意外地破坏事情,并且 B) 这样做不会与组件一起工作优化重新渲染。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-03-01
    • 1970-01-01
    • 1970-01-01
    • 2018-01-03
    相关资源
    最近更新 更多