This course is now available in English.Click here to change the language.

d

Monimutkaisempi tila, Reactin debuggaus

Monimutkaisempi tila

Edellisessä esimerkissä sovelluksen tila oli yksinkertainen, se koostui ainoastaan yhdestä kokonaisluvusta. Entä jos sovellus tarvitsee monimutkaisemman tilan?

Helpoin ja useimmiten paras tapa on luoda sovellukselle useita erillisiä tiloja tai tilan "osia" kutsumalla funktiota useState useampaan kertaan.

Seuraavassa sovellukselle luodaan kaksi alkuarvon 0 saavaa tilaa left ja right:

const App = (props) => {
  const [left, setLeft] = useState(0)
  const [right, setRight] = useState(0)

  return (
    <div>
      <div>
        {left}
        <button onClick={() => setLeft(left + 1)}>
          left
        </button>
        <button onClick={() => setRight(right + 1)}>
          right
        </button>
        {right}
      </div>
    </div>
  )
}

Komponentti saa käyttöönsä tilan alustuksen yhteydessä funktiot setLeft ja setRight joiden avulla se voi päivittää tilan osia.

Komponentin tila tai yksittäinen tilan pala voi olla minkä tahansa tyyppinen. Voisimme toteuttaa saman toiminnallisuuden tallentamalla nappien left ja right painallukset yhteen olioon

{
  left: 0,
  right: 0
}

sovellus muuttuisi seuraavasti:

const App = (props) => {
  const [clicks, setClicks] = useState({
    left: 0, right: 0
  })

  const handleLeftClick = () => {
    const newClicks = { 
      left: clicks.left + 1, 
      right: clicks.right 
    }
    setClicks(newClicks)
  }

  const handleRightClick = () => {
    const newClicks = { 
      left: clicks.left, 
      right: clicks.right + 1 
    }
    setClicks(newClicks)
  }

  return (
    <div>
      <div>
        {clicks.left}
        <button onClick={handleLeftClick}>left</button>
        <button onClick={handleRightClick}>right</button>
        {clicks.right}
      </div>
    </div>
  )
}

Nyt komponentilla on siis ainoastaan yksi tila. Näppäinten painallusten yhteydessä on nyt huolehdittava koko tilan muutoksesta.

Tapahtumankäsittelijä vaikuttaa hieman sotkuiselta. Kun nappia left painetaan, suoritetaan seuraava funktio:

const handleLeftClick = () => {
  const newClicks = { 
    left: clicks.left + 1, 
    right: clicks.right 
  }
  setClicks(newClicks)
}

uudeksi tilaksi siis aseteaan seuraava olio

{
  left: clicks.left + 1,
  right: clicks.right
}

eli kentän left arvo on sama kuin alkuperäisen tilan kentän left + 1 ja kentän right arvo on sama kuin alkuperäisen tilan kenttä right.

Uuden tilan määrittelevän olion modostaminen onnistuu hieman tyylikkäämmin hyödyntämällä kesällä 2018 kieleen tuotua object spread -syntaksia:

const handleLeftClick = () => {
  const newClicks = { 
    ...clicks, 
    left: clicks.left + 1 
  }
  setClicks(newClicks)
}

const handleRightClick = () => {
  const newClicks = { 
    ...clicks, 
    right: clicks.right + 1 
  }
  setClicks(newClicks)
}

Merkintä vaikuttaa hieman erikoiselta. Käytännössä { ...clicks } luo olion, jolla on kenttinään kopiot olion clicks kenttien arvoista. Kun aaltosulkeisiin lisätään asioita, esim. { ...clicks, right: 1 }, tulee uuden olion kenttä right saamaan arvon 1.

Esimerkissämme siis

{ ...clicks, right: clicks.right + 1 }

luo oliosta clicks kopion, missä kentän right arvoa kasvatetaan yhdellä.

Apumuuttujat ovat oikeastaan turhat, ja tapahtumankäsittelijät voidaan määritellä seuraavasti:

const handleLeftClick = () =>
  setClicks({ ...clicks, left: clicks.left + 1 })

const handleRightClick = () =>
  setClicks({ ...clicks, right: clicks.right + 1 })

Lukijalle voi tässä vaiheessa herätä kysymys miksi emme hoitaneet tilan päivitystä seuraavalla tavalla

const handleLeftClick = () => {
  clicks.left++
  setClicks(clicks)
}

Sovellus näyttää toimivan. Reactissa ei kuitenkaan ole sallittua muuttaa tilaa suoraan, sillä voi olla arvaamattomat seuraukset. Tilan muutos tulee aina tehdä asettamalla uudeksi tilaksi vanhan perusteella tehty kopio!

Kaiken tilan pitäminen yhdessä oliossa on tämän sovelluksen kannalta huono ratkaisu; etuja siinä ei juuri ole, mutta sovellus monimutkaistuu merkittävästi. Onkin ehdottomasti parempi ratkaisu tallettaa nappien klikkaukset erillisiin tilan paloihin.

On kuitenkin tilanteita, joissa jokin osa tilaa kannattaa pitää monimutkaisemman tietorakenteen sisällä. Reactin dokumentaatiossa on hieman ohjeistusta aiheeseen liityen.

Taulukon käsittelyä

Tehdään sovellukseen vielä laajennus, lisätään sovelluksen tilaan taulukko allClicks joka muistaa kaikki näppäimenpainallukset.

const App = (props) => {
  const [left, setLeft] = useState(0)
  const [right, setRight] = useState(0)
  const [allClicks, setAll] = useState([])
  const handleLeftClick = () => {    setAll(allClicks.concat('L'))    setLeft(left + 1)  }
  const handleRightClick = () => {    setAll(allClicks.concat('R'))    setRight(right + 1)  }
  return (
    <div>
      <div>
        {left}
        <button onClick={handleLeftClick}>left</button>
        <button onClick={handleRightClick}>right</button>
        {right}
        <p>{allClicks.join(' ')}</p>      </div>
    </div>
  )
}

Kaikki klikkaukset siis talletetaan omaan tilan osaansa allClicks, joka alustetaan tyhjäksi taulukoksi

const [allClicks, setAll] = useState([])

Kun esim. nappia left painetaan, lisätään tilan taulukkoon allClicks kirjain L:

const handleLeftClick = () => {
  setAll(allClicks.concat('L'))
  setLeft(left + 1)
}

Tilan osa allClicks saa nyt arvokseen taulukon, missä on entisen taulukon alkiot ja L. Uuden alkion liittäminen on tehty metodilla concat, joka toimii siten, että se ei muuta olemassaolevaa taulukkoa vaan luo uuden taulukon, mihin uusi alkio on lisätty.

Kuten jo aiemmin mainittiin, Javascriptissa on myös mahdollista lisätä taulukkoon metodilla push ja sovellus näyttäisi tässä tilanteessa toimivan myös jos lisäys hoidettaisiin siten että allClicks-tilaa muuteaan pushaamalla siihen alkio ja sitten päivitetään tila:

const handleLeftClick = () => {
  allClicks.push('L')
  setAll(allClick)
  setLeft(left + 1)
}

Älä kuitenkaan tee näin. Kuten jo mainitsimme, React-komponentin tilaa, eli esimerkiksi muuttujaa allClicks ei saa muuttaa. Vaikka tilan muuttaminen näyttääkin toimivan joissaikin tilanteissa, voi seurauksena olla hankalasti havaittavia ongelmia.

Katsotaan vielä tarkemmin, miten kaikkien painallusten historia renderöidään ruudulle:

const App = (props) => {
  // ...

  return (
    <div>
      <div>
        {left}
        <button onClick={handleLeftClick}>left</button>
        <button onClick={handleRightClick}>right</button>
        {right}
        <p>{allClicks.join(' ')}</p>      </div>
    </div>
  )
}

Taulukolle allClicks kutsutaan metodia join, joka muodostaa taulukosta merkkijonon, joka sisältää taulukon alkiot erotettuina parametrina olevalla merkillä, eli välilyönnillä.

Ehdollinen renderöinti

Muutetaan sovellusta siten, että näppäilyhistorian renderöinnistä vastaa komponentti History:

const History = (props) => {
  if (props.allClicks.length === 0) {
    return (
      <div>
        the app is used by pressing the buttons
      </div>
    )
  }

  return (
    <div>
      button press history: {props.allClicks.join(' ')}
    </div>
  )
}

const App = (props) => {
  // ...

  return (
    <div>
      <div>
        {left}
        <button onClick={handleLeftClick}>left</button>
        <button onClick={handleRightClick}>right</button>
        {right}
        <History allClicks={allClicks} />      </div>
    </div>
  )
}

Nyt komponentin toiminta riippuu siitä, onko näppäimiä jo painettu. Jos ei, eli taulukko allClicks on tyhjä, renderöi komponentti "käyttöohjeen" sisältävän divin.

<div>the app is used by pressing the buttons</div>

ja muussa tapauksessa näppäilyhistorian:

<div>
  button press history: {props.allClicks.join(' ')}
</div>

Komponentin History ulkoasun muodostamat React-elementit siis ovat erilaisia riippuen sovelluksen tilasta, eli komponentissa on ehdollista renderöintiä.

Reactissa on monia muitakin tapoja ehdolliseen renderöintiin. Katsotaan niitä tarkemmin seuraavassa osassa.

Muutetaan vielä sovellusta siten, että se käyttää aiemmin määrittelemäämme komponenttia Button painikkeiden muodostamiseen:

const History = (props) => {
  if (props.allClicks.length === 0) {
    return (
      <div>
        the app is used by pressing the buttons
      </div>
    )
  }

  return (
    <div>
      button press history: {props.allClicks.join(' ')}
    </div>
  )
}

const Button = ({ handleClick, text }) => (  <button onClick={handleClick}>    {text}  </button>)
const App = (props) => {
  const [left, setLeft] = useState(0)
  const [right, setRight] = useState(0)
  const [allClicks, setAll] = useState([])

  const handleLeftClick = () => {
    setAll(allClicks.concat('L'))
    setLeft(left + 1)
  }

  const handleRightClick = () => {
    setAll(allClicks.concat('R'))
    setRight(right + 1)
  }

  return (
    <div>
      <div>
        {left}
        <Button handleClick={handleLeftClick} text='left' />        <Button handleClick={handleRightClick} text='right' />        {right}
        <History allClicks={allClicks} />
      </div>
    </div>
  )
}

Vanha React

Tällä kurssilla käyttämämme tapa React-komponenttien tilan määrittelyyn, eli state hook on siis uutta Reactia ja käytettävissä versiosta 16.8.0 lähtien. Ennen hookeja Javascript-funktioina määriteltyihin React-komponentteihin ei ollut mahdollista saada tilaa ollenkaan, tilaa edellyttävät komponentit oli pakko määritellä Class-komponentteina Javascriptin luokkasyntaksia hyödyntäen.

Olemme tällä kurssilla tehneet hieman radikaalinkin ratkaisun käyttää pelkästään hookeja ja näin ollen opetella heti alusta asti ohjelmoimaan "huomisen" Reactia. Luokkasyntaksin hallitseminen on kuitenkin sikäli tärkeää, että vaikka funktiona määriteltävät komponentit ovat Reactin tulevaisuus, on maailmassa miljardeja rivejä vanhaa Reactia, jota kenties sinäkin joudut jonain päivänä ylläpitämään. Dokumentaation ja internetistä löytyvien esimerkkien suhteen tilanne on sama, törmäät class-komponentteihin välittömästi.

Tutustummekin riittävällä tasolla class-komponentteihin hieman myöhemmin kurssilla.

React-sovellusten debuggaus

Ohjelmistokehittäjän elämä koostuu pääosin debuggaamisesta (ja olemassaolevan koodin lukemisesta). Silloin tällöin syntyy toki muutama rivi uuttakin koodia, mutta suuri osa ajasta ihmetellään miksi joku on rikki tai miksi joku asia ylipäätään toimii. Hyvät debuggauskäytänteet ja työkalut ovatkin todella tärkeitä.

Onneksi React on debuggauksen suhteen jopa harvinaisen kehittäjäystävällinen kirjasto.

Muistutetaan vielä tärkeimmästä web-sovelluskehitykseen liittyvästä asiasta:

Web-sovelluskehityksen sääntö numero yksi

Pidä selaimen developer-konsoli koko ajan auki.

Välilehdistä tulee olla auki nimenomaan Console jollei ole erityistä syytä käyttää jotain muuta välilehteä.

Pidä myös koodi ja web-sivu koko ajan molemmat yhtä aikaa näkyvillä.

Jos ja kun koodi ei käänny, eli selaimessa alkaa näkyä punaista

fullstack content

älä kirjota enää lisää koodia vaan selvitä ongelma välittömästi. Koodauksen historia ei tunne tilannetta, missä kääntymätön koodi alkaisi ihmeenomaisesti toimimaan kirjoittamalla suurta määrää lisää koodia, enkä usko että sellaista ihmettä nähdään tälläkään kurssilla.

Vanha kunnon printtaukseen perustuva debuggaus kannattaa aina. Eli jos esim. komponentissa

const Button = ({ handleClick, text }) => (
  <button onClick={handleClick}>
    {text}
  </button>
)

olisi jotain ongelmia, kannattaa komponentista alkaa printtailla konsoliin. Pystyäksemme printtaamaan, tulee funktio muuttaa pitempään muotoon ja propsit kannattaa kenties vastaanottaa ilman destrukturointia:

const Button = (props) => { 
  console.log(props)  const { handleClick, text } = props
  return (
    <button onClick={handleClick}>
      {text}
    </button>
  )
}

näin selviää heti onko esim. joku propsia vastaava attribuutti nimetty väärin komponenttia käytettäessä.

HUOM kun käytät komentoa console.log debuggaukseen, älä yhdistele asioita "javamaisesti" plussalla, eli sen sijaan että kirjoittaisit

console.log('props value is' + props)

erottele tulostettavat asiat pilkulla:

console.log('props value is', props)

Jos yhdistät merkkijonoon olion, tuloksena on suhteellisen hyödytön tulostusmuoto

props value is [Object object]

kun taas pilkulla tulostettavat asiat erotellessa saat developer-konsoliin olion, jonka sisältöä on mahdollista tarkastella.

Konsoliin tulostus ei ole suinkaan ainoa keino debuggaamiseen. Koodin suorituksen voi pysäyttää Chromen developer konsolin debuggeriin kirjoittamalla mihin tahansa kohtaa koodia komennon debugger.

Koodi pysähtyy kun suoritus etenee sellaiseen pisteeseen, missä komento debugger suoritetaan:

fullstack content

Menemällä välilehdelle Console on helppo tutkia muuttujien tilaa:

fullstack content

Kun bugi selviää, voi komennon debugger poistaa ja uudelleenladata sivun.

Debuggerissa on mahdollista suorittaa koodia tarvittaessa rivi riviltä Sources-välilehden oikealta laidalta.

Debuggeriin pääsee myös ilman komentoa debugger, lisäämällä Sources-välilehdellä sopiviin kohtiin koodia breakpointeja. Komponentin muuttujien arvojen tarkkailu on mahdollista Scope-osassa:

fullstack content

Chromeen kannattaa ehdottomasti asentaa React developer tools -lisäosa, joka tuo konsoliin uuden tabin React:

fullstack content

Uuden konsolitabin avulla voidaan tarkkailla sovelluksen React-elementtejä ja niiden tilaa ja propseja.

React developer tools ei osaa toistaiseksi näyttää hookeilla muodostettua tilaa kovin hyvin.

fullstack content

Komponentin tila on määritelty seuraavasti:

const [left, setLeft] = useState(0)
const [right, setRight] = useState(0)
const [allClicks, setAll] = useState([])

Dev tools näyttää hookien tilan siinä järjestyksessä kun ne on määritelty koodissa. Hookien left ja right tila näkyy hyvin, mutta valitettavasti dev tools ei tässä vaiheessa osaa näyttää taulukkotyyppistä hookia allClicks kunnolla.

Dev toolsin hookien toteutusta käsitellään tässä issuessa.

Dev toolsin hookeja tukevasta versiosta on jo tehty epävirallinen beta-julkaisu. En saanut itse Chrome-versiota toimimaan, mutta Firefox-versio toimii ja näyttää hookatun taulukkomuotoisen tilan hyvin

fullstack content

Hookien säännöt

Jotta hookeilla muodostettu sovelluksen tila toimisi oikein, on hookeja käytettävä tiettyjä rajoituksia noudattaen.

Funktiota useState (eikä seuraavassa osassa esiteltävää funktiota useEffect) ei saa kutsua loopissa, ehtolausekkeiden sisältä tai muista kun komponentin määrittelevästä funktioista. Tämä takaa sen, että hookeja kutsutaan aina samassa järjestyksessä, jos näin ei ole, sovellus toimii miten sattuu.

Hookeja siis kuuluu kutsua ainoastaan React-komponentin määrittelevän funktion rungosta:

const App = (props) => {
  // nämä ovat ok
  const [age, setAge] = useState(0)
  const [name, setName] = useState('Juha Tauriainen')

  if ( age > 10 ) {
    // ei näin!
    const [foobar, setFoobar] = useState(null)
  }

  for ( let i = 0; i < age; i++ ) {
    // eikä näin!
    const [rightWay, setRightWay] = useState(false)
  }

  const notGood = () => {
    // eikä myöskään näin
    const [x, setX] = useState(-1000)
  }

  return (
    //...
  )
}

Tapahtumankäsittely revisited

Edellisen vuoden kurssin perusteella tapahtumankäsittely on osoittautunut monelle haastavaksi.

Tarkastellaan asiaa vielä uudelleen.

Oletetaan, että käytössä on äärimmäisen yksinkertainen sovellus:

const App = (props) => {
  const [value, setValue] = useState(10)

  return (
    <div>
      {value}
      <button>reset to zero</button>
    </div>
  )
}

ReactDOM.render(
  <App />, 
  document.getElementById('root')
)

Haluamme, että napin avulla tilan tallettava muuttuja value saadaan nollattua.

Jotta saamme napin reagoimaan, on sille lisättävä tapahtumankäsittelijä.

Tapahtumankäsittelijän tulee aina olla funktio tai viite funktioon. Jos tapahtumankäsittelijän paikalle yritetään laittaa jotain muuta, ei nappi toimi.

Jos esim. antaisimme tapahtumankäsittelijäksi merkkijonon:

<button onClick={'crap...'}>button</button>

React varoittaa asiasta konsolissa

index.js:2178 Warning: Expected `onClick` listener to be a function, instead got a value of `string` type.
    in button (at index.js:20)
    in div (at index.js:18)
    in App (at index.js:27)

myös seuraavanlainen yritys olisi tuhoon tuomittu

<button onClick={value + 1}>button</button>

nyt tapahtumankäsittelijäksi on yritetty laittaa value + 1 mikä tarkoittaa laskuoperaation tulosta. React varoittaa tästäkin konsolissa

index.js:2178 Warning: Expected `onClick` listener to be a function, instead got a value of `number` type.

Myöskään seuraava ei toimi

<button onClick={value = 0}>button</button>

taaskaan tapahtumankäsittelijänä ei ole funktio vaan sijoitusoperaatio. Konsoliin tulee valitus. Tämä tapa on myös toisella tavalla väärin. Tilan muuttaminen ei onnistu suoraan tilan arvon tallentavaa muuttujaa muuttamalla.

Entä seuraava:

<button onClick={console.log('clicked the button')}>
  button
</button>

konsoliin tulostuu kertaalleen clicked the button, mutta nappia painellessa ei tapahdu mitään. Miksi tämä ei toimi vaikka tapahtumankäsittelijänä on nyt funktio console.log?

Ongelma on nyt siinä, että tapahtumankäsittelijänä on funktion kutsu, eli varsinaiseksi tapahtumankäsittelijäksi tulee funktion kutsun paluuarvo, joka on tässä tapauksessa määrittelemätön arvo undefined.

Funktiokutsu console.log('clicked the button') suoritetaan siinä vaiheessa kun komponentti renderöidään, ja tämän takia konsoliin tulee tulostus kertalleen.

Myös seuraava yritys on virheellinen

<button onClick={setValue(0)}>button</button>

jälleen olemme yrittäneet laittaa tapahtumankäsittelijäksi funktiokutsun. Ei toimi. Tämä yritys aiheuttaa myös toisen ongelman. Kun komponenttia renderöidään, suoritetaan tapahtumankäsittelijänä oleva funktiokutsu setValue(0) joka taas saa aikaan komponentin uudelleenrenderöinnin. Ja uudelleenrenderöinnin yhteydessä funktiota kutsutaan uudelleen käynnistäen jälleen uusi uudelleenrenderöinti, ja joudutaan päättymättömään rekursioon.

Jos haluamme suorittaa tietyn funktiokutsun tapahtuvan nappia painettaessa, toimii seuraava

<button onClick={() => console.log('clicked the button')}>
  button
</button>

Nyt tapahtumankäsittelijä on nuolisyntaksilla määritelty funktio () => console.log('clicked the button'). Kun komponentti renderöidään, ei suoriteta mitään, ainoastaan talletetaan funktioviite tapahtumankäsittelijäksi. Itse funktion suoritus tapahtuu vasta napin painallusten yhteydessä.

Saamme myös nollauksen toimimaan samalla tekniikalla

<button onClick={() => setValue(0)}>button</button>

eli nyt tapahtumankäsittelijä on funktio () => setValue(0).

Tapahtumakäsittelijäfunktioiden määrittely suoraan napin määrittelyn yhteydessä ei välttämättä ole paras mahdollinen idea.

Usein tapahtumankäsittelijä määritelläänkin jossain muualla. Seuraavassa määritellään funktio ja sijoitetaan se muuttujaan handleClick komponentin rungossa:

const App = (props) => {
  const [value, setValue] = useState(10)

  const handleClick = () =>
    console.log('clicked the button')

  return (
    <div>
      {value}
      <button onClick={handleClick}>button</button>
    </div>
  )
}

Muuttujassa handleClick on nyt talletettuna viite itse funktioon. Viite annetaan napin määrittelyn yhteydessä attribuutin onClick:

<button onClick={handleClick}>button</button>

Tapahtumankäsittelijäfunktio voi luonnollisesti koostua useista komennoista, tällöin käytetään nuolifunktion aaltosulullista muotoa:

const App = (props) => {
  const [value, setValue] = useState(10)

  const handleClick = () => {    console.log('clicked the button')    setValue(0)  }
  return (
    <div>
      {value}
      <button onClick={handleClick}>button</button>
    </div>
  )
}

Mennään lopuksi funktion palauttavaan funktioon. Kuten aiemmin jo mainittiin, et tarvitse ainakaan tämän osan, et kenties koko kurssin tehtävissä funktiota palauttavia funktioita, joten voit melko huoletta hypätä seuraavan ohi jos asia tuntuu nyt hankalalta.

Muutetaan koodia seuraavasti

const App = (props) => {
  const [value, setValue] = useState(10)

  const hello = () => {    const handler = () => console.log('hello world')    return handler  }
  return (
    <div>
      {value}
      <button onClick={hello()}>button</button>
    </div>
  )
}

Koodi näyttää hankalalta mutta se ihme kyllä toimii.

Tapahtumankäsittelijäksi on nyt "rekisteröity" funktiokutsu:

<button onClick={hello()}>button</button>

Aiemmin varoteltiin, että tapahtumankäsittelijä ei saa olla funktiokutsu vaan sen on oltava funktio tai viite funktioon. Miksi funktiokutsu kuitenkin toimii nyt?

Kun komponenttia renderöidään suoritetaan seuraava funktio:

const hello = () => {
  const handler = () => console.log('hello world')

  return handler
}

funktion paluuarvona on nyt toinen, muuttujaan handler määritelty funktio.

eli kun react renderöi seuraavan rivin

<button onClick={hello()}>button</button>

sijoittaa se onClick-käsittelijäksi funktiokutsun hello() paluuarvon. Eli oleellisesti ottaen rivi "muuttuu" seuraavaksi

<button onClick={() => console.log('hello world')}>
  button
</button>

koska funktio hello palautti funktion, on tapahtumankäsittelijä nyt funktio.

Mitä järkeä tässä konseptissa on?

Muutetaan koodia hiukan:

const App = (props) => {
  const [value, setValue] = useState(10)

  const hello = (who) => {    const handler = () => {      console.log('hello', who)    }    return handler  }
  return (
    <div>
      {value}
      <button onClick={hello('world')}>button</button>      <button onClick={hello('react')}>button</button>      <button onClick={hello('function')}>button</button>    </div>
  )
}

Nyt meillä on kolme nappia joiden tapahtumankäsittelijät määritellään parametrin saavan funktion button avulla.

Ensimmäinen nappi määritellään seuraavasti

<button onClick={hello('world')}>button</button>

Tapahtumankäsittelijä siis saadaan suorittamalla funktiokutsu hello('world'). Funktiokutsu palauttaa funktion

() => {
  console.log('hello', 'world')
}

Toinen nappi määritellään seuraavasti

<button onClick={hello('react')}>button</button>

Tapahtumankäsittelijän määrittelevä funktiokutsu hello('react') palauttaa

() => {
  console.log('hello', 'react')
}

eli molemmat napit saavat oman, yksilöllisen tapahtumankäsittelijänsä.

Funktioita palauttavia funktioita voikin hyödyntää määrittelemään geneeristä toiminnallisuutta, jota voi tarkentaa parametrien avulla. Tapahtumankäsittelijöitä luovan funktion hello voikin ajatella olevan eräänlainen tehdas, jota voi pyytää valmistamaan sopivia tervehtimiseen tarkoitettuja tapahtumankäsittelijäfunktioita.

Käyttämämme määrittelytapa

const hello = (who) => {
  const handler = () => {
    console.log('hello', who)
  }

  return handler
}

on hieman verboosi. Eliminoidaan apumuuttuja, ja määritellään palautettava funktio suoraan returnin yhteydessä:

const hello = (who) => {
  return () => {
    console.log('hello', who)
  }
}

ja koska funktio hello sisältää ainoastaan yhden komennon, eli returnin, voidaan käyttää aaltosulutonta muotoa

const hello = (who) =>
  () => {
    console.log('hello', who)
  }

ja tuodaan vielä "kaikki nuolet" samalle riville

const hello = (who) => () => {
  console.log('hello', who)
}

Voimme käyttää samaa kikkaa myös muodostamaan tapahtumankäsittelijöitä, jotka asettavat komponentin tilalle halutun arvon. Muutetaan koodi muotoon:

render() {
  const setToValue = (newValue) => () => {
    setValue(newValue)
  }

  return (
    <div>
      {value}
      <button onClick={setToValue(1000)}>thousand</button>
      <button onClick={setToValue(0)}>reset</button>
      <button onClick={setToValue(value + 1)}>increment</button>
    </div>
  )
}

Kun komponentti renderöidään, ja tehdään nappia thousand

<button onClick={setToValue(1000)}>thousand</button>

tulee tapahtumankäsittelijäksi funktiokutsun setToValue(1000) paluuarvo eli seuraava funktio

() => {
    setValue(1000)
}

Kasvatusnapin generoima rivi on seuraava

<button onClick={setToValue(value + 1)}>increment</button>

Tapahtumankäsittelijän muodostaa funktiokutsu setToValue(value + 1), joka saa parametrikseen tilan tallettavan muuttujan value nykyisen arvon kasvatettuna yhdellä. Jos value olisi 10, tulisi tapahtumankäsittelijäksi funktio

() => {
  setValue(11)
}

Funktioita palauttavia funktioita ei tässäkään tapauksessa olisi ollut pakko käyttää. Muutetaan tilan päivittämisestä huolehtiva funktio setToValue normaaliksi funktioksi:

const App = (props) => {
  const [value, setValue] = useState(10)

  const setToValue = (newValue) => {
    setValue(newValue)
  }

  return (
    <div>
      {value}
      <button onClick={() => setToValue(1000)}>
        thousand
      </button>
      <button onClick={() => setToValue(0)}>
        reset
      </button>
      <button onClick={() => setToValue(value + 1)}>
        increment
      </button>
    </div>
  )
}

Voimme nyt määritellä tapahtumankäsittelijän funktioksi, joka kutsuu funktiota setToValue sopivalla parametrilla, esim. nollaamisen tapahtumankäsittelijä:

<button onClick={() => setToValue(0)}>reset</button>

On aikalailla makuasia käyttääkö tapahtumankäsittelijänä funktioita palauttavia funktioita vai nuolifunktioita.

Tapahtumankäsittelijän vieminen alikomponenttiin

Eriytetään vielä painike omaksi komponentikseen

const Button = (props) => (
  <button onClick={props.handleClick}>
    {props.text}
  </button>
)

Komponentti saa siis propsina handleClick tapahtumankäsittelijän ja propsina text merkkijonon, jonka se renderöi painikkeen tekstiksi.

Komponentin Button käyttö on helppoa, on toki pidettävä huolta siitä, että komponentille annettavat propsit on nimetty niin kuin komponentti olettaa:

fullstack content

Älä määrittele komponenttia komponentin sisällä

Eriytetään vielä sovelluksestamme luvun näyttäminen omaan komponenttiin Display.

Muutetaan ohjelmaa seuraavasti, eli määritelläänkin uusi komponentti App-komponentin sisällä:

// tämä on oikea paikka määritellä komponentti!
const Button = (props) => (
  <button onClick={props.handleClick}>
    {props.text}
  </button>
)

const App = props => {
  const [value, setValue] = useState(10)

  const setToValue = newValue => {
    setValue(newValue)
  }

  // älä määrittele komponenttia täällä!
  const Display = props => <div>{props.value}</div>
  return (
    <div>
      <Display value={value} />
      <Button onClick={() => setToValue(1000)} text="thousand" />
      <Button onClick={() => setToValue(0)} text="reset" />
      <Button onClick={() => setToValue(value + 1)} text="increment" />
    </div>
  )
}

Kaikki näyttää toimivan. Mutta älä tee koskaan näin!, eli määrittele komponenttia toisen komponentin sisällä. Tapa on hyödytön ja johtaa useissa tilanteissa ikäviin ongelmiin. Siirretäänkin komponentin Display määrittely oikeaan paikkaan, eli komponentin App määrittelevän funktion ulkopuolelle:

const Display = props => <div>{props.value}</div>

const Button = (props) => (
  <button onClick={props.handleClick}>
    {props.text}
  </button>
)

const App = props => {
  const [value, setValue] = useState(10)

  const setToValue = newValue => {
    setValue(newValue)
  }

  return (
    <div>
      <Display value={value} />
      <Button onClick={() => setToValue(1000)} text="thousand" />
      <Button onClick={() => setToValue(0)} text="reset" />
      <Button onClick={() => setToValue(value + 1)} text="increment" />
    </div>
  )
}

Hyödyllistä materiaalia

Internetissä on todella paljon Reactiin liittyvää materiaalia. Tällä hetkellä ongelman muodostaa kuitenkin se, että käytämme kurssilla niin uutta Reactia, että suurin osa internetistä löytyvästä tavarasta on meidän kannaltamme vanhentunutta.

Seuraavassa muutamia linkkejä:

  • Reactin docs kannattaa ehdottomasti käydä jossain vaiheessa läpi, ei välttämättä kaikkea nyt, osa on ajankohtaista vasta kurssin myöhemmissä osissa ja kaikki Class-komponentteihin liittyvä on kurssin kannalta epärelevanttia
  • Reactin sivuilla oleva tutoriaali sen sijaan on aika huono
  • Egghead.io:n kursseista Start learning React on laadukas, ja hieman uudempi The Beginner's guide to React on myös kohtuullisen hyvä; molemmat sisältävät myös asioita jotka tulevat tällä kurssilla vasta myöhemmissä osissa. Molemmissa toki se ongelma, että ne käyttävät Class-komponentteja