web-dev-qa-db-fra.com

React Hooks: accès à l'état entre les fonctions sans changer les références de fonction du gestionnaire d'événements

Dans un composant basé sur la classe React je fais quelque chose comme ceci:

class SomeComponent extends React.Component{
    onChange(ev){
        this.setState({text: ev.currentValue.text});
    }
    transformText(){
        return this.state.text.toUpperCase();
    }
    render(){
        return (
            <input type="text" onChange={this.onChange} value={this.transformText()} />
        );
    }
}

Ceci est un peu un exemple artificiel pour simplifier mon point. Ce que je veux essentiellement faire, c'est maintenir une référence constante à la fonction onChange. Dans l'exemple ci-dessus, lorsque React restitue mon composant, il ne restituera pas l'entrée si la valeur d'entrée n'a pas changé.

Choses importantes à noter ici:

  1. this.onChange est une référence constante à la même fonction.
  2. this.onChange doit être en mesure d'accéder à la définition de l'état (dans ce cas, this.setState)

Maintenant, si je devais réécrire ce composant à l'aide de crochets:

function onChange(setText, ev) {
    setText(ev.currentValue.text);
};

function transformText(text) {
    return text.toUpperCase();
};

function SomeComponent(props) {
    const [text, setText] = useState('');

    return (
        <input type="text" onChange={onChange} value={transformText()} />
    );
}

Le problème est maintenant que je dois passer les méthodes text à transformText et setText à onChange respectivement. Les solutions possibles auxquelles je peux penser sont:

  1. Définissez les fonctions à l'intérieur de la fonction de composant et utilisez des fermetures pour transmettre la valeur.
  2. Dans la fonction de composant, liez la valeur aux méthodes, puis utilisez les méthodes liées.

Faire l'une ou l'autre de ces modifications modifiera la référence constante aux fonctions que je dois conserver afin de ne pas rendre le composant input. Comment dois-je procéder avec des crochets? Est-ce même possible?

Veuillez noter qu'il s'agit d'un exemple très simplifié et artificiel. Mon cas d'utilisation réel est assez complexe et je ne veux absolument pas restituer inutilement les composants.

Edit: Ce n'est pas un doublon de Que fait useCallback dans React? parce que j'essaie de comprendre comment obtenir un effet similaire à ce qui était fait à la manière des composants de classe, et tout en useCallback fournit une façon de le faire, ce n'est pas idéal pour des problèmes de maintenabilité.

11
asleepysamurai

Définissez les fonctions à l'intérieur de la fonction de composant et utilisez des fermetures pour transmettre la valeur. Ensuite, ce que vous recherchez est useCallback pour éviter des rendus inutiles. (pour cet exemple, ce n'est pas très utile)

function SomeComponent(props) {
  const [text, setText] = useState('');

  const onChange = useCallback((ev)  => {
    setText(ev.target.value);
  }, []);

  function transformText(text) {
    return text.toUpperCase();
  };

  return (
    <input type="text" onChange={onChange} value={transformText(text)} />
  );
}

En savoir plus ici

7
Mohamed Ramrami

C'est là que vous pouvez construire votre propre crochet (Dan Abramov a exhorté à ne pas utiliser le terme "crochets personnalisés" car cela rend la création de votre propre crochet plus difficile/plus avancée qu'elle ne l'est, qui est juste un copier/coller votre logique) extraire la logique de transformation du texte

"Coupez" simplement le code commenté ci-dessous de réponse de Mohamed .

function SomeComponent(props) {
  // const [text, setText] = React.useState("");

  // const onChange = ev => {
  //   setText(ev.target.value);
  // };

  // function transformText(text) {
  //   return text.toUpperCase();
  // }

  const { onChange, text } = useTransformedText();

  return (
    <input type="text" onChange={React.useCallback(onChange)} value={text} />
  );
}

Et collez-le dans une nouvelle fonction (préfixe avec "use *" par convention). Nommez l'état et le rappel à renvoyer (sous forme d'objet ou de tableau selon votre situation)

function useTransformedText(textTransformer = text => text.toUpperCase()) {
  const [text, setText] = React.useState("");

  const onChange = ev => {
    setText(ev.target.value);
  };

  return { onChange, text: textTransformer(text) };
}

Comme la logique de transformation peut être transmise (mais utilise UpperCase par défaut), vous pouvez utiliser la logique partagée à l'aide de votre propre hook.

function UpperCaseInput(props) {
  const { onChange, text } = useTransformedText();

  return (
    <input type="text" onChange={React.useCallback(onChange)} value={text} />
  );
}

function LowerCaseInput(props) {
  const { onChange, text } = useTransformedText(text => text.toLowerCase());

  return (
    <input type="text" onChange={React.useCallback(onChange)} value={text} />
  );
}

Vous pouvez utiliser les composants ci-dessus comme ci-dessous.

function App() {
  return (
    <div className="App">
      To Upper case: <UpperCaseInput />
      <br />
      To Lower case: <LowerCaseInput />
    </div>
  );
}

Le résultat ressemblerait à ceci.

result demo

Vous pouvez exécuter le code de travail ici.
Edit so.answer.54597527

7
dance2die

Je sais que c'est une mauvaise forme de répondre à ma propre question, mais sur la base de cette réponse , et cette réponse , il semble que je vais devoir créer mon propre crochet personnalisé pour ce faire .

J'ai essentiellement construit un hook qui lie une fonction de rappel avec les arguments donnés et la mémorise. Il ne lie à nouveau le rappel que si les arguments donnés changent.

Si quelqu'un trouve un besoin pour un crochet similaire, je l'ai ouvert en tant que projet séparé. Il est disponible sur Github et NPM .

0
asleepysamurai

Le cas n'est pas spécifique aux hooks, il en serait de même pour le composant classe et setState dans le cas où transformText et onChange devraient être extraits d'une classe. Il n'est pas nécessaire d'extraire des fonctions unifilaires, on peut donc supposer que les fonctions réelles sont suffisamment complexes pour justifier l'extraction.

C'est parfaitement bien d'avoir une fonction de transformation qui accepte une valeur comme argument.

Quant au gestionnaire d'événements, il doit avoir une référence à setState, cela limite les façons dont il peut être utilisé.

Une recette courante consiste à utiliser la fonction de mise à jour de l'état. S'il doit accepter une valeur supplémentaire (par exemple, une valeur d'événement), il doit s'agir d'une fonction d'ordre supérieur.

const transformText = text => text.toUpperCase();

const onChange = val => _prevState => ({ text: val });

function SomeComponent(props) {
    const [text, setText] = useState('');

    return (
        <input type="text" onChange={e => setText(onChange(e.currentValue.text)} value={transformText(text)} />
    );
}

Cette recette ne semble pas utile dans ce cas car l'original onChange ne fait pas grand-chose. Cela signifie également que l'extraction n'était pas justifiée.

Une manière spécifique aux hooks est que setText peut être passé comme rappel, contrairement à this.setState. Donc onChange peut être une fonction d'ordre supérieur:

const transformText = text => text.toUpperCase();

const onChange = setState => e => setState({ text: e.currentValue.text });

function SomeComponent(props) {
    const [text, setText] = useState('');

    return (
        <input type="text" onChange={onChange(setText)} value={transformText(text)} />
    );
}

Si l'intention est de réduire le rendu des enfants provoqué par des changements dans la prop onChange, onChange doit être mémorisé avec useCallback ou useMemo. Cela est possible car la fonction de définition de useState ne change pas entre les mises à jour des composants:

...
function SomeComponent(props) {
    const [text, setText] = useState('');
    const memoizedOnChange = useMemo(() => onChange(setText), []);

    return (
        <input type="text" onChange={memoizedOnChange} value={transformText(text)} />
    );
}

La même chose peut être obtenue en n'extrayant pas onChange et en utilisant useCallback:

...
function SomeComponent(props) {
    const [text, setText] = useState('');
    const onChange = e => setText({ text: e.currentValue.text });
    const memoizedOnChange = useCallback(onChange, []);

    return (
        <input type="text" onChange={memoizedOnChange} value={transformText(text)} />
    );
}
0
Estus Flask