Définissez efficacement les fonctions useCallback dans React avec des paramètres basés sur des boucles
P粉550823577
P粉550823577 2024-01-16 14:06:30
0
2
449

Question

Dans React, j'ai souvent besoin d'utiliser quelque chose comme useCallback pour mémoriser une fonction dans une liste d'éléments (créée via une boucle) afin d'éviter de restituer tous les composants si un seul élément change en raison d'un identifiant de référence incompatible... Malheureusement, il est étonnamment difficile d'expirer. Par exemple, considérons le code suivant :

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

Button se trouvent les composants externes fournis par Ant Design, etc. Cette référence de fonction sera alors différente à chaque rendu car elle est en ligne, forçant ainsi un nouveau rendu.

Une (mauvaise) solution

Pour éviter ce problème, je peux penser à une autre solution : créer un nouveau composant 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}> <>
    })
  }
)

Pourquoi je veux une meilleure façon

Bien que cela fonctionne, cela est très peu pratique pour plusieurs raisons :

  • Le code prête à confusion
  • Je dois envelopper Button tous les éléments dans des bibliothèques externes telles que
  • et réécrire les composants qui ne sont pas initialement destinés à gérer cette imbrication... Cela casse la modularité et rend le code plus complexe
  • <MyButton index1={index1} index2={index2} index3={index3 onClick={myFunction}>,这意味着我需要完全通用地创建一个更复杂的版本 MyButton 来检查嵌套级别的数量。我无法使用 index={[index1,index2,index3]}Cette combinaison est mauvaise : si je voulais imbriquer les éléments dans plusieurs listes, ce serait encore plus sale car il me faudrait ajouter un nouvel index à chaque niveau de la liste, comme
  • puisque c'est un tableau, il n'y a pas de Stable les références.
  • indexPour autant que je sache, il n'y a pas de convention de dénomination pour les
  • , ce qui signifie qu'il est plus difficile de partager du code ou de développer des bibliothèques entre projets

Est-ce qu'il me manque une meilleure solution ? Étant donné que les listes sont partout, je ne peux pas croire qu'il n'y ait pas de solution appropriée à ce problème, et je suis surpris de voir le peu de documentation disponible à ce sujet.

Modifier

J'essaye ça :

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

et utilisez-le comme ceci : onClickonChange,...),如果我有它就无法直接工作多个属性(例如 onClickonChange
const myactionIndexed = useCallback((e, i) => dispatch(removeSolverConstraint({id: i})), []);
return <WrapperOnClick index={i} Component={Button} onClick={myactionIndexed} danger><CloseCircleOutlined /></WrapperOnClick>
Mais ce n'est toujours pas parfait, j'ai notamment besoin d'un wrapper pour différents niveaux d'imbrication, et chaque fois que je cible une nouvelle propriété, je dois créer une nouvelle version (

), je n'ai jamais vu cela auparavant, donc je veux une meilleure solution .

Modifier

J'ai essayé diverses idées, notamment l'utilisation de fast-memoize, mais je ne comprends toujours pas tous les résultats : parfois, fast-memoize fonctionne, parfois cela échoue... et je ne sais pas si fast-memoize est une solution recommandée : Il semble étrange d’utiliser un outil tiers pour un cas d’utilisation aussi courant. Découvrez mes tests ici https://codesandbox.io/embed/magical-dawn-67mgxp?fontsize=14&hidenavigation=1&theme=dark🎜
P粉550823577
P粉550823577

répondre à tous(2)
P粉001206492
  1. Tout d'abord, il n'est pas recommandé d'utiliser l'index comme paramètre, accessoire ou clé, car lorsque vous supprimez le premier, tous les composants enfants seront restitués.
  2. Et en fonction de votre scène si vous voulez éviter de refaire le rendu, j'ai quelques idées auxquelles vous pouvez vous référer, comme ceci :
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>
  );
}

Testez ici https://codesandbox.io /s/sharp-wind-rd48q4?file=/src/App.js

P粉916760429

Attention : je ne suis pas un expert de React (d'où ma question !), alors merci de commenter ci-dessous et/ou d'ajouter un +1 si vous pensez que cette solution est la manière canonique de le faire dans React (ou -1 si ce n'est pas le cas) ^^ ). Je suis également curieux de savoir pourquoi certaines autres solutions échouent (par exemple, basées sur la mémorisation proxy (ce qui prend en fait 10 fois plus de temps que l'absence de mise en cache et ne met pas du tout en cache) ou la mémorisation rapide (qui ne se met pas toujours en cache, selon la façon dont Je l'utilise )), donc si vous savez, je suis intéressé de savoir)

Comme ce problème ne m'intéresse pas beaucoup, j'ai essayé de comparer un tas de solutions (14 !) avec diverses options (pas de mémoire, utilisation de bibliothèques externes (mémoire rapide vs mémoire proxy), utilisation de wrappers), utilisation de composants externes, etc...

La meilleure façon semble être de créer un nouveau composant contenant l'intégralité de l'élément de la liste, pas seulement le dernier bouton. Cela permet d'obtenir un code assez propre (même si je dois créer deux composants pour la liste et l'élément, au moins cela a du sens sémantiquement), évite les bibliothèques externes et semble être plus efficace que tout ce que j'ai essayé (au moins à mon avis (par exemple) :

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

Je n'aime toujours pas vraiment cette solution car j'ai besoin de transférer beaucoup de contenu du composant parent au composant enfant, mais cela semble être la meilleure solution que je puisse obtenir...

Vous pouvez voir une liste de mes tentatives ici, j'ai utilisé le code ci-dessous. Voici la vue du profileur (techniquement, la différence de temps entre toutes les versions n'est pas si grande (sauf la version 7 qui utilise proxy-memoize, je l'ai supprimée car elle était plus longue, peut-être 10x, et était en cours de création. Les graphiques sont plus difficiles à lire) , mais je m'attends à ce que cette différence soit plus grande sur les listes plus longues, où les éléments sont plus complexes à dessiner (ici je n'ai qu'un seul texte et un seul bouton). Notez que toutes les versions ne sont pas exactement les mêmes (certaines utilisent ,一些 , certaines listes normales, certaines listes conçues par Ant...), donc les comparaisons temporelles n'ont de sens qu'entre les versions qui effectuent la même opération. Quoi qu'il en soit, ma principale préoccupation est de voir ce qui est mis en cache et ce qui ne l'est pas, ce qui est clairement visible dans le profileur (les blocs gris clair sont mis en cache) :

Un autre fait intéressant est que vous souhaiterez peut-être faire un benchmark avant de mémoriser, car l'amélioration peut ne pas être significative, du moins pour les composants simples (taille 5 ici, juste un texte et un bouton).

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>
  );
}
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal