reactjs React:为在循环中接受参数的函数定义useCallback的干净方法

juud5qan  于 2023-06-22  发布在  React
关注(0)|答案(3)|浏览(97)

问题

在react中,我真的经常需要使用useCallback之类的东西在项目列表中记住函数(通过循环创建),以避免在单个元素更改时重新呈现所有组件,因为引用标识符不匹配...不幸的是,这是令人惊讶的难以到期。例如,考虑以下代码:

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

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

坏的解决方案

为了避免这个问题,我可以想到另一个解决方案:创建一个新的组件MyButton,它接受两个prop 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}> <>
    })
  }
)

为什么我想要更好的方法

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

  • 代码混乱
  • 我需要 Package 来自外部库(如Button)的所有元素,并重写不打算处理这种嵌套的组件。这就破坏了模块化,使代码更加复杂
  • 这是一个很差的组成:如果我想在多个列表中嵌套元素,这将更加肮脏,因为我需要像<MyButton index1={index1} index2={index2} index3={index3} onClick={myFunction}>那样为列表的每一层添加一个新索引,这意味着我需要创建一个更复杂的MyButton版本来检查嵌套层的数量。我不能使用index={[index1, index2, index3]},因为这是一个数组,因此没有稳定的引用。
  • 据我所知,index es的命名没有约定,这意味着在项目之间共享代码或开发库更困难

有没有一个更好的解决方案我错过了吗?考虑到列表是如此无所不在,我不相信没有合适的解决方案,而且我很惊讶地看到很少有关于这方面的文档。

编辑我尝试做:

// 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>

但这仍然不是完美的,特别是我需要一个 Package 器用于不同的嵌套级别,每当我针对一个新属性(onClickonChange,...)时,我需要创建一个新版本,如果我有多个属性(例如,onChange,...),它将无法直接工作。onClickonChange),我以前从未见过这种情况,所以我猜有更好的解决方案。

edit我尝试了各种方法,包括使用fast-memoize,但我仍然不理解所有的结果:有时候,快速备忘录的作品,而有时它失败了…我不知道快速备忘录是否是推荐的解决方案:对于这样一个常见的用例使用第三方工具似乎很奇怪。在这里查看我的测试https://codesandbox.io/embed/magical-dawn-67mgxp?fontsize=14&hidenavigation=1&theme=dark

rsl1atfo

rsl1atfo1#

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

由于我对这个问题不感兴趣,所以我试着对一堆解决方案(14!),这取决于各种选择(无备忘录,使用外部库(快速备忘录vs代理备忘录),使用 Package 器,使用外部组件等...
最好的方法似乎是创建一个新的组件**,包含列表的整个元素**,而不仅仅是最后一个按钮。这使得代码相当干净(即使我需要为列表和项创建两个组件,至少在语义上是有意义的),避免了外部库,并且似乎比我尝试的所有其他方法更有效(至少在我的示例中):

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>
  );
});

我仍然不太喜欢这个解决方案,因为我需要将很多东西从父组件转发到子组件,但它似乎是我能得到的最好的解决方案。
你可以看到我的尝试列表here,我使用了下面的代码。这是侧写师的视角(从技术上讲,所有版本之间在时间方面没有很大的差异(除了使用代理记忆的版本7,我删除了它,因为它比它长了很多,也许是10倍,并且使图形更难阅读),但我希望这种差异在更长的列表中更大,其中项目更复杂(这里我只有一个文本和一个按钮)。请注意,并非所有版本都完全相同(有些版本使用<button>,有些版本使用</Button>,有些版本使用普通列表,有些版本使用Ant设计列表......),因此时间比较只在做相同事情的版本之间有意义。无论如何,我最关心的是看到什么被缓存,什么没有,这在分析器中清晰可见(浅灰色块被缓存):

另一个有趣的事实是,您可能希望在memoizing之前进行基准测试,因为改进可能并不显著,至少对于简单的组件(这里大小为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>
  );
}
wnavrhmk

wnavrhmk2#

1.首先,不建议使用index作为参数或props或key,因为当你删除第一个时,所有的子组件都将重新渲染。
1.而根据你的场景如果想避免重新渲染,我有一些想法,大家可以参考一下,像这样:

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

bqucvtff

bqucvtff3#

为了解决这个问题,您可以使用react-fast-comparelodash.isEqual等库来对useCallback中的依赖进行彻底的比较。这将消除对每个物品的唯一标识符的手动管理的需要。

import { useCallback } from 'react';
import isEqual from 'react-fast-compare';

//我的朋友

const MyComponent = () => {
  const memoizedCallback = useCallback(() => {
    // callback logic...
  }, [dependency1, dependency2, isEqual]);

  // .....
};

https://www.npmjs.com/package/react-fast-compare

相关问题