在 React 中使用基於循環的參數有效地定義 useCallback 函數
P粉550823577
P粉550823577 2024-01-16 14:06:30
0
2
458

問題

在反應中,我確實經常需要使用諸如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}> <>
    })
  }
)

為什麼我想要更好的方法

雖然這確實有效,但由於多種原因,這是非常不實用的:

  • 程式碼混亂
  • 我需要包裝 Button 等外部庫中的所有元素,並重寫原本不打算處理這種嵌套的元件…這會破壞模組化並使程式碼更加複雜
  • 這個組合很糟糕:如果我想將元素嵌套在多個列表中,它會更髒,因為我需要在列表的每一層添加一個新索引,例如<MyButton index1= {index1} index2={index2} index3={index3 onClick={myFunction}>,這意味著我需要完全通用地創建一個更複雜的版本MyButton 來檢查嵌套級別的數量。我無法使用 index={[index1,index2,index3]},因為這是一個數組,因此沒有穩定的參考。
  • 據我所知,indexes 的命名沒有約定,這意味著專案之間共享程式碼或開發庫更加困難

我缺少更好的解決方案嗎?考慮到清單無處不在,我不敢相信對此沒有適當的解決方案,而且我很驚訝地看到這方面的文檔很少。

編輯 我嘗試這樣做:

// 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粉550823577
P粉550823577

全部回覆(2)
P粉001206492
  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

P粉916760429

警告:我不是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>
  );
}
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板