Přehodnocení modelu součásti pomocí háčků

Pokud jste fanouškem Reactu, možná jste již slyšeli, že vydání s Hooks (v16.8) je tady.

Už pár týdnů hraju s alfa verzí a moc se mi líbí. Adopce však nebyla jen duhami a jednorožci.

Učení useState a useReducer bylo docela jednoduché a zlepšilo to, jak zvládám stav.

Psal jsem o useState v dřívějším příspěvku. Zde je krátká verze:

function Counter() {
  /*
    create a new state pair with useState,
    you can specify the initial value
    as an argument
  */
  const [count, setCount] = useState(0)

  /*
    create a function to increase this count
    you have access to the current count as it
    is a local variable.

    Calling setCount will trigger a re-render
    just like setState would.
  */
  function increase() {
    setCount(count + 1)
  }

  return (
    <div>
      {count}
      <button onClick={increase}>Increase</button>
    </div>
  )
}

Nicméně jsem opravdu bojoval s useEffect háček.

Nežádoucí účinky mohou znamenat cokoli od aktualizace názvu dokumentu až po zadání požadavku API. Cokoli, co se stane mimo váš strom vykreslování React, je pro komponentu vedlejší efekt.

U tříd byste to obvykle dělali v componentDidMount . S háčky to vypadá takto:

import React, { useState, useEffect } from 'react'

// username is passed in props
render(<UserProfile username="siddharthkp" />)

function UserProfile(props) {
  // create a new state pair with empty object as default
  const [user, setUser] = useState({})

  // create a pair for loading state
  const [loading, setLoading] = useState(false)

  // Similar to componentDidMount
  useEffect(function() {
    // set loading to true at start
    setLoading(true)

    // fetch the user's details
    // username is passed in props
    fetch('/get-user?username=' + props.username)
      .then(response => response.json())
      .then(user => {
        setUser(user) // set user in state
        setLoading(false) // set loading to false
      })
  })

  if (loading) return <div>Fetching user... </div>
  else return <div>Hi {user.name}</div>
}

Tohle mi přijde povědomé. Vypadá to jako componentDidMount v jiném obleku.

No, není to stejné. Výše uvedený kód obsahuje chybu!

Podívejte se na tento náhled, je v nekonečné smyčce načítání uživatele a jeho opětovného vykreslování (a nejen proto, že je to gif!)

componentDidMount se volá po namontování komponenty. Vystřelí jen jednou.

Na druhou stranu efekt uvnitř useEffect se ve výchozím nastavení použije na každé vykreslení.

Toto je jemný posun v mentálním modelu, musíme změnit způsob, jakým přemýšlíme o životním cyklu komponenty – namísto připojování a aktualizace musíme myslet z hlediska vykreslování a efektů

useEffect nám umožňuje předat volitelný argument - pole dependencies který informuje React, kdy by měl být účinek znovu aplikován. Pokud se žádná ze závislostí nezmění, efekt se znovu neuplatní.

useEffect(function effect() {}, [dependencies])

Některým lidem to vadí – zdá se, že něco, co bylo jednoduché, je nyní složité a bez užitku.

Výhoda useEffect spočívá v tom, že nahrazuje tři různé metody API (componentDidMount , componentDidUpdate a componentWillUnmount ) a proto vás nutí přemýšlet o všech těchto scénářích od začátku – nejprve vykreslení, aktualizaci nebo znovu vykreslení a odpojení.

Ve výše uvedené komponentě by komponenta měla znovu načíst podrobnosti o uživateli, když chceme zobrazit profil jiného uživatele, tj. když props.username změny.

S komponentou třídy byste to řešili pomocí componentDidUpdate nebo getDerivedStateFromProps . To obvykle přichází jako následná myšlenka a do té doby komponenta zobrazuje zastaralá data.

S useEffect , jste nuceni o těchto případech použití přemýšlet brzy. Můžeme předat props.username jako další argument k useEffect .

useEffect(
  function() {
    setLoading(true) // set loading to true

    // fetch the user's details
    fetch('/get-user?username=' + props.username)
      .then(response => response.json())
      .then(user => {
        setUser(user) // set user in state
        setLoading(false) // set loading to false
      })
  },
  [props.username]
)

React nyní bude sledovat props.username a znovu použít efekt, když se změní.

Promluvme si o jiném druhu vedlejšího efektu:posluchačích událostí.

Snažil jsem se vytvořit nástroj, který vám ukáže, které tlačítko klávesnice je stisknuto. Přidání posluchače na window poslouchat události klávesnice je vedlejší efekt.

Krok 1:Aktivní přidání posluchače událostí

function KeyDebugger(props) {
  const [key, setKey] = useState(null)

  function handleKeyDown(event) {
    setKey(event.key) // set key in state
  }

  useEffect(function() {
    // attach event listener
    window.addEventListener('keydown', handleKeyDown)
  })

  return <div>Last key hit was: {key}</div>
}

Toto vypadá podobně jako předchozí příklad.

Tento efekt bude aplikován na každé vykreslení a skončíme s více posluchači událostí, které se spustí při stejné události. To může vést k neočekávanému chování a nakonec k úniku paměti!

Krok 2:Fáze čištění

useEffect nám poskytuje způsob, jak vyčistit naše posluchače.

Pokud vrátíme funkci z efektu, React ji před opětovným použitím efektu spustí.

function KeyDebugger(props) {
  const [key, setKey] = useState(null)

  function handleKeyDown(event) {
    setKey(event.key)
  }

  useEffect(function() {
    window.addEventListener('keydown', handleKeyDown)

    return function cleanup() {
      // remove the event listener we had attached
      window.removeEventListener('keydown', handleKeyDown)
    }
  })

  return <div>Last key hit was: {key}</div>
}
Poznámka:Kromě spuštění před opětovným použitím efektu je funkce vyčištění také volána, když se komponenta odpojí.

Mnohem lepší. Můžeme provést ještě jednu optimalizaci.

Krok 3:Přidejte závislosti pro opětovné použití efektu

Pamatujte:Pokud nepředáme závislosti, spustí se při každém vykreslení.

V tomto případě potřebujeme efekt použít pouze jednou, tj. jednou připojit posluchač události k oknu.

Pokud se nezmění samotný posluchač, samozřejmě! Měli bychom přidat posluchače handleKeyDown jako jediná závislost zde.

function KeyDebugger(props) {
  const [key, setKey] = useState(null)

  function handleKeyDown(event) {
    setKey(event.key)
  }

  useEffect(
    function() {
      window.addEventListener('keydown', handleKeyDown)

      return function cleanup() {
        window.removeEventListener('keydown', handleKeyDown)
      }
    },
    [handleKeyDown]
  )

  return <div>Last key hit was: {key}</div>
}

dependencies jsou mocným tipem.

  • žádné závislosti:efekt se použije na každé vykreslení
  • [] :použít pouze při prvním vykreslení
  • [props.username] :použít při změně proměnné

Tento efekt můžeme dokonce abstrahovat do vlastního háku se zapečeným vyčištěním. Díky tomu se naše součástka o jednu věc méně stará.

function KeyDebugger(props) {
  const [key, setKey] = useState(null)

  function handleKeyDown(event) {
    setKey(event.key)
  }

  useEventListener('keydown', handleKeyDown)

  return <div>Last key hit was: {key}</div>
}

// re-usable event listener hook with cleanup
function useEventListener(eventName, callback) {
  useEffect(function() {
    window.addEventListener(eventName, callback)

    return function cleanup() {
      window.removeEventListener(eventName, callback)
    }
  }, [])
}
Poznámka:useEventListener jak je definováno výše, funguje pro náš příklad, ale není úplnou implementací. Pokud jste zvědaví, jak by vypadala robustní verze, podívejte se na toto repo.

Přidejme k našemu KeyDebugger ještě jednu funkci . Po sekundě by měla klávesa zmizet, dokud nestisknete jinou klávesu.

To je jen setTimeout , mělo by to být snadné, že?

V handleKeyDown , můžeme klíč deaktivovat po sekundové prodlevě. A jako odpovědní vývojáři také vymažeme časový limit ve funkci čištění.

function KeyDebugger(props) {
  const [key, setKey] = useState(null)
  let timeout

  function handleKeyDown(event) {
    setKey(event.key)

    timeout = setTimeout(function() {
      setKey(null) // reset key
    }, 1000)
  }

  useEffect(function() {
    window.addEventListener('keydown', handleKeyDown)

    return function cleanup() {
      window.removeEventListener('keydown', handleKeyDown)
      clearTimeout(timeout) // additional cleanup task
    }
  }, [])

  return <div>Last key hit was: {key}</div>
}

Tento kód se stal o něco složitějším než dříve, a to díky dvěma vedlejším účinkům probíhajícím ve stejném efektu - setTimeout vnořeno do keydown posluchač. To ztěžuje sledování změn.

Protože jsou tyto dva efekty vnořené, nemohli jsme také využít výhod našeho vlastního háku. Jedním ze způsobů, jak tento kód zjednodušit, je rozdělit je do příslušných háčků.

Vedlejší poznámka:Ve výše uvedeném kódu je velmi jemná chyba, kterou je obtížné objevit – protože časový limit není vymazán, když key změny budou nadále volána stará zpětná volání, což může vést k chybám.
function KeyDebugger(props) {
  const [key, setKey] = useState(null)

  function handleKeyDown(event) {
    setKey(event.key)
  }

  // keyboard event effect
  useEventListener('keydown', handleKeyDown)

  // timeout effect
  useEffect(
    function() {
      let timeout = setTimeout(function() {
        setKey(null)
      }, 1000)

      return function cleanup() {
        clearTimeout(timeout)
      }
    },
    [key]
  )

  return <div>Last key hit was: {key}</div>
}

Vytvořením dvou různých efektů jsme schopni udržet logiku oddělenou (snáze sledovatelnou) a definovat různé závislosti pro každý efekt. Pokud chceme, můžeme efekt časového limitu extrahovat také do vlastního háku – useTimeout.

Vedlejší poznámka:Protože tato komponenta spouští čištění na každém key změnit, neobsahuje chybu vedlejší poznámky z dříve.

Vím, že to zpočátku zní obtížně, ale slibuji, že s trochou cviku to bude snadné.

Doufám, že to bylo užitečné na vaší cestě.

Sid

P.S. Pracuji na kurzu React Hooks – Naučte se React Hooks stavbou hry. Opravdu věřím, že to bude úžasné.

Navštivte Reag.games a podívejte se na ukázku kurzu a pošlete svůj e-mail, abyste získali slevu, až bude spuštěn (15. března).