Muter l'état dans un useEffect()

Voir la vidéo
Description Sommaire

Dans ce chapitre nous allons revenir sur le hook useEffect() pour parler d'une erreur qui est souvent faite quand on débute : muter l'état à la racine d'un useEffect.

Cas °1 : Dériver une valeur

La première erreur qui est parfois commise est l'utilisation d'un état pour gérer une valeur dérivée.

const [fullname, setFullname] = useState('');

useEffect(() => {
    setFullName(`${firstname} ${lastname}`)
}, [firstname, lastname])

J'ai pris ici un exemple simple mais il illustre le problème. On utilise un état pour gérer une valeur qui peut en réalité être dérivée depuis une autre valeur. On peut simplement écrire :

const fullname = `${firstname} ${lastname}`

On peut aussi utiliser useMemo() si on souhaite éviter de faire un rendu systématique ou pour créer une valeur qui ne change pas à chaque rendu.

const person = useMemo(() => ({firstname, lastname}))

Cas n°2 : Changer un état au début d'une action

Une autre situation plus commune est le changement d'état au début d'une action. Prenons l'exemple d'un composant qui va charger du contenu depuis une API.

function App () {
    const [data, setData] = useState()
    const [url, setUrl] = useState('')
    const [loading, setLoading] = useState(true)

    useEffect(() => {
        setLoading(true)
        fetchData(url)
            .then(setData)
            .finally(() => setLoading(false))
    }, [url])

    return <>
        <button onClick={() => setUrl('url-a')}>A</button>
        <button onClick={() => setUrl('url-b')}>B</button>
    </>
}

Le problème ici est que lorsque l'on clique sur un bouton on va avoir 2 rendus consécutifs

  • Un premier rendu causé par le changement de url
  • Un second rendu, causé par le changement de loading dans le useEffect()

Pour éviter d'utiliser une mutation dans le useEffect() on peut déplacer le changement d'état dans la fonction branchée à l'écouteur.

function App () {
    const [data, setData] = useState()
    const [url, setUrl] = useState('')
    const [loading, setLoading] = useState(true)

    useEffect(() => {
        fetchData(url)
            .then(setData)
            .finally(() => setLoading(false))
    }, [url])

    const handlerForUrl = (url) => () => {
        setLoading(true)
        setUrl(url)
    }

    return <>
        <button onClick={handlerForUrl('url-a')}>A</button>
        <button onClick={handlerForUrl('url-b')}>B</button>
    </>
}

On évite ainsi les rendus successif lors d'un clic sur un bouton.

Cas n°3 : Changer un état lors d'un changement de props

La solution précédent n'est cependant pas possible si ce qui déclenche le useEffect() provient d'une propriété.

function App ({ url }) {
    const [data, setData] = useState()
    const [loading, setLoading] = useState(true)

    useEffect(() => {
        setLoading(true)
        fetchData(url)
            .then(setData)
            .finally(() => setLoading(false))
    }, [url])

    return <>
        <button onClick={() => setUrl('url-a')}>A</button>
        <button onClick={() => setUrl('url-b')}>B</button>
    </>
}

On ne peut donc pas combiner l'opération qui change l'url et le chargement. Il faut dans cette situation repenser la logique de notre composant.

  • Plutôt que de sauvegarder l'état du chargement dans un booléen, on va plutôt sauvegarder l'URL qui a été chargée.
  • L'état de chargement sera donc dérivé depuis cet état.
function App ({ url }) {
    const [data, setData] = useState()
    const [loadedUrl, setLoadedUrl] = useState('')
    const loading = loadedUrl !== url

    useEffect(() => {
        fetchData(url)
            .then(setData)
            .finally(() => setLoadedUrl(url))
    }, [url])

    return <>
        <button onClick={() => setUrl('url-a')}>A</button>
        <button onClick={() => setUrl('url-b')}>B</button>
    </>
}

Avec ce changement, on se débarasse de la mutation à la racine du useEffect() et on économise le rendu initial.

Publié
Technologies utilisées
Auteur :
Grafikart
Partager