Siirry sisältöön

b

custom-hookit

Kurssin seitsemännen osan tehtävät poikkeavat jossain määrin aiemmista osista. Edellisessä ja tässä luvussa on normaaliin tapaan luvun teoriaan liittyviä tehtäviä.

Tämän ja seuraavan luvun tehtävien lisäksi seitsemäs osa sisältää kertaavan ja soveltavan tehtäväsarjan, jossa laajennetaan osissa 4 ja 5 tehtyä Blog list ‑sovellusta.

Hookit

React tarjoaa yhteensä 15 erilaista valmista hookia, joista ylivoimaisesti eniten käytetyt ovat meillekin jo tutut useState ja useEffect.

Käytimme osassa 5 hookia useImperativeHandle, jonka avulla komponentin sisäinen funktio pystyttiin tarjoamaan näkyville komponentin ulkopuolelle. Osassa 6 taas olivat käytössä useReducer ja useContext kun toteutimme Reduxia muistuttavan tilanhallintaratkaisun.

Muutaman edellisen vuoden aikana moni Reactin apukirjasto on ruvennut tarjoamaan hook-perustaisen rajapinnan. Osassa 6 käytimme React Redux ‑kirjaston hookeja useSelector ja useDispatch välittämään Redux-storen ja dispatch-funktion niitä tarvitseville komponenteille.

Myös edellisessä luvussa käsitellyn React Routerin API perustuu osin hookeihin, joiden avulla päästiin käsiksi routejen parametroituun osaan, sekä navigation-olioon, joka mahdollistaa selaimen osoiterivin manipuloinnin koodista.

Kuten osassa 1 mainittiin, hookit eivät ole mitä tahansa funktiota, vaan niitä on käytettävä tiettyjä sääntöjä noudattaen. Seuraavassa vielä hookien käytön säännöt suoraan Reactin dokumentaatiosta kopioituna:

Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function.

Don’t call Hooks from regular JavaScript functions. Instead, you can:

  • Call Hooks from React function components.
  • Call Hooks from custom Hooks

On olemassa ESLint-sääntö, jonka avulla voidaa varmistaa, että sovellus käyttää hookeja oikein. Create React Appiin valmiiksi asennettu säätö eslint-plugin-react-hooks varoittaa, jos yrität käyttää hookia väärin:

fullstack content

Custom-hookit

React tarjoaa mahdollisuuden myös omien eli custom-hookien määrittelyyn. Custom-hookien pääasiallinen tarkoitus on Reactin dokumentaation mukaan mahdollistaa komponenttien logiikan uusiokäyttö:

Building your own Hooks lets you extract component logic into reusable functions.

Custom-hookit ovat tavallisia JavaScript-funktioita, jotka voivat kutsua mitä tahansa muita hookeja kunhan ne vain toimivat hookien sääntöjen puitteissa. Custom-hookin nimen täytyy alkaa sanalla use.

Teimme osassa 1 laskurin, jonka arvoa voi kasvattaa, vähentää ja nollata. Sovelluksen koodi on seuraava:

import { useState } from 'react'
const App = () => {
  const [counter, setCounter] = useState(0)

  return (
    <div>
      <div>{counter}</div>
      <button onClick={() => setCounter(counter + 1)}>
        plus
      </button>
      <button onClick={() => setCounter(counter - 1)}>
        minus
      </button>      
      <button onClick={() => setCounter(0)}>
        zero
      </button>
    </div>
  )
}

Eriytetään laskurilogiikka custom-hookiksi. Hookin koodi on seuraava:

const useCounter = () => {
  const [value, setValue] = useState(0)

  const increase = () => {
    setValue(value + 1)
  }

  const decrease = () => {
    setValue(value - 1)
  }

  const zero = () => {
    setValue(0)
  }

  return {
    value, 
    increase,
    decrease,
    zero
  }
}

Hook siis käyttää sisäisesti useState-hookia luomaan itselleen tilan. Hook palauttaa olion, joka sisältää kenttinään hookin tilan arvon sekä funktiot hookin tallettaman arvon muuttamiseen.

React-komponentti käyttää hookia seuraavaan tapaan:

const App = () => {
  const counter = useCounter()

  return (
    <div>
      <div>{counter.value}</div>
      <button onClick={counter.increase}>
        plus
      </button>
      <button onClick={counter.decrease}>
        minus
      </button>      
      <button onClick={counter.zero}>
        zero
      </button>
    </div>
  )
}

Näin komponentin App tila ja sen manipulointi on siirretty kokonaisuudessaan hookin useCounter vastuulle.

Samaa hookia voitaisiin uusiokäyttää sovelluksessa, joka laski vasemman ja oikean napin painalluksia:

const App = () => {
  const left = useCounter()
  const right = useCounter()

  return (
    <div>
      {left.value}
      <button onClick={left.increase}>
        left
      </button>
      <button onClick={right.increase}>
        right
      </button>
      {right.value}
    </div>
  )
}

Nyt sovellus luo kaksi erillistä laskuria. Toisen käsittelyfunktioineen se tallentaa muuttujaan left ja toisen muuttujaan right.

Lomakkeiden käsittely on Reactissa jokseenkin vaivalloista. Seuraavassa on sovellus, joka pyytää lomakkeella käyttäjän nimen, syntymäajan ja pituuden:

const App = () => {
  const [name, setName] = useState('')
  const [born, setBorn] = useState('')
  const [height, setHeight] = useState('')

  return (
    <div>
      <form>
        name: 
        <input
          type='text'
          value={name}
          onChange={(event) => setName(event.target.value)} 
        /> 
        <br/> 
        birthdate:
        <input
          type='date'
          value={born}
          onChange={(event) => setBorn(event.target.value)}
        />
        <br /> 
        height:
        <input
          type='number'
          value={height}
          onChange={(event) => setHeight(event.target.value)}
        />
      </form>
      <div>
        {name} {born} {height} 
      </div>
    </div>
  )
}

Jokaista lomakkeen kenttää varten on oma tilansa. Jotta tila pysyy ajan tasalla lomakkeelle syötettyjen tietojen kanssa, on jokaiselle input-elementille rekisteröity sopiva onChange-käsittelijä.

Määritellään custom-hook useField, joka yksinkertaistaa lomakkeen tilan hallintaa:

const useField = (type) => {
  const [value, setValue] = useState('')

  const onChange = (event) => {
    setValue(event.target.value)
  }

  return {
    type,
    value,
    onChange
  }
}

Hook-funktio saa parametrina kentän tyypin. Funktio palauttaa kaikki input-kentän tarvitsemat attribuutit eli tyypin, kentän arvon sekä onChange-tapahtumankäsittelijän.

Hookia voidaan käyttää seuraavasti:

const App = () => {
  const name = useField('text')
  // ...

  return (
    <div>
      <form>
        <input
          type={name.type}
          value={name.value}
          onChange={name.onChange} 
        /> 
        // ...
      </form>
    </div>
  )
}

Spread-attribuutit

Pääsemme itse asiassa helpommalla. Koska oliolla name on nyt täsmälleen ne kentät, jotka input-komponentti odottaa saavansa propseina, voimme välittää propsit hyödyntäen spread-syntaksia seuraavasti:

<input {...name} /> 

Eli kuten Reactin dokumentaation esimerkki kertoo, seuraavat kaksi tapaa välittää propseja komponentille tuottavat saman lopputuloksen:

<Greeting firstName='Arto' lastName='Hellas' />

const person = {
  firstName: 'Arto',
  lastName: 'Hellas'
}

<Greeting {...person} />

Sovellus pelkistyy muotoon:

const App = () => {
  const name = useField('text')
  const born = useField('date')
  const height = useField('number')

  return (
    <div>
      <form>
        name: 
        <input  {...name} /> 
        <br/> 
        birthdate:
        <input {...born} />
        <br /> 
        height:
        <input {...height} />
      </form>
      <div>
        {name.value} {born.value} {height.value}
      </div>
    </div>
  )
}

Lomakkeiden käsittely yksinkertaistuu huomattavasti, kun ikävät tilan synkronoimiseen liittyvät detaljit on kapseloitu custom-hookin vastuulle.

Custom-hookit eivät selvästikään ole pelkkä uusiokäytön väline, vaan ne mahdollistavat myös entistä paremman tavan jakaa koodia pienempiin, modulaarisiin osiin.

Internetistä alkaa löytyä yhä enenevissä määrin valmiita hookeja sekä muuta hookeihin liittyvä hyödyllistä materiaalia. Esim. seuraavia kannattaa vilkaista: