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>
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
enuseLayoutEffect
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')
})
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 <></>
}
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
etcomponentDidUpdate
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>