Tentukan fungsi useCallback dengan cekap dalam React dengan parameter berasaskan gelung
P粉550823577
P粉550823577 2024-01-16 14:06:30
0
2
471

Soalan

Dalam React, saya selalunya perlu menggunakan sesuatu seperti useCallback untuk mengingati fungsi dalam senarai item (dicipta melalui gelung) untuk mengelakkan memaparkan semula semua komponen jika satu elemen berubah disebabkan pengecam rujukan yang tidak sepadan... Malangnya, ini amat sukar untuk tamat tempoh. Sebagai contoh, pertimbangkan kod berikut:

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

di mana Button adalah komponen luaran yang disediakan oleh reka bentuk semut dll. Rujukan fungsi ini kemudiannya akan berbeza pada setiap pemaparan kerana ia adalah sebaris, sekali gus memaksa pemaparan semula.

Penyelesaian (buruk)

Untuk mengelakkan masalah ini, saya boleh memikirkan penyelesaian lain: buat komponen baharu 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}> <>
    })
  }
)

Kenapa saya mahukan cara yang lebih baik

Walaupun ini berfungsi, ia sangat tidak praktikal kerana beberapa sebab:

  • Kod mengelirukan
  • Saya perlu membungkus Button semua elemen dalam perpustakaan luaran seperti
  • dan menulis semula komponen yang pada asalnya tidak bertujuan untuk mengendalikan sarang ini... Ini memecahkan modulariti dan menjadikan kod lebih kompleks
  • <MyButton index1={index1} index2={index2} index3={index3 onClick={myFunction}>,这意味着我需要完全通用地创建一个更复杂的版本 MyButton 来检查嵌套级别的数量。我无法使用 index={[index1,index2,index3]}Gabungan ini adalah buruk: jika saya ingin menyarangkan elemen dalam berbilang senarai, ia akan menjadi lebih kotor kerana saya perlu menambah indeks baharu pada setiap peringkat senarai, seperti
  • kerana ini adalah tatasusunan, tidak ada Stabil rujukan.
  • indexSetahu saya, tiada konvensyen penamaan untuk
  • es, yang bermaksud lebih sukar untuk berkongsi kod atau membangunkan perpustakaan antara projek

Adakah saya kehilangan penyelesaian yang lebih baik? Memandangkan senarai ada di mana-mana, saya tidak percaya tidak ada penyelesaian yang sesuai untuk ini, dan saya terkejut melihat betapa sedikit dokumentasi yang ada mengenai perkara ini.

Edit

Saya cuba ini:

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

dan gunakannya seperti ini: onClickonChange,...),如果我有它就无法直接工作多个属性(例如 onClickonChange
const myactionIndexed = useCallback((e, i) => dispatch(removeSolverConstraint({id: i})), []);
return <WrapperOnClick index={i} Component={Button} onClick={myactionIndexed} danger><CloseCircleOutlined /></WrapperOnClick>
Tetapi ia masih tidak sempurna, khususnya saya memerlukan pembungkus untuk tahap bersarang yang berbeza, dan setiap kali saya menyasarkan harta baharu, saya perlu mencipta versi baharu (

), saya tidak pernah melihatnya sebelum ini, jadi saya Mahukan penyelesaian yang lebih baik .

Edit

Saya mencuba pelbagai idea, termasuk menggunakan fast-memoize, tetapi saya masih tidak memahami semua keputusan: kadangkala, fast-memoize berfungsi, kadangkala ia gagal... dan saya tidak tahu sama ada fast-memoize ialah penyelesaian yang disyorkan: Nampaknya pelik menggunakan alat pihak ketiga untuk kes penggunaan biasa seperti itu. Lihat ujian saya di sini https://codesandbox.io/embed/magical-dawn-67mgxp?fontsize=14&hidenavigation=1&theme=dark🎜
P粉550823577
P粉550823577

membalas semua(2)
P粉001206492
  1. Pertama sekali, tidak disyorkan untuk menggunakan indeks sebagai parameter atau prop atau kunci, kerana apabila anda memadamkan yang pertama, semua komponen anak akan dipaparkan semula.
  2. Dan bergantung pada adegan anda jika anda ingin mengelakkan render semula, saya mempunyai beberapa idea yang boleh anda rujuk, seperti ini:
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>
  );
}

Uji di sini https://codesandbox.io /s/sharp-wind-rd48q4?file=/src/App.js

P粉916760429

Amaran: Saya bukan pakar React (oleh itu soalan saya!), jadi sila komen di bawah dan/atau tambahkan +1 jika anda fikir penyelesaian ini adalah cara kanonik untuk melakukannya dalam React (atau -1 jika tidak ^^ ). Saya juga ingin tahu mengapa beberapa penyelesaian lain gagal (cth. berdasarkan proksi-memoize (yang sebenarnya mengambil masa 10x lebih lama daripada tiada caching, dan tidak cache sama sekali) atau fast-memoize (yang tidak selalu cache, bergantung pada cara Saya menggunakannya )), jadi jika anda tahu saya berminat untuk mengetahuinya)

Memandangkan saya kurang berminat dalam masalah ini, saya cuba menanda aras sekumpulan penyelesaian (14!) terhadap pelbagai pilihan (tiada memori, menggunakan perpustakaan luaran (memori cepat vs. memori proksi), menggunakan pembungkus), menggunakan Komponen luaran dsb. .

Cara terbaik nampaknya ialah mencipta komponen baharu mengandungi keseluruhan elemen senarai, bukan hanya butang terakhir. Ini membolehkan kod yang cukup bersih (walaupun saya perlu mencipta dua komponen untuk senarai dan item, sekurang-kurangnya ia masuk akal dari segi semantik), mengelakkan perpustakaan luaran, dan nampaknya lebih cekap daripada semua yang saya cuba (sekurang-kurangnya pada pendapat saya (contohnya):

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

Saya masih tidak begitu menyukai penyelesaian ini kerana saya perlu memajukan banyak kandungan daripada komponen induk kepada komponen anak, tetapi ini nampaknya penyelesaian terbaik yang saya boleh dapatkan...

Anda boleh melihat senarai percubaan saya di sini, saya menggunakan kod di bawah. Berikut ialah pandangan dari profiler (secara teknikalnya perbezaan masa antara semua versi tidak begitu besar (kecuali versi 7 yang menggunakan proxy-memoize, saya mengeluarkannya kerana ia lebih panjang, mungkin 10x, dan sedang dibuat Carta lebih sukar dibaca) , tetapi saya menjangkakan perbezaan ini lebih besar pada senarai yang lebih panjang, di mana item lebih kompleks untuk dilukis (di sini saya hanya mempunyai satu teks dan satu butang). Ambil perhatian bahawa semua versi tidak betul-betul sama (sesetengahnya menggunakan ,一些 , beberapa senarai biasa, beberapa senarai rekaan Ant...), jadi perbandingan masa hanya bermakna antara versi yang menjalankan operasi yang sama. Bagaimanapun, kebimbangan utama saya adalah untuk melihat apa yang dicache dan apa yang tidak dicache, yang jelas kelihatan dalam profiler (blok kelabu muda dicache):

Satu lagi fakta menarik ialah anda mungkin ingin menanda aras sebelum menghafal, kerana peningkatan mungkin tidak ketara, sekurang-kurangnya untuk komponen mudah (saiz 5 di sini, hanya satu teks dan satu butang).

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>
  );
}
Muat turun terkini
Lagi>
kesan web
Kod sumber laman web
Bahan laman web
Templat hujung hadapan