搜索

首页  >  问答  >  正文

在 React 中使用基于循环的参数有效地定义 useCallback 函数

问题

在反应中,我确实经常需要使用诸如 useCallback 之类的内容来记忆项目列表中的函数(通过循环创建),以避免由于引用标识符不匹配而导致单个元素发生更改而重新渲染所有组件……不幸的是,这是令人惊讶的是很难到期。例如,考虑以下代码:

const MyComp = memo({elements} => {
  {
    elements.map((elt, i) => {
      <>{elt.text}<Button onClick={(e) => dispatch(removeElement({id: i}))}> <>
    })
  }
})

其中 Button 是由 ant design 等提供的外部组件。然后,这个函数引用在每次渲染时都会不同,因为它是内联的,因此强制重新渲染。

一个(糟糕的)解决方案

为了避免这个问题,我可以想到另一种解决方案:创建一个新组件 MyButton,它接受两个属性 index={i}onClick 而不是单个 onClick,并将参数 index 附加到任何调用onClick:

const MyButton = ({myOnClick, id, ...props}) => {
  const myOnClickUnwrap = useCallback(e => myOnClick(e, id), [myOnClick]);
  return <Button onClick={myOnClickUnwrap} ...props/>
};

const MyComp = memo({elements} => {
  const myOnClick = useCallback((e, id) => dispatch(removeElement({id: id})), []);
  return 
    {
      elements.map((elt, i) => {
      <>{elt.text}<Button id={i} onClick={myOnClick}> <>
    })
  }
)

为什么我想要更好的方法

虽然这确实有效,但由于多种原因,这是非常不实用的:

我缺少更好的解决方案吗?考虑到列表无处不在,我不敢相信对此没有适当的解决方案,而且我很惊讶地看到这方面的文档很少。

编辑 我尝试这样做:

// Define once:
export const WrapperOnChange = memo(({onChange, index, Component, ...props}) => {
    const onChangeWrapped = useCallback(e => onChange(e, index), [onChange, index]);
    return <Component {...props} onChange={onChangeWrapped} />
});

export const WrapperOnClick = memo(({onClick, index, Component, ...props}) => {
    const onClickWrapped = useCallback(e => onClick(e, index), [onClick, index]);
    return <Component {...props} onClick={onClickWrapped} />
});

并像这样使用它:

const myactionIndexed = useCallback((e, i) => dispatch(removeSolverConstraint({id: i})), []);
return <WrapperOnClick index={i} Component={Button} onClick={myactionIndexed} danger><CloseCircleOutlined /></WrapperOnClick>
但这仍然不完美,特别是我需要一个用于不同嵌套级别的包装器,每当我定位一个新属性时我都需要创建一个新版本(onClickonChange,...),如果我有它就无法直接工作多个属性(例如 onClickonChange),我以前从未见过这个,所以我想有更好的解决方案。

编辑 我尝试了各种想法,包括使用 fast-memoize,但我仍然不明白所有结果:有时,fast-memoize 有效,有时失败......而且我不知道 fast-memoize 是否是推荐的解决方案:似乎对于如此常见的用例使用第三方工具很奇怪。在这里查看我的测试https://codesandbox.io/embed/magical-dawn-67mgxp?fontsize=14&hidenavigation=1&theme=dark

P粉550823577P粉550823577302 天前451

全部回复(2)我来回复

  • P粉001206492

    P粉0012064922024-01-17 12:54:38

    1. 首先,不建议使用index作为参数或props或key,因为当你删除第一个时,所有子组件都会重新渲染。
    2. 并且根据你的场景如果你想避免重新渲染,我有一些想法,你可以参考一下,像这样:
    const WrapperEvent = (Component) => {
      return memo(function Hoc({ onClick, onChange, onOtherEvent, eventData, ...restProps }) {
        return (
          <Component onClick={() => onClick?.(eventData)} onChange={() => onChange?.(eventData)} onOtherEvent={() => onOtherEvent?.(eventData)} {...restProps} />
        )
      })
    }
    
    const WrapperButton = WrapperEvent(MyButton)
    
    const Version2 = memo(({ deleteElement, elements }) => {
      return (
        <>
          <ul>
            {elements.map((e) => (
              <li key={e}>
                <Text>{e}</Text>{" "}
                <WrapperButton eventData={e} onClick={deleteElement}>Delete</WrapperButton>
              </li>
            ))}
          </ul>
        </>
      );
    });
    
    const initialState = ["Egg", "Milk", "Potatoes", "Tomatoes"].concat(
      [...Array(0).keys()].map((e) => e.toString())
    );
    export default function App() {
      const [elements, setElements] = useState(initialState);
      const restart = useCallback((e) => setElements(initialState), []);
      const deleteElement = useCallback(
        (name) => setElements((elts) => elts.filter((e, i) => e !== name)),
        []
      );
      return (
        <div className="App">
          <h1>Trying to avoid redraw</h1>
          <button onClick={restart}>Restart</button>
          <Version2 elements={elements} deleteElement={deleteElement} />
        </div>
      );
    }
    

    这里的测试 https://codesandbox.io /s/sharp-wind-rd48q4?file=/src/App.js

    回复
    0
  • P粉916760429

    P粉9167604292024-01-17 00:55:55

    警告:我不是 React 专家(因此我的问题!),所以请在下面发表评论和/或添加 +1,如果您认为此解决方案是在 React 中进行的规范方法(或 -1不是^^)。我也很好奇为什么其他一些解决方案失败了(例如基于 proxy-memoize(实际上比没有缓存长 10 倍,并且根本不缓存)或 fast-memoize(并不总是缓存,具体取决于如何我使用它)),所以如果你知道我有兴趣知道)

    由于我对这个问题没什么兴趣,所以我尝试根据各种选择(无记忆、使用外部库(快速记忆与代理记忆)、使用包装器)对一堆解决方案(14!)进行基准测试,使用外部组件等...

    最好的方法似乎是创建一个新组件包含列表的整个元素,而不仅仅是最后一个按钮。这允许相当干净的代码(即使我需要为列表和项目创建两个组件,至少它在语义上是有意义的),避免外部库,并且似乎比我尝试过的所有其他方法更有效(至少以我的例子为例):

    const Version13Aux = memo(({ onClickIndexed, index, e }) => {
      const action = useCallback((e) => onClickIndexed(e, index), [
        index,
        onClickIndexed
      ]);
      return (
        <li>
          <Text>{e}</Text>
          <Button onClick={action}>Delete</Button>
        </li>
      );
    });
    
    const Version13 = memo(({ deleteElement, elements }) => {
      const actionOnClickIndexed = useCallback((e, i) => deleteElement(i), [
        deleteElement
      ]);
      return (
          <ul>
            {elements.map((e, i) => (
              <Version13Aux index={i} onClickIndexed={actionOnClickIndexed} e={e} />
            ))}
          </ul>
      );
    });

    我仍然不太喜欢这个解决方案,因为我需要将许多内容从父组件转发到子组件,但这似乎是我能得到的最佳解决方案......

    您可以查看我的尝试列表 这里,我使用了下面的代码。这是探查器的视图(从技术上讲,所有版本之间的时间差异并不大(除了使用 proxy-memoize 的版本 7,我删除了它,因为它更长,也许是 10 倍,并且正在制作图表)更难阅读),但我预计这种差异在较长的列表上会更大,其中项目绘制起来更加复杂(这里我只有一个文本和一个按钮)。请注意,所有版本并不完全相同(有些版本使用 ,一些普通列表,一些 Ant 设计列表...),所以时间比较只有在执行相同操作的版本之间才有意义。无论如何,我主要关心查看缓存的内容和未缓存的内容,这在分析器中清晰可见(缓存了浅灰色块):

    另一个有趣的事实是,您可能希望在记忆之前进行基准测试,因为改进可能并不显着,至少对于简单的组件而言(此处大小为 5,只有一个文本和一个按钮)。

    import "./styles.css";
    import { Button, List, Typography } from "antd";
    import { useState, useCallback, memo, useMemo } from "react";
    import { memoize, memoizeWithArgs } from "proxy-memoize";
    import memoizeFast from "fast-memoize";
    const { Text } = Typography;
    
    const Version1 = memo(({ deleteElement, elements }) => {
      return (
        <>
          <h2>
            Version 1: naive version that should be inneficient (normal button)
          </h2>
          <p>
            Interestingly, since button is not a component, but a normal html
            component, nothing is redrawn.
          </p>
          <ul>
            {elements.map((e, i) => (
              <li>
                <Text>{e}</Text>{" "}
                <button onClick={(e) => deleteElement(i)}>Delete</button>
              </li>
            ))}
          </ul>
        </>
      );
    });
    
    const Version2 = memo(({ deleteElement, elements }) => {
      return (
        <>
          <h2>
            Version 2: naive version that should be inneficient (Ant design button)
          </h2>
          <p>
            Using for instance Ant Design's component instead of button shows the
            issue. Because onClick is inlined, the reference is different on every
            call which triggers a redraw.
          </p>
          <ul>
            {elements.map((e, i) => (
              <li>
                <Text>{e}</Text>{" "}
                <Button onClick={(e) => deleteElement(i)}>Delete</Button>
              </li>
            ))}
          </ul>
        </>
      );
    });
    
    const Version3AuxButton = memo(({ onClickIndexed, index }) => {
      const action = (e) => onClickIndexed(e, index);
      return <Button onClick={action}>Delete</Button>;
    });
    
    const Version3 = memo(({ deleteElement, elements }) => {
      const actionOnClickIndexed = useCallback((e, i) => deleteElement(i), [
        deleteElement
      ]);
      return (
        <>
          <h2>Version 3: works but really dirty (needs a new wrapper)</h2>
          <p>
            This works, but I don't like this solution because I need to manually
            create a new component, which makes the code more complicated, and it
            composes poorly since I need to create a new version for every
            nested-level.
          </p>
          <ul>
            {elements.map((e, i) => (
              <li>
                <Text>{e}</Text>
                <Version3AuxButton
                  index={i}
                  onClickIndexed={actionOnClickIndexed}
                />
              </li>
            ))}
          </ul>
        </>
      );
    });
    
    // We try to create a wrapper to automatically do the above code
    const WrapperOnClick = memo(
      ({ onClickIndexed, index, Component, ...props }) => {
        const onClickWrapped = useCallback((e) => onClickIndexed(e, index), [
          onClickIndexed,
          index
        ]);
        return <Component {...props} onClick={onClickWrapped} />;
      }
    );
    
    const Version4 = memo(({ deleteElement, elements }) => {
      const actionOnClickIndexed = useCallback((e, i) => deleteElement(i), [
        deleteElement
      ]);
      return (
        <>
          <h2>Version 4: using a wrapper, it does work</h2>
          <p>
            Using a wrapper gives slightly less ugly code (at least I don’t need to
            redefine one wrapper per object), but still it’s not perfect (need to
            improve it to deal with nested level, different names (onChange,
            onClick, myChange…), multiple elements (what if you have both onClick
            and onChange that you want to update?), and still I don't see how to use
            it with List.item from Ant Design)
          </p>
          <ul>
            {elements.map((e, i) => (
              <li>
                <Text>{e}</Text>{" "}
                <WrapperOnClick
                  Component={Button}
                  index={i}
                  onClickIndexed={actionOnClickIndexed}
                >
                  Delete
                </WrapperOnClick>
              </li>
            ))}
          </ul>
        </>
      );
    });
    
    const Version5naive = memo(({ deleteElement, elements }) => {
      return (
        <>
          <h2>
            Version 5 naive: using no wrapper but List from Ant design. I don’t
            cache anything nor use usecallback: it does NOT work
          </h2>
          <p>
            Sometimes, with this version I got renders every second without apparent
            reason. Not sure why I don’t have this issue here.
          </p>
          <List
            header={<div>Header</div>}
            footer={<div>Footer</div>}
            bordered
            dataSource={elements}
            renderItem={(e, i) => (
              <List.Item>
                <Text>{e}</Text>{" "}
                <Button onClick={(e) => deleteElement(i)}>Delete</Button>
              </List.Item>
            )}
          />
        </>
      );
    });
    
    const Version5 = memo(({ deleteElement, elements }) => {
      const header = useMemo((e) => <div>Header</div>, []);
      const footer = useMemo((e) => <div>Footer</div>, []);
      const renderItem = useCallback(
        (e, i) => (
          <List.Item>
            <Text>{e}</Text>{" "}
            <Button onClick={(e) => deleteElement(i)}>Delete</Button>
          </List.Item>
        ),
        [deleteElement]
      );
      return (
        <>
          <h2>
            Version 5: like version 5 naive (using no wrapper but List from Ant
            design) with an additional useCallback: it does NOT work
          </h2>
          <p></p>
          <List
            header={header}
            footer={footer}
            bordered
            dataSource={elements}
            renderItem={renderItem}
          />
        </>
      );
    });
    
    const Version6 = memo(({ deleteElement, elements }) => {
      const header = useMemo((e) => <div>Header</div>, []);
      const footer = useMemo((e) => <div>Footer</div>, []);
      const actionOnClickIndexed = useCallback((e, i) => deleteElement(i), [
        deleteElement
      ]);
      const renderItem = useCallback(
        (e, i) => (
          <List.Item>
            <Text>{e}</Text>{" "}
            <WrapperOnClick
              Component={Button}
              index={i}
              onClickIndexed={actionOnClickIndexed}
            >
              Delete
            </WrapperOnClick>
          </List.Item>
        ),
        [actionOnClickIndexed]
      );
      return (
        <>
          <h2>Version 6: using a wrapper + List</h2>
          <p>
            This kind of work… at least the button seems to be cached, but not
            perfect as it shares all issues of the wrappers. I’m also unsure how to,
            e.g., memoize the whole item, and not just the button.
          </p>
          <List
            header={header}
            footer={footer}
            bordered
            dataSource={elements}
            renderItem={renderItem}
          />
        </>
      );
    });
    
    const Version7 = memo(({ deleteElement, elements }) => {
      const header = useMemo((e) => <div>Header</div>, []);
      const footer = useMemo((e) => <div>Footer</div>, []);
      const renderItem = useMemo(
        () =>
          // if we use memoizeWithArgs from proxy-memoize instead, the preprocessing is much longer
          //  and does not even work.
          memoizeWithArgs((e, i) => (
            <List.Item>
              <Text>{e}</Text>{" "}
              <Button onClick={(e) => deleteElement(i)}>Delete</Button>
            </List.Item>
          )),
        [deleteElement]
      );
      return (
        <>
          <h2>
            Version 7: using no wrapper but memoizeWithArgs from proxy-memoize: it
            does NOT work, wayyy longer than anything else.
          </h2>
          <p>
            I don't know why, but using proxy-memoize gives a much bigger render
            time, and does not even cache the elements.
          </p>
          <List
            header={header}
            footer={footer}
            bordered
            dataSource={elements}
            renderItem={renderItem}
          />
        </>
      );
    });
    
    const Version8 = memo(({ deleteElement, elements }) => {
      const header = useMemo((e) => <div>Header</div>, []);
      const footer = useMemo((e) => <div>Footer</div>, []);
      const renderItem = useMemo(
        () =>
          // if we use memoizeWithArgs from proxy-memoize instead, the preprocessing is much longer
          //  and does not even work.
          memoizeFast((e, i) => (
            <List.Item>
              <Text>{e}</Text>{" "}
              <Button onClick={(e) => deleteElement(i)}>Delete</Button>
            </List.Item>
          )),
        [deleteElement]
      );
      return (
        <>
          <h2>
            Version 8: using no wrapper but memoize from fast-memoize: it does work
          </h2>
          <p></p>
          <List
            header={header}
            footer={footer}
            bordered
            dataSource={elements}
            renderItem={renderItem}
          />
        </>
      );
    });
    
    const Version9 = memo(({ deleteElement, elements }) => {
      const computeElement = useMemo(
        () =>
          memoizeFast((e, i) => (
            <li>
              <Text>{e}</Text>{" "}
              <Button onClick={(e) => deleteElement(i)}>Delete</Button>
            </li>
          )),
        [deleteElement]
      );
      return (
        <>
          <h2>
            Version 9: like version 2, but use fast-memoize on whole element: does
            NOT work
          </h2>
          <p>I don't understand why this fails while Version 8 works.</p>
          <ul>{elements.map(computeElement)}</ul>
        </>
      );
    });
    
    const Version10 = memo(({ deleteElement, elements }) => {
      const del = useMemo(() => memoizeFast((i) => (e) => deleteElement(i)), [
        deleteElement
      ]);
      return (
        <>
          <h2>
            Version 10: like version 2 (+Text), but use fast-memoize only on delete
          </h2>
          <p>
            I don't understand why this fails while Version 8 works (but to be
            honest, I'm not even sure if it fails, since buttons sometimes just
            don't appear at all, while other renders from scratch without saying
            why): to be more precise, it does not involve caching from the library…
            or maybe this kind of cache is not shown by the tools since it is done
            by another external library? But then, why are the item grey in version
            8?
          </p>
          <ul>
            {elements.map((e, i) => (
              <li>
                <Text>{e}</Text> <Button onClick={del(i)}>Delete</Button>
              </li>
            ))}
          </ul>
        </>
      );
    });
    
    const Version11 = memo(({ deleteElement, elements }) => {
      const del = useMemo(() => memoizeFast((i) => (e) => deleteElement(i)), [
        deleteElement
      ]);
      const computeElement = useMemo(
        () =>
          memoizeFast((e, i) => (
            <li>
              <Text>{e}</Text> <Button onClick={del(i)}>Delete</Button>
            </li>
          )),
        [del]
      );
      return (
        <>
          <h2>Version 11: like version 9 + 10, does NOT work</h2>
          <p>Not sure why it fails, even worse than 9 and 10 separately.</p>
          <ul>{elements.map(computeElement)}</ul>
        </>
      );
    });
    
    const Version12 = memo(({ deleteElement, elements }) => {
      const MemoizedList = useMemo(
        () => () => {
          return elements.map((e, i) => (
            <li key={e}>
              <Text>{e}</Text>{" "}
              <Button onClick={(e) => deleteElement(i)}>Delete</Button>
            </li>
          ));
        },
        [elements, deleteElement]
      );
      return (
        <>
          <h2>Version 12: memoize the whole list: not what I want</h2>
          <p>
            Answer proposed in
            https://stackoverflow.com/questions/76446359/react-clean-way-to-define-usecallback-for-functions-taking-arguments-in-loop/76462654#76462654,
            but it fails as if a single element changes, the whole list is redrawn.
          </p>
          <ul>
            <MemoizedList />
          </ul>
        </>
      );
    });
    
    const Version13Aux = memo(({ onClickIndexed, index, e }) => {
      const action = useCallback((e) => onClickIndexed(e, index), [
        index,
        onClickIndexed
      ]);
      return (
        <li>
          <Text>{e}</Text>
          <Button onClick={action}>Delete</Button>
        </li>
      );
    });
    
    const Version13 = memo(({ deleteElement, elements }) => {
      const actionOnClickIndexed = useCallback((e, i) => deleteElement(i), [
        deleteElement
      ]);
      return (
        <>
          <h2>
            Version 13: simple list (not Ant): works but I don’t like the fact that
            we need to create auxiliary elements.
          </h2>
          <p>
            This works, but I don't like this solution because I need to manually
            create a new component, which can make the code more complicated.
          </p>
          <ul>
            {elements.map((e, i) => (
              <Version13Aux index={i} onClickIndexed={actionOnClickIndexed} e={e} />
            ))}
          </ul>
        </>
      );
    });
    
    const Version14Aux = memo(({ onClickIndexed, index, e }) => {
      const action = useCallback((e) => onClickIndexed(e, index), [
        index,
        onClickIndexed
      ]);
      return (
        <List.Item>
          <Text>{e}</Text> <Button onClick={action}>Delete</Button>
        </List.Item>
      );
    });
    
    const Version14 = memo(({ deleteElement, elements }) => {
      const actionOnClickIndexed = useCallback((e, i) => deleteElement(i), [
        deleteElement
      ]);
    
      const header = useMemo((e) => <div>Header</div>, []);
      const footer = useMemo((e) => <div>Footer</div>, []);
      const renderItem = useCallback(
        (e, i) => (
          <Version14Aux index={i} onClickIndexed={actionOnClickIndexed} e={e} />
        ),
        [actionOnClickIndexed]
      );
      return (
        <>
          <h2>Version 14: like version 13, but for Ant lists</h2>
          <p>
            This works, but I don't like this solution so much because I need to
            manually create a new component, which can make the code slightly more
            complicated. But it seems the most efficient solution (better than
            memoize etc), and the code is still not too bloated while avoiding third
            party libraries… So it might be the best solution.
          </p>
          <List
            header={header}
            footer={footer}
            bordered
            dataSource={elements}
            renderItem={renderItem}
          />
        </>
      );
    });
    
    const initialState = ["Egg", "Milk", "Potatoes", "Tomatoes"];
    export default function App() {
      const [elements, setElements] = useState(initialState);
      const restart = useCallback((e) => setElements(initialState), []);
      const deleteElement = useCallback(
        (index) => setElements((elts) => elts.filter((e, i) => i !== index)),
        []
      );
      return (
        <div className="App">
          <h1>Trying to avoid redraw</h1>
          <button onClick={restart}>Restart</button>
          <Version1 elements={elements} deleteElement={deleteElement} />
          <Version2 elements={elements} deleteElement={deleteElement} />
          <Version3 elements={elements} deleteElement={deleteElement} />
          <Version4 elements={elements} deleteElement={deleteElement} />
          <Version5naive elements={elements} deleteElement={deleteElement} />
          <Version5 elements={elements} deleteElement={deleteElement} />
          <Version6 elements={elements} deleteElement={deleteElement} />
          <Version8 elements={elements} deleteElement={deleteElement} />
          <Version9 elements={elements} deleteElement={deleteElement} />
          <Version10 elements={elements} deleteElement={deleteElement} />
          <Version11 elements={elements} deleteElement={deleteElement} />
          <Version12 elements={elements} deleteElement={deleteElement} />
          <Version13 elements={elements} deleteElement={deleteElement} />
          <Version14 elements={elements} deleteElement={deleteElement} />
          {
            // Version 7 is soo long that I need to put it in the end or
            // on the profiler I can’t click on other items that
            // are too close to the scroll bar
            // <Version7 elements={elements} deleteElement={deleteElement} />
          }
        </div>
      );
    }

    回复
    0
  • 取消回复