web-dev-qa-db-fra.com

Qu'est-ce que l'ordre d'exécution useEffect et sa logique de nettoyage interne dans les hooks React?

Selon le document React, useEffect déclenchera une logique de nettoyage avant de relancer la partie useEffect.

Si votre effet renvoie une fonction, React l'exécutera quand il sera temps de nettoyer ...

Il n'y a pas de code spécial pour gérer les mises à jour car useEffect les gère par défaut. Il nettoie les effets précédents avant d'appliquer les effets suivants ...

Cependant, lorsque j'utilise requestAnimationFrame et cancelAnimationFrame dans useEffect, j'ai trouvé que cancelAnimationFrame ne pouvait pas arrêter l'animation normalement. Parfois, j'ai trouvé que l'ancienne animation existe toujours, tandis que l'effet suivant apporte une autre animation, ce qui provoque des problèmes de performances de mon application Web (surtout lorsque j'ai besoin de rendre des éléments DOM lourds).

Je ne sais pas si react hook fera des choses supplémentaires avant d'exécuter le code de nettoyage, ce qui fait que ma partie d'annulation d'animation ne fonctionnera pas bien, useEffect hook fera quelque chose comme la fermeture pour verrouiller la variable d'état ?

Qu'est-ce que l'ordre d'exécution useEffect et sa logique de nettoyage interne? Y a-t-il un problème avec le code que j'écris ci-dessous, qui fait que cancelAnimationFrame ne peut pas fonctionner parfaitement?

Merci.

//import React, { useState, useEffect } from "react";

const {useState, useEffect} = React;

//import ReactDOM from "react-dom";

function App() {
  const [startSeconds, setStartSeconds] = useState(Math.random());
  const [progress, setProgress] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setStartSeconds(Math.random());
    }, 1000);

    return () => clearInterval(interval);
  }, []);

  useEffect(
    () => {
      let raf = null;

      const onFrame = () => {
        const currentProgress = startSeconds / 120.0;
        setProgress(Math.random());
        // console.log(currentProgress);
        loopRaf();
        if (currentProgress > 100) {
          stopRaf();
        }
      };

      const loopRaf = () => {
        raf = window.requestAnimationFrame(onFrame);
        // console.log('Assigned Raf ID: ', raf);
      };

      const stopRaf = () => {
        console.log("stopped", raf);
        window.cancelAnimationFrame(raf);
      };

      loopRaf();

      return () => {
        console.log("Cleaned Raf ID: ", raf);
        // console.log('init', raf);
        // setTimeout(() => console.log("500ms later", raf), 500);
        // setTimeout(()=> console.log('5s later', raf), 5000);
        stopRaf();
      };
    },
    [startSeconds]
  );

  let t = [];
  for (let i = 0; i < 1000; i++) {
    t.Push(i);
  }

  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <text>{progress}</text>
      {t.map(e => (
        <span>{progress}</span>
      ))}
    </div>
  );
}

ReactDOM.render(<App />,
document.querySelector("#root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.7.0-alpha.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.7.0-alpha.2/umd/react-dom.production.min.js"></script>
<div id="root"></div>
9
hijiangtao

Mettez ces trois lignes de code dans un composant et vous verrez leur ordre de priorité.

  useEffect(() => {
    console.log('useEffect')
    return () => {
      console.log('useEffect cleanup')
    }
  })

  window.requestAnimationFrame(() => console.log('requestAnimationFrame'))

  useLayoutEffect(() => {
    console.log('useLayoutEffect')
    return () => {
      console.log('useLayoutEffect cleanup')
    }
  })

useLayoutEffect > requestAnimationFrame > useEffect

Le problème que vous rencontrez est dû au fait que loopRaf demande une autre image d'animation avant que la fonction de nettoyage de useEffect ne soit exécutée.

Des tests supplémentaires ont montré que useLayoutEffect est toujours appelé avant requestAnimationFrame et que sa fonction de nettoyage est appelée avant la prochaine exécution, ce qui évite les chevauchements.

Changez useEffect en useLayoutEffect et cela devrait résoudre votre problème.

useEffect et useLayoutEffect sont appelés dans l'ordre dans lequel ils apparaissent dans votre code pour des types similaires, tout comme les appels useState.

Vous pouvez le voir en exécutant les lignes suivantes:

  useEffect(() => {
    console.log('useEffect-1')
  })
  useEffect(() => {
    console.log('useEffect-2')
  })
  useLayoutEffect(() => {
    console.log('useLayoutEffect-1')
  })
  useLayoutEffect(() => {
    console.log('useLayoutEffect-2')
  })
3
Kyle Richardson

Une chose qui n'est pas claire dans les réponses ci-dessus est l'ordre dans lequel les effets s'exécutent lorsque vous avez plusieurs composants dans le mixage. Nous avons effectué un travail qui implique la coordination entre un parent et ses enfants via useContext, donc l'ordre est plus important pour nous. useLayoutEffect et useEffect fonctionnent de différentes manières à cet égard.

useEffect exécute le nettoyage et le nouvel effet avant de passer au composant suivant (profondeur d'abord) et de faire de même.

useLayoutEffect exécute les nettoyages de chaque composant (profondeur d'abord), puis exécute les nouveaux effets de tous les composants (profondeur d'abord).

render parent
render a
render b
layout cleanup a
layout cleanup b
layout cleanup parent
layout effect a
layout effect b
layout effect parent
effect cleanup a
effect a
effect cleanup b
effect b
effect cleanup parent
effect parent
const Test = (props) => {
  const [s, setS] = useState(1)

  console.log(`render ${props.name}`)

  useEffect(() => {
    const name = props.name
    console.log(`effect ${props.name}`)
    return () => console.log(`effect cleanup ${name}`)
  })

  useLayoutEffect(() => {
    const name = props.name
    console.log(`layout effect ${props.name}`)
    return () => console.log(`layout cleanup ${name}`)
  })

  return (
    <>
      <button onClick={() => setS(s+1)}>update {s}</button>
      <Child name="a" />
      <Child name="b" />
    </>
  )
}

const Child = (props) => {
  console.log(`render ${props.name}`)

  useEffect(() => {
    const name = props.name
    console.log(`effect ${props.name}`)
    return () => console.log(`effect cleanup ${name}`)
  })

  useLayoutEffect(() => {
    const name = props.name
    console.log(`layout effect ${props.name}`)
    return () => console.log(`layout cleanup ${name}`)
  })

  return <></>
}
5
Aidan Kane

Il existe deux crochets différents sur lesquels vous devez vous concentrer lorsque vous travaillez avec des crochets et essayez d'implémenter des fonctionnalités de cycle de vie.

Selon les documents:

useEffect s'exécute une fois que react rend votre composant et garantit que votre rappel d'effet ne bloque pas la peinture du navigateur. Cela diffère du comportement des composants de classe où componentDidMount et componentDidUpdate s'exécutent de manière synchrone après le rendu.

et donc l'utilisation de requestAnimationFrame dans ces cycles de vie fonctionne de manière apparente mais a un léger problème avec useEffect. Et donc useEffect doit être utilisé lorsque les modifications que vous devez apporter ne bloquent pas les mises à jour visuelles, comme les appels d'API qui entraînent un changement de DOM après la réception d'une réponse.

useLayoutEffect est un autre hook moins populaire mais extrêmement pratique pour gérer les mises à jour visuelles du DOM. Selon les documents

La signature est identique à useEffect, mais elle se déclenche de manière synchrone après toutes les mutations DOM. Utilisez-le pour lire la disposition du DOM et effectuer un nouveau rendu synchrone. Les mises à jour programmées dans useLayoutEffect seront vidées de manière synchrone, avant que le navigateur ait la possibilité de peindre.

Donc, si votre effet mute le DOM (via une référence de nœud DOM) et que la mutation DOM change l'apparence du nœud DOM entre le moment où il est rendu et votre effet le mute, alors vous ne voulez pas utiliser useEffect. Vous voudrez utiliser useLayoutEffect. Sinon, l'utilisateur pourrait voir un scintillement lorsque vos mutations DOM prennent effet, ce qui est exactement le cas avec requestAnimationFrame

//import React, { useState, useEffect } from "react";

const {useState, useLayoutEffect} = React;

//import ReactDOM from "react-dom";

function App() {
  const [startSeconds, setStartSeconds] = useState("");
  const [progress, setProgress] = useState(0);

  useLayoutEffect(() => {
    setStartSeconds(Math.random());

    const interval = setInterval(() => {
      setStartSeconds(Math.random());
    }, 1000);

    return () => clearInterval(interval);
  }, []);

  useLayoutEffect(
    () => {
      let raf = null;

      const onFrame = () => {
        const currentProgress = startSeconds / 120.0;
        setProgress(Math.random());
        // console.log(currentProgress);
        loopRaf();
        if (currentProgress > 100) {
          stopRaf();
        }
      };

      const loopRaf = () => {
        raf = window.requestAnimationFrame(onFrame);
        // console.log('Assigned Raf ID: ', raf);
      };

      const stopRaf = () => {
        console.log("stopped", raf);
        window.cancelAnimationFrame(raf);
      };

      loopRaf();

      return () => {
        console.log("Cleaned Raf ID: ", raf);
        // console.log('init', raf);
        // setTimeout(() => console.log("500ms later", raf), 500);
        // setTimeout(()=> console.log('5s later', raf), 5000);
        stopRaf();
      };
    },
    [startSeconds]
  );

  let t = [];
  for (let i = 0; i < 1000; i++) {
    t.Push(i);
  }

  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <text>{progress}</text>
      {t.map(e => (
        <span>{progress}</span>
      ))}
    </div>
  );
}

ReactDOM.render(<App />,
document.querySelector("#root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.7.0-alpha.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.7.0-alpha.2/umd/react-dom.production.min.js"></script>
<div id="root"></div>
3
Shubham Khatri