b

custom-hookit

Kurssin seitsemännen osan tehtävät poikkeavat jossain määrin. 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ä Bloglist-sovellusta.

Hookit

React tarjoaa yhteensä 10 erilaista valmista hookia, näistä 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.

Viimeisen 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. Reduxin hook-perustainen api onkin huomattavasti helpompi käyttää kuin vanhempi, mutta edelleen käytössä oleva connect-api.

Myös edellisessä luvussa käsitellyn react-routerin api perustuu osin hookeihin, joiden avulla päästiin käsiksi routejen parametroituun osaan, sekä history-olioon, joka mahdollistaa selaimen osoiterivin manipuloinnin koodista.

Kuten osassa 1 mainittiin, hookit eivät ole mitä tahansa funktiota, 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 heti jos yrität käyttää hookia väärin:

60ea

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 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 React, { useState } from 'react'
const App = (props) => {
  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 seuraavassa

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 = (props) => {
  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 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 synkroonissa 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ää seuraavalla tavalla:

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 itseasiassa helpommalla. Koska oliolla name on nyt täsmälleen ne kentät, mitä input-komponentti odottaa saavansa propsina, voimme välittää propsit hyödyntäen spread-syntaksia, seuraavasti:

<input {...name} /> 

Eli kuten Reactin dokumentaation esimerkki sanoo, 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, 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

Lisää hookeista