Creative Commons -lisenssi

Osan 1 oppimistavoitteet

  • React
    • funktiona ja luokkana määriteltävät komponentit
    • tietojen ja funktioiden välittäminen lapsikomponentteihin propseina
    • komponentin tila
    • tilan päivittämisen periaatteet
    • tapahtumankäsittelyn perusteet
  • Javascript
    • muuttujien määrittely
    • taulukko ja sen operaatiot (mm. concat, forEach, join ja map)
    • literaalisyntaksilla määritellyt oliot
    • funktioiden määrittely
    • this:in käyttäytyminen
    • funktioita palauttavat funktiot
    • luokkasyntaksi
    • class propertynä määritellyt metodit

React

Alamme nyt tutustua kurssin ehkä tärkeimpään teemaan, React-kirjastoon. Tehdään heti yksinkertainen React-sovellus ja tutustutaan samalla Reactin peruskäsitteistöön.

Ehdottomasti helpoin tapa päästä alkuun on create-react-app-nimisen työkalun käyttö. create-react-app on mahdollista asentaa, mutta asennukseen ei ole tarvetta jos Noden mukana asentunut npm-työkalu on versioltaan vähintään 5.3. Tällöin npm:n mukana asentuu komento npx, joka mahdollistaa create-react-app:in käytön asentamatta sitä erikseen. Npm:n version saa selville komennolla npm -v.

Luodaan sovellus nimeltään osa1 ja käynnistetään se:

$ npx create-react-app osa1
$ cd osa1
$ npm start

Sovellus käynnistyy oletusarvoisesti localhostin porttiin 3000, eli osoitteeseen http://localhost:3000

Chromen pitäisi aueta automaattisesti. Avaa konsoli välittömästi. Avaa myös tekstieditori siten, että näet koodin ja web-sivun samaan aikaan ruudulla:

Sovelluksen koodi on hakemistossa src. Yksinkertaistetaan valmiina olevaa koodia siten, että tiedoston index.js sisällöksi tulee:

import React from 'react'
import ReactDOM from 'react-dom'

const App = () => (
  <div>
    <p>Hello world</p>
  </div>
)

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

Voit poistaa tiedostot App.js, App.css, App.test.js, logo.svg ja registerServiceWorker.js

Komponentti

Tiedosto index.js määrittelee nyt React-komponentin nimeltään App ja viimeisen rivin komento

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

renderöi komponentin sisällön tiedoston public/index.html määrittelemään div-elementtiin, jonka id:n arvona on ‘root’

Tiedosto public/index.html on oleellisesti ottaen tyhjä, voit kokeilla lisätä sinne HTML:ää. Reactilla ohjelmoitaessa yleensä kuitenkin kaikki renderöitävä sisältö määritellään Reactin komponenttien avulla.

Tarkastellaan vielä tarkemmin komponentin määrittelevää koodia:

const App = () => (
  <div>
    <p>Hello world</p>
  </div>
)

Kuten arvata saattaa, komponentti renderöityy div-tagina, jonka sisällä on p-tagin sisällä oleva teksti Hello world.

Teknisesti ottaen komponentti on määritelty Javascript-funktiona. Seuraava siis on funktio (joka ei saa yhtään parametria):

() => (
  <div>
    <p>Hello world</p>
  </div>
)

joka sijoitetaan vakioarvoiseen muuttujaan App

const App = ...

Javascriptissa on muutama tapa määritellä funktioita. Käytämme nyt Javascriptin hieman uudemman version EcmaScript 6:n eli ES6:n nuolifunktiota (arrow functions).

Koska funktio koostuu vain yhdestä lausekkeesta, on käytössämme lyhennysmerkintä, joka vastaa oikeasti seuraavaa koodia:

const App = () => {
  return (
    <div>
      <p>Hello world</p>
    </div>
  )
}

eli funktio palauttaa sisältämänsä lausekkeen arvon.

Komponentin määrittelevä funktio voi sisältää mitä tahansa Javascript-koodia. Muuta komponenttisi seuraavaan muotoon ja katso mitä konsolissa tapahtuu:

const App = () => {
  console.log('Hello from komponentti')
  return (
    <div>
      <p>Hello world</p>
    </div>
  )}

Komponenttien sisällä on mahdollista renderöidä myös dynaamista sisältöä.

Muuta komponentti muotoon:

const App = () => {
  const now = new Date()
  const a = 10
  const b = 20
  return (
    <div>
      <p>Hello world, it is {now.toString()}</p>
      <p>{a} plus {b} is {a + b}</p>
    </div>
  )
}

Aaltosulkeiden sisällä oleva Javascript-koodi evaluoidaan ja evaluoinnin tulos upotetaan määriteltyyn kohtaan komponentin tuottamaa HTML-koodia.

JSX

Näyttää siltä, että React-komponentti palauttaa HTML-koodia. Näin ei kuitenkaan ole. React-komponenttien ulkoasu kirjoitetaan yleensä JSX:ää käyttäen. Vaikka JSX näyttää HTML:ltä, kyseessä on kuitenkin tapa kirjoittaa Javascriptiä. React komponenttien palauttama JSX käännetään konepellin alla Javascriptiksi.

Käännösvaiheen jälkeen ohjelmamme näyttää seuraavalta:

import React from 'react';
import ReactDOM from 'react-dom';

const App = () => {
  const now = new Date();
  const a = 10;
  const b = 20;
  return React.createElement(
    'div', null,
    React.createElement(
      'p', null, 'Hello world, it is ', now.toString()
    ),
    React.createElement(
      'p', null, a, ' plus ', b, ' is ', a + b
    )
  )
}

ReactDOM.render(React.createElement(App, null), document.getElementById('root'));

Käännöksen hoitaa Babel. Create-react-app:illa luoduissa projekteissa käännös on konfiguroitu tapahtumaan automaattisesti. Tulemme tutustumaan aiheeseen tarkemmin kurssin osassa 7.

Reactia olisi myös mahdollista kirjoittaa “suoraan Javascriptinä” käyttämättä JSX:ää. Kukaan täysijärkinen ei kuitenkaan niin tee.

Käytännössä JSX on melkein kuin HTML:ää sillä erotuksella, että mukaan voi upottaa helposti dynaamista sisältöä kirjoittamalla sopivaa Javascriptiä aaltosulkeiden sisälle. Idealtaan JSX on melko lähellä monia palvelimella käytettäviä templating-kieliä kuten Java Springin yhteydessä käytettävää thymeleafia.

JSX on “XML:n kaltainen”, eli jokainen tagi tulee sulkea. Esimerkiksi rivinvaihto on tyhjä elementti, joka kirjoitetaan HTML:ssä tyypillisesti

<br>

mutta JSX:ää kirjoittaessa tagi on pakko sulkea:

<br />

Monta komponenttia

Muutetaan sovellusta seuraavasti (yläreunan importit jätetään esimerkeistä nyt ja jatkossa pois):

const Hello = () => {
  return (
    <div>
      <p>Hello world</p>
    </div>
  )
}

const App = () => {
  return (
    <div>
      <h1>Greetings</h1>
      <Hello />
    </div>
  )
}

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

Olemme määritelleet uuden komponentin Hello, jota käytetään komponentista App. Komponenttia voidaan luonnollisesti käyttää monta kertaa:

const App = () => {
  return (
    <div>
      <h1>Greetings</h1>
      <Hello />
      <Hello />
      <Hello />
    </div>
  )
}

Komponenttien tekeminen Reactissa on helppoa ja komponentteja yhdistelemällä monimutkaisempikin sovellus on mahdollista pitää kohtuullisesti ylläpidettävänä. Reactissa filosofiana onkin koostaa sovellus useista, pieneen asiaan keskittyvistä uudelleenkäytettävistä komponenteista.

props: tiedonvälitys komponenttien välillä

Komponenteille on mahdollista välittää dataa propsien avulla.

Muutetaan komponenttia Hello seuraavasti

const Hello = (props) => {
  return (
    <div>
      <p>Hello {props.name}</p>
    </div>
  )
}

komponentin määrittelevällä funktiolla on nyt parametri props. Parametri saa arvokseen olion, jonka kenttinä ovat kaikki eri “propsit”, jotka komponentin käyttäjä määrittelee.

Propsit määritellään seuraavasti:

const App = () => {
  return (
    <div>
      <h1>Greetings</h1>
      <Hello name="Arto" />
      <Hello name="Pekka" />
    </div>
  )
}

Propseja voi olla mielivaltainen määrä ja niiden arvot voivat olla “kovakoodattuja” merkkijonoja tai Javascript-lausekkeiden tuloksia. Jos propsin arvo muodostetaan Javascriptillä, tulee se olla aaltosulkeissa.

Muutetaan koodia siten, että komponentti Hello käyttää kahta propsia:

const Hello = (props) => {
  return (
    <div>
      <p>Hello {props.name}, you are {props.age} years old</p>
    </div>
  )
}

const App = () => {
  const nimi = 'Pekka'
  const ika = 10
  return (
    <div>
      <h1>Greetings</h1>
      <Hello name="Arto" age={26 + 10} />
      <Hello name={nimi} age={ika} />
    </div>
  )
}

Komponentti App lähettää propseina muuttujan arvoja, summalausekkeen evaluoinnin tuloksen ja normaalin merkkijonon.

Muutama huomio

React on konfiguroitu antamaan varsin hyviä virheilmoituksia. Kannattaa kuitenkin edetä ainakin alussa todella pienin askelin ja varmistaa, että jokainen muutos toimii halutulla tavalla.

Konsolin tulee olla koko ajan auki. Jos selain ilmoittaa virheestä, ei kannata kirjoittaa sokeasti lisää koodia ja toivoa ihmettä tapahtuvaksi, vaan tulee yrittää ymmärtää virheen syy ja esim. palata edelliseen toimivaan tilaan:

Kannattaa myös muistaa, että React-koodissakin on mahdollista ja kannattavaa lisätä koodin sekaan sopivia konsoliin tulostavia console.log()-komentoja. Tulemme hieman myöhemmin tutustumaan muutamiin muihinkin tapoihin debugata Reactia.

Kannattaa pitää mielessä, että React-komponenttien nimien tulee alkaa isolla kirjaimella. Jos yrität määritellä komponentin seuraavasti:

const footer = () => {
  return (
    <div>greeting app created by <a href="https://github.com/mluukkai">mluukkai</a></div>
  )
}

ja ottaa se käyttöön

const App = () => {
  return (
    <div>
      <h1>Greetings</h1>
      <Hello name="Arto" age={26 + 10} />
      <footer />
    </div>
  )
}

sivulle ei kuitenkaan ilmesty näkyviin Footer-komponentissa määriteltyä sisältöä, vaan React luo sivulle ainoastaan tyhjän footer-elementin. Jos muutat komponentin nimen alkamaan isolla kirjaimella, React luo sivulle div-elementin, joka määriteltiin Footer-komponentissa.

Kannattaa myös pitää mielessä, että React-komponentin sisällön tulee (yleensä) sisältää yksi juurielementti. Eli jos yrittäisimme määritellä komponentin App ilman uloimmaista div-elementtiä:

const App = () => {
  return (
    <h1>Greetings</h1>
    <Hello name="Arto" age={26 + 10} />
    <Footer />
  )
}

seurauksena on virheilmoitus:

Reactin versiosta 0.16 asti juurielementin käyttö ei ole ollut enää ainoa toimiva vaihtoehto, myös taulukollinen komponentteja on validi tapa:

const App = () => {
  return (
    [
      <h1>Greetings</h1>,
      <Hello name="Arto" age={26 + 10} />,
      <Footer />
    ]
  )
}

Määritellessä sovelluksen juurikomponenttia, tämä ei kuitenkaan ole järkevää ja näyttää koodissakin pahalta.

React-tehtävät, osa 1

Tee nyt tehtävät 1.1 ja 1.2

Javascriptiä

Kurssin aikana on websovelluskehityksen rinnalla tavoite ja tarve oppia riittävässä määrin Javascriptiä.

Javascript on kehittynyt viime vuosina nopeaan tahtiin, ja käytämme kurssilla kielen uusimpien versioiden piirteitä, joista osa ei ole vielä edes ehtinyt kielen viimeisimpään standardoituun versioon. Javascript-standardin virallinen nimi on ECMAScript. Tämän hetken tuorein versio on kesäkuussa 2017 julkaistu ES8, toiselta nimeltään ECMAScript 2017.

Selaimet eivät vielä osaa kaikkia Javascriptin uusimpien versioiden ominaisuuksia. Tämän takia selaimessa suoritetaan useimmiten koodia joka on käännetty (englanniksi transpiled) uudemmasta Javascriptin versiosta johonkin vanhempaan, laajemmin tuettuun versioon.

Tällä hetkellä johtava tapa tehdä transpilointi on Babel. Create-react-app:in avulla luoduissa React-sovelluksissa on valmiiksi konfiguroitu automaattinen transpilaus. Katsomme kurssin osassa 7 tarkemmin miten transpiloinnin konfigurointi tapahtuu.

Node.js on melkein missä vaan, mm. palvelimilla toimiva, Googlen chrome V8-javascriptmoottoriin perustuva Javascript-suoritusympäristö. Harjoitellaan hieman Javascriptiä Nodella. Tässä oletetaan, että koneellasi on Node.js:stä vähintään versio v8.6.0. Noden tuoreet versiot osaavat suoraan Javascriptin uusia versioita, joten koodin transpilaus ei ole tarpeen.

Koodi kirjoitetaan .js-päätteiseen tiedostoon, ja suoritetaan komennolla node tiedosto.js

Koodia on mahdollisuus kirjoittaa myös Node.js-konsoliin, joka aukeaa kun kirjoitat komentorivillä node tai myös selaimen developer toolin konsoliin. Chromen uusimmat versiot osaavat suoraan transpiloimatta melko hyvin Javascriptin uusiakin piirteitä.

Javascript muistuttaa nimensä ja syntaksinsa puolesta läheisesti Javaa. Perusmekanismeiltaan kielet kuitenkin poikkeavat radikaalisti. Java-taustalta tultaessa Javascriptin käyttäytyminen saattaa aiheuttaa hämmennystä, varsinkin jos kielen piirteistä ei viitsitä ottaa selvää.

Tietyissä piireissä on myös ollut suosittua yrittää “simuloida” Javascriptilla eräitä Javan piirteitä ja ohjelmointitapoja. En suosittele.

Muuttujat

Javascriptissä on muutama tapa määritellä muuttujia:

const x = 1
let y = 5

console.log(x, y)  // tulostuu 1, 5
y += 10
console.log(x, y)  // tulostuu 1, 15
y = 'teksti'
console.log(x, y)  // tulostuu 1, teksti
x = 4              // aiheuttaa virheen

const ei oikeastaan määrittele muuttujaa vaan vakion, jonka arvoa ei voi enää muuttaa. let taas määrittelee normaalin muuttujan.

Esimerkistä näemme myös, että muuttujan tallettaman tiedon tyyppi voi vaihtaa tyyppiä suorituksen aikana, y tallettaa aluksi luvun ja lopulta merkkijonon.

Javascriptissa on myös mahdollista määritellä muuttujia avainsanan var avulla. Var oli pitkään ainoa tapa muuttujien määrittelyyn, const ja let tulivat kieleen mukaan vasta versiossa ES6. Var toimii tietyissä tilanteissa eri tavalla kuin useimpien muiden kielien muuttujien määrittely. Tällä kurssilla varin käyttö ei ole suositeltavaa eli käytä aina const:ia tai let:iä!

Lisää aiheesta esim. youtubessa var, let and const - ES6 JavaScript Features

Taulukot

Taulukko ja muutama esimerkki sen käytöstä

const t = [1, -1, 3]

t.push(5)

console.log(t.length)  // tulostuu 4
console.log(t[1])      // tulostuu -1

t.forEach((luku) => {
  console.log(luku)    // tulostuu 1, -1, 3 ja 5 omille riveilleen
})

t[6] = 99

console.log(t)         // tulostuu [ 1, -1, 3, 5, <2 empty items>, 99 ]

Huomattavaa esimerkissä on se, että taulukon sisältöä voi muuttaa vaikka sen on määritelty const:ksi. Koska taulukko on olio, viittaa muuttuja koko ajan samaan olioon. Olion sisältö muuttuu sitä mukaa kuin taulukkoon lisätään uusia alkioita.

Eräs tapa käydä taulukon alkiot läpi on esimerkissä käytetty forEach, joka saa parametrikseen nuolisyntaksilla määritellyn funktion

(luku) => {
  console.log(luku)
}

forEach kutsuu funktiota jokaiselle taulukon alkiolle antaen taulukon alkion aina parametrina. forEachin parametrina oleva funktio voi saada myös muita parametreja.

Taulukoille on määritelty runsaasti hyödyllisiä operaatioita. Katsotaan pieni esimerkki operaation map käytöstä.

const t = [1, 2, 3, 4]

const m1 = t.map((luku) => luku * 2)
console.log(m1) // tulostuu [2, 4, 6, 8]

const m2 = t.map((luku) => '<li>' + luku + '</li>')
console.log(m2) // tulostuu [ '<li>1</li>', '<li>2</li>', '<li>3</li>', '<li>4</li>' ]

Map siis muodostaa taulukon perusteella uuden taulukon, jonka jokainen alkio muodostetaan map:in parametrina olevan funktion avulla. Kuten tulemme kurssin osassa2 näkemään, mapia käytetään Reactissa todella usein.

Taulukon yksittäisiä alkioita on helppo sijoittaa muuttujiin destrukturoivan sijoituslauseen avulla:

const t = [1, 2, 3, 4, 5]

const [eka, toka, ...loput] = t

console.log(eka, toka)      // tulostuu 1, 2
console.log(loput)          // tulostuu [3, 4 ,5]

Eli muuttujiin eka ja toka tulee sijoituksen ansiosta taulukon kaksi ensimmäistä lukua. Muuttujaan loput “kerätään” sijoituksesta jäljellejääneet luvut omaksi taulukoksi.

Oliot

Javasriptissa on muutama tapa määritellä olioita. Erittäin yleisesti käytetään olioliteraaleja, eli määritellään olio luettelemalla sen kentät (englanniksi property) aaltosulkeiden sisällä:

const olio1 = {
  nimi: 'Arto Hellas',
  ika: 35,
  koulutus: 'Filosofian tohtori'
}

const olio2 = {
  nimi: 'Full Stack -websovelluskehitys',
  taso: 'aineopinto',
  laajuus: 5
}

const olio3 = {
  nimi: {
    etunimi: 'Jami',
    sukunimi: 'Kousa'
  },
  arvosanat: [2, 3, 5, 3],
  laitos: 'TKTL'
}

Kenttien arvot voivat olla tyypiltään mitä vaan, lukuja, merkkijonoja, taulukoita, olioita…

Olioiden kenttiin viitataan pistenotaatiolla, tai hakasulkeilla:

console.log(olio1.nimi)          // tulostuu Arto Hellas
const kentanNimi = 'ika'
console.log(olio1[kentanNimi])   // tulostuu 35

Olioille voidaan lisätä kenttiä myös lennossa joko pistenotaation tai hakasulkeiden avulla:

olio1.osoite = 'Tapiola'
olio1['salainen numero'] = 12341

Jälkimmäinen lisäyksistä on pakko tehdä hakasulkeiden avulla, sillä pistenotaatiota käytettäessä ‘salainen numero’ ei kelpaa kentän nimeksi.

Javascriptissä olioilla voi luonnollisesti olla myös metodeja. Palaamme aiheeseen funktioiden käsittelyn jälkeen.

Olioita on myös mahdollista määritellä ns. konstruktorifunktioiden avulla, jolloin saadaan aikaan hieman monien ohjelmointikielten, esim. Javan luokkia (class) muistuttava mekanismi. Javascriptissä ei kuitenkaan ole luokkia samassa mielessä kuin olio-ohjelmointikielissä. Kieleen on kuitenkin lisätty versiosta ES6 alkaen luokkasyntaksi, joka helpottaa tietyissä tilanteissa olio-ohjelmointikielimäisten luokkien esittämistä. Palaamme asiaan hetken kuluttua.

Reactissa konstruktorifunktioihin perustuvalle olioiden määrittelylle ei ole kovin usein tarvetta, joten sivuutamme sen tällä kurssilla.

Funktiot

Olemme jo tutustuneet ns. nuolifunktioiden määrittelyyn. Täydellinen eli “pitkän kaavan” mukaan menevä tapa nuolifunktion määrittelyyn on seuraava

const summa = (p1, p2) => {
  console.log(p1)
  console.log(p2)
  return p1 + p2
}

ja funktiota kutsutaan kuten olettaa saattaa

const vastaus = summa(1,5)
console.log(vastaus)

Jos parameteja on vain yksi, voidaan sulut jättää määrittelystä pois:

const nelio = p => {
  console.log(p)
  return p * p
}

Jos funktio sisältää ainoastaan yhden lausekkeen, ei aaltosulkeita tarvita. Tällöin funktio palauttaa ainoan lausekkeensa arvon. Eli edellinen voitaisiin ilmaista lyhyemmin seuraavasti:

const nelio = p => p * p

Tämä muoto on erityisen kätevä käsiteltäessä taulukkoja esim. map-metodin avulla:

const t = [1, 2, 3]
const tnelio = t.map(p => p * p)
// tnelio on nyt [1, 4, 9]

Nuolifunktio on tullut Javascriptiin vasta muutama vuosi sitten version ES6 myötä. Tätä ennen ja paikoin nykyäänkin funktioiden määrittely tapahtui avainsanan function avulla.

Määrittelytapoja on kaksi, funktiolle voidaan antaa function declaration -tyyppisessä määrittelyssä nimi jonka avulla funktioon voidaan viitata:

function tulo(a, b) {
  return a * b
}

const vastaus = tulo(2, 6)

Toinen tapa on tehdä määrittely funktiolausekkeena. Tällöin funktiolle ei tarvitse antaa nimeä ja määrittely voi sijaita muun koodin seassa:

const keskiarvo = function(a, b) {
  return (a + b) / 2
}

const vastaus = keskiarvo(2, 5)

Tehtäviä javascriptistä

Tee nyt tehtävät 1.3-1.5

Olioiden metodit ja this

Kaikille kolmelle tavalle määritellä funktio on oma paikkansa.

Nuolifunktiot ja avainsanan function avulla määritellyt funktiot kuitenkin poikkeavat radikaalisti siitä miten ne käyttäytyvät avainsanan this suhteen.

Voimme liittää oliolle metodeja määrittelemällä niille kenttiä, jotka ovat funktioita:

const arto = {
  nimi: 'Arto Hellas',
  ika: 35,
  koulutus: 'Filosofian tohtori',
  tervehdi: function () {
    console.log('hello, my name is', this.nimi)
  }
}

arto.tervehdi()  // tulostuu hello, my name is Arto Hellas

metodeja voidaan liittää olioille myös niiden luomisen jälkeen:

const arto = {
  nimi: 'Arto Hellas',
  ika: 35,
  koulutus: 'Filosofian tohtori',
  tervehdi: function () {
    console.log('hello, my name is', this.nimi)
  }
}

arto.vanhene = function() {
  this.ika += 1
}

console.log(arto.ika)  // tulostuu 35
arto.vanhene()
console.log(arto.ika)  // tulostuu 36

Muutetaan oliota hiukan

const arto = {
  nimi: 'Arto Hellas',
  tervehdi: function () {
    console.log('hello, my name is', this.nimi)
  },
  laskeSumma: function (a, b) {
    console.log(a + b)
  }
}

arto.laskeSumma(1, 4)   // tulostuu 5

const viiteSummaan = arto.laskeSumma
viiteSummaan(10, 15)   // tulostuu 25

Oliolla on nyt metodi laskeSumma, joka osaa laskea parametrina annettujen lukujen summan. Metodia voidaan kutsua normaaliin tapaan olion kautta arto.laskeSumma(1, 4) tai tallettamalla metodiviite muuttujaan ja kutsumalla metodia muuttujan kautta viiteSummaan(10, 15).

Jos yritämme samaa metodille tervehdi, aiheutuu ongelmia:

const arto = {
  nimi: 'Arto Hellas',
  tervehdi: function () {
    console.log('hello, my name is', this.nimi)
  },
  laskeSumma: function (a, b) {
    console.log(a + b)
  }
}

arto.tervehdi()        // tulostuu hello, my name is Arto Hellas

const viiteTervehdykseen = arto.tervehdi
viiteTervehdykseen()   // tulostuu hello, my name is undefined

Kutsuttaessa metodia viitteen kautta, on metodi kadottanut tiedon siitä mikä oli alkuperäinen this. Toisin kuin melkein kaikissa muissa kielissä, Javascriptissa this:n arvo määrittyy sen mukaan miten metodia on kutsuttu. Kutsuttaessa metodia viitteen kautta, this:in arvoksi tulee ns. globaali objekti ja lopputulos ei ole yleensä ollenkaan se, mitä sovelluskehittäjä olettaa.

This:in kadottaminen aiheuttaa Reactilla ja Node.js:lla ohjelmoidessa monia potentiaalisia ongelmia. Eteen tulee erittäin usein tilanteita, missä Reactin/Noden (oikeammin ilmaistuna selaimen Javascript-moottorin) tulee kutsua joitain käyttäjän määrittelemien olioiden metodeja. Tälläinen tilanne tulee esim. jos pyydetään Artoa tervehtimään sekunnin kuluttua metodia setTimeout hyväksikäyttäen.

const arto = {
  nimi: 'Arto Hellas',
  tervehdi: function () {
    console.log('hello, my name is', this.nimi)
  }
}

setTimeout(arto.tervehdi, 1000)

Javascriptissa this:in arvo siis määräytyy siitä miten metodia on kutsuttu. setTimeoutia käytettäessä metodia kutsuu Javascript-moottori ja this viittaa Timeout-olioon.

On useita mekanismeja, joiden avulla alkuperäinen this voidaan säilyttää, eräs näistä on metodin bind käyttö:

setTimeout(arto.tervehdi.bind(arto), 1000)

Komento arto.tervehdi.bind(arto) luo uuden funktion, missä se on sitonut this:in tarkoittamaan Artoa riippumatta siitä missä ja miten metodia kutsutaan.

Nuolifunktioiden avulla on mahdollista ratkaista eräitä this:iin liittyviä ongelmia. Olioiden metodeina niitä ei kuitenkaan kannata käyttää, sillä silloin this ei toimi ollenkaan. Palaamme nuolifunktioiden this:in käyttäytymiseen myöhemmin.

Jos haluat ymmärtää paremmin javascriptin this:in toimintaa, löytyy internetistä runsaasti materiaalia aiheesta. Esim. egghead.io:n 20 minuutin screencastsarja Understand JavaScript’s this Keyword in Depth on erittäin suositeltava!

Luokat

Kuten aiemmin mainittiin, Javascriptissä ei ole olemassa olio-ohjelmointikielten luokkamekanismia. Javascriptissa on kuitenkin ominaisuuksia, jotka mahdollistavat olio-ohjelmoinnin luokkien “simuloinnin”. Emme mene nyt sen tarkemmin Javascriptin olioiden taustalla olevaan prototyyppiperintämekanismiin.

Tutustumme kuitenkin pikaisesti ES6:n myötä Javascriptiin tulleeseen luokkasyntaksiin, joka helpottaa oleellisesti luokkien (tai luokan kaltaisten asioiden) määrittelyä Javascriptissa.

Seuraavassa on määritelty “luokka” Henkilö ja sille kaksi Henkilö-oliota:

class Henkilo {
  constructor(nimi, ika) {
    this.nimi = nimi
    this.ika = ika
  }
  tervehdi() {
    console.log('hello, my name is', this.nimi)
  }
}

const arto = new Henkilo('Arto Hellas', 35)
arto.tervehdi()

const jami = new Henkilo('Jami Kousa', 21)
jami.tervehdi()

Syntaksin osalta luokat ja niistä luodut oliot muistuttavat erittäin paljon esim. Javan olioita. Käyttäytymiseltäänkin ne ovat aika lähellä Javan olioita. Perimmiltään kyseessä on kuitenkin edelleen Javascriptin prototyyppiperintään perustuvista olioista. Molempien olioiden todellinen tyyppi on Object sillä Javascriptissä ei perimmiltään ole muita tyyppejä kuin Boolean, Null, Undefined, Number, String, Symbol ja Object

Luokkasyntaksin tuominen Javascriptiin on osin kiistelty lisäys, ks. esim. Not Awesome: ES6 Classes tai Is “Class” In ES6 The New “Bad” Part?

ES6:n luokkasyntaksia käytetään kuitenkin paljon Reactissa ja Node.js:ssä ja siksi mekin käytämme sitä sopivissa määrin. Olio-ohjelmointimainen luokkahierarkioiden luominen ei kuitenkaan ole Reactin eikä tämän kurssin suositeltavan hengen mukaista. Reactia ohjelmoitaessa pyritään enemmän funktionaaliseen ohjelmointityyliin.

Javascript-materiaalia

Javascriptistä löytyy verkosta suuret määrät sekä hyvää että huonoa materiaalia. Tällä sivulla lähes kaikki Javascriptin ominaisuuksia käsittelevät linkit ovat Mozillan javascript -materiaaliin.

Mozillan sivuilta kannattaa lukea oikeastaan välittömästi A re-introduction to JavaScript (JS tutorial).

Jos haluat tutustua todella syvällisesti Javascriptiin, löytyy internetistä ilmaiseksi mainio kirjasarja You-Dont-Know-JS

egghead.io:lla on tarjolla runsaasti laadukkaita screencasteja Javascriptista, Reactista ym. kiinnostavasta. Valitettavasti materiaali on osittain maksullista.

Paluu Reactin äärelle

Palataan jälleen Reactin pariin.

Aiemmassa esimerkissämme käytimme funktionaalisia komponentteja, eli määrittelimme kaikki komponentit nuolifunktioiden avulla, esim:

const Hello = (props) => {
  return (
    <div>
      <p>Hello {props.name}, you are {props.age} years old</p>
    </div>
  )
}

Toinen tapa komponenttien määrittelyyn on käyttää luokkasyntaksia. Tällöin komponentti määritellään luokaksi, joka perii React.Component-luokan.

Muutetaan esimerkkisovelluksen komponentti Hello -luokaksi seuraavasti:

class Hello extends React.Component {
  render() {
    return (
      <div>
        <p>Hello {this.props.name}, you are {this.props.age} years old</p>
      </div>
    )
  }
}

Luokkakomponenttien tulee määritellä ainakin metodi render, joka palauttaa komponentin ulkoasun määrittelevät React-elementit eli käytännössä JSX:n.

Luokkakomponentissa viitataan komponentin propseihin this-viitteen kautta. Eli koska komponenttia käytetään seuraavasti

<Hello name="Arto" age={36} />

päästään nimeen ja ikään käsiksi luokkamuotoisen komponentin sisällä viittaamalla this.props.name ja this.props.age. Huomaa ero funktionaaliseen komponenttiin!

Luokkakomponenteille voidaan tarvittaessa määritellä muitakin metodeja ja “oliomuuttujia”, eli kenttiä.

Voisimme esim. määritellä metodin seuraavasti:

class Hello extends React.Component {
  bornYear() {
    const yearNow = new Date().getFullYear()
    return yearNow - this.props.age
  }
  render() {
    return (
      <div>
        <p>
          Hello {this.props.name}, you are {this.props.age} years old <br />
          So you were probably born {this.bornYear()}
        </p>
      </div>
    )
  }
}

Metodia kutsutaan render:in sisältä käyttäen this-viitettä syntaksilla this.bornYear().

Tässä tilanteessa ei kuitenkaan ole varsinaisesti mitään hyötyä määritellä apufunktiota bornYear metodiksi, joten parempi olisi määritellä se metodin render sisäisenä apumetodina:

class Hello extends React.Component {
  render() {
    const bornYear = () => {
      const yearNow = new Date().getFullYear()
      return yearNow - this.props.age
    }

    return (
      <div>
        <p>
          Hello {this.props.name}, you are {this.props.age} years old <br />
          So you were probably born {bornYear()}
        </p>
      </div>
    )
  }
}

Huomaa, että nyt metodia ei kutsuta viitteen this kautta, vaan syntaksilla bornYear(), sillä metodi ei ole komponentin eli this:in tasolla määritelty. Metodia bornYear ei nyt voi kutsua mistään muualta kuin metodin render sisältä, sillä se ei näy renderin ulkopuolelle.

Ennen kuin siirrymme eteenpäin, tarkastellaan erästä pientä, mutta käyttökelpoista ES6:n mukanaan tuomaa uutta piirrettä Javascriptissä, eli sijoittamisen yhteydessä tapahtuvaa destrukturointia.

Jouduimme äskeisessä koodissa viittaamaan propseina välitettyyn dataan hieman ikävästi muodossa this.props.name ja this.props.age. Näistä this.props.age pitää toistaa metodissa render kahteen kertaan.

Koska this.props on nyt olio

this.props = {
  name: 'Arto Hellas',
  age: 35
}

voimme suoraviivaistaa metodia render siten, että sijoitamme kenttien arvot muuttujiin name ja age jonka jälkeen niitä on mahdollista käyttää koodissa suoraan:

render() {
  const name = this.props.name
  const age = this.props.age
  const bornYear = () => new Date().getFullYear() - age

  return (
    <div>
      <p>
        Hello {name}, you are {age} years old <br />
        So you were probably born {bornYear()}
      </p>
    </div>
  )
}

Huomaa, että olemme myös hyödyntäneet nuolifunktion kompaktimpaa kirjoitustapaa metodin bornYear määrittelyssä.

Destrukturointi tekee asian vielä helpommaksi, sen avulla voimme “kerätä” olion oliomuuttujien arvot suoraan omiin yksittäisiin muuttujiin:

class Hello extends React.Component {
  render() {
    const {name, age} = this.props
    const bornYear = () => new Date().getFullYear() - age

    return (
      <div>
        <p>
          Hello {name}, you are {age} years old <br />
          So you were probably born {bornYear()}
        </p>
      </div>
    )
  }
}

Eli koska

this.props = {
  name: 'Arto Hellas',
  age: 35
}

saa const {name, age} = this.props aikaan sen, että name saa arvon ‘Arto Hellas’ ja age arvon 35.

Komponentti Hello on oikeastaan luonteeltaan sellainen, että sitä ei ole järkevää määritellä luokkasyntaksilla. Reactin best practice onkin käyttää funktioiden avulla määriteltyjä komponentteja aina kuin mahdollista.

Sivun uudelleenrenderöinti

Toistaiseksi tekemämme sovellukset ovat olleet sellaisia, että kun niiden komponentit on kerran renderöity, niiden ulkoasua ei ole enää voinut muuttaa. Entä jos haluaisimme toteuttaa laskurin, jonka arvo kasvaa esim. ajan kuluessa tai nappien painallusten yhteydessä?

Aloitetaan seuraavasta rungosta:

const App = (props) => {
  const {counter} = props
  return (
    <div>{counter.value}</div>
  )
}

const counter = {
  value: 1
}

ReactDOM.render(
  <App counter={counter} />,
  document.getElementById('root')
)

Sovelluksen juurikomponentille siis annetaan viite laskuriin. Juurikomponentti renderöi arvon ruudulle. Entä laskurin arvon muuttuessa? Jos lisäämme ohjelmaan esim. komennon

counter.value += 1

ei komponenttia kuitenkaan renderöidä uudelleen. Voimme saada komponentin uudelleenrenderöitymään kutsumalla uudelleen metodia ReactDOM.render, esim. seuraavasti

const App = (props) => {
  const {counter} = props
  return (
    <div>{counter.value}</div>
  )
}

const counter = {
  value: 1
}

const renderoi = () => {
  ReactDOM.render(
    <App counter={counter} />,
    document.getElementById('root')
  )
}

renderoi()
counter.value += 1
renderoi()
counter.value += 1
renderoi()

Copypastea vähentämään on komponentin renderöinti kääritty funktioon renderoi.

Nyt komponentti renderöityy kolme kertaa, saaden ensin arvon 1, sitten 2 ja lopulta 3. 1 ja 2 tosin ovat ruudulla niin vähän aikaa, että niitä ei ehdi havaita.

Hieman mielenkiintoisempaan toiminnallisuuteen pääsemme tekemällä renderöinnin ja laskurin kasvatuksen toistuvasti sekunnin välein käyttäen SetInterval:

setInterval(() => {
  renderoi()
  counter.value += 1;
}, 1000)

ReactDOM.render-metodin toistuva kutsuminen ei kuitenkaan ole suositeltu tapa päivittää komponentteja. Tutustutaan seuraavaksi järkevämpään tapaan.

Tilallinen komponentti

Muutetaan esimerkkisovelluksen komponentti App luokkaperustaiseksi:

class App extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      counter: 1
    }
  }

  render() {
    return (
      <div>{this.state.counter}</div>
    )
  }
}

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

Komponentilla on nyt metodin render lisäksi konstruktori. Komponentin konstruktori saa parametrikseen sille välitettävät muuttujat parametrin props välityksellä, konstruktorin ensimmäisen rivin on oltava kutsu super(props).

Luokkiin perustuvilla komponenteilla voi olla tila, joka talletetaan muuttujaan state.

Konstruktori määrittelee komponentin alkutilan olevan:

{
  counter: 1
}

Eli tila sisältää kentän counter, jonka arvo on 1. React-komponenttien tilaa, eli muuttujaa this.state ei saa päivittää suoraan, tilan päivitys on tehtävä aina funktion setState avulla. Metodin kutsuminen päivittää tilan ja aiheuttaa komponentin uuden renderöinnin (ellei sitä ole estetty myöhemmin esiteltävällä tavalla). Uudelleenrenderöinnin yhteydessä myös kaikki komponentin sisältämät alikomponentit renderöidään.

Muutetaan komponenttia App siten, että konstruktorissa käynnistetään ajastin, joka kutsuu funktiota setState toistuvasti sekunnin välein korottaen laskurin arvoa aina yhdellä:

class App extends React.Component {
  constructor() {
    super()
    this.state = {
      counter: 1
    }

    setInterval(() => {
      this.setState({ counter: this.state.counter + 1 })
    }, 1000)
  }
  render() {
    return (
      <div>{this.state.counter}</div>
    )
  }
}

Ruudulle renderöity laskurin arvo päivittyy sillä aina komponentin tilan muuttuessa React kutsuu komponentin metodia render.

Jos komponentti ei renderöidy vaikka sen omasta mielestä pitäisi, tai se renderöityy “väärään aikaan”, debuggaamista auttaa joskus metodiin render liitetty konsoliin tulostus. Esim. jos lisäämme koodiin seuraavan,

class App extends React.Component {
  // ...
  render() {
    console.log('renderöidään', this.state.counter)
    return (
      <div>{this.state.counter}</div>
    )
  }
}

on konsolista helppo seurata metodin render kutsuja:

Tapahtumankäsittely

Mainitsimme jo osassa 0 muutamaan kertaan tapahtumankäsittelijät, eli funktiot, jotka on rekisteröity kutsuttavaksi tiettyjen tapahtumien eli eventien yhteydessä. Esim. käyttäjän interaktio sivun elementtien kanssa aiheuttaa joukon erinäisiä tapahtumia.

Muutetaan sovellusta siten, että laskurin kasvaminen tapahtuukin käyttäjän painaessa button-elementin avulla toteutettua nappia.

Button-elementit tukevat mm. hiiritapahtumia (mouse events), joista yleisin on click.

Reactissa funktion rekisteröiminen tapahtumankäsittelijäksi tapahtumalle click tapahtuu seuraavasti:

const funktio = () => { /* koodi */ }

//...

<button onClick={funktio}>
  plus
</button>

Eli laitetaan button:in onClick-attribuutin arvoksi aaltosulkeissa oleva viite koodissa määriteltyyn funktioon.

Tapahtumankäsittelijäfunktio voidaan määritellä suoraan onClick-määrittelyn yhteydessä:

class App extends React.Component {
  constructor() {
    super()
    this.state = {
      counter: 1
    }
  }

  render() {
    return (
      <div>
        <div>{this.state.counter}</div>
        <button onClick={() => console.log('clicked')}>
          plus
        </button>
      </div>
    )
  }
}

Nyt jokainen napin plus painallus tulostaa konsoliin clicked.

Muuttamalla tapahtumankäsittelijä seuraavaan muotoon

<button onClick={() => this.setState({ counter: this.state.counter + 1 })}>
  plus
</button>

saamme halutun toiminnallisuuden.

Lisätään sovellukseen myös nappi laskurin nollaamiseen:

class App extends React.Component {
  constructor() {
    super()
    this.state = {
      counter: 1
    }
  }

  render() {
    return (
      <div>
        <div>{this.state.counter}</div>
        <div>
          <button onClick={() => this.setState({ counter: this.state.counter + 1 })}>
            plus
          </button>
          <button onClick={() => this.setState({ counter: 0 })}>
            zero
          </button>
        </div>
      </div>
    )
  }
}

Sovelluksemme on valmis!

Metodien käyttö ja this

Tapahtumankäsittelijöiden määrittely suoraan JSX-templatejen sisällä ei useimmiten ole kovin viisasta. Eriytetään nappien tapahtumankäsittelijät omiksi metodeikseen:

class App extends React.Component {
  constructor() {
    super()
    this.state = {
      counter: 1
    }
  }

  kasvataYhdella() {
    this.setState({ counter: this.state.counter + 1 })
  }

  nollaa() {
    this.setState({ counter: 0 })
  }

  render() {
    return (
      <div>
        <div>{this.state.counter}</div>
        <div>
          <button onClick={this.kasvataYhdella}>
            plus
          </button>
          <button onClick={this.nollaa}>
            zero
          </button>
        </div>
      </div>
    )
  }
}

Komponentin määrittelemälle luokalle on nyt lisätty metodit kasvataYhdella ja nollaa. Metodeihin viitataan nappeja vastaavista React-elementeistä:

<button onClick={this.kasvataYhdella}>

Kun testaamme nyt sovellusta, törmäämme ongelmaan. Virheilmoitus on erittäin hyvä:

Eli törmäämme jo aiemmin mainittuun ongelmaan alkuperäisen this:in kadottamisesta.

Kun selaimen Javascriptin runtime kutsuu takaisinkutsufunktiota, this ei enää viittaa komponenttiin App vaan on arvoltaan undefined eli määrittelemätön:

Ongelmaan on useita erilaisia ratkaisuja. Eräs näistä on jo aiemmin mainittu bindaaminen, eli esim. komennolla this.kasvataYhdella.bind(this) voimme muodostaa uuden funktion, jonka koodi on alkuperäisen funktion koodi, missä this on sidottu viittaamaan parametrina olevaan arvoon, eli komponenttiin itseensä.

Eli sovellus toimii taas jos koodi muutetaan muotoon:

<button onClick={this.kasvataYhdella.bind(this)}>
  plus
</button>
<button onClick={this.nollaa.bind(this)}>
  zero
</button>

Jos samaa metodia joudutaan kutsumaan useasta kohtaa koodia, on hieman ikävää kirjoittaa toistuvasti metodin bindattu muoto React-elementtien sekaan.

Yksi mahdollisuus onkin suorittaa bindaukset konstruktorissa:

class App extends React.Component {
  constructor() {
    super()
    this.state = {
      counter: 1
    }
    this.kasvataYhdella = this.kasvataYhdella.bind(this)
    this.nollaa = this.nollaa.bind(this)
  }

Nyt riittää viitata metodeihin “normaalisti”, ilman bindiä:

<button onClick={this.kasvataYhdella}>
  plus
</button>
<button onClick={this.nollaa}>
  zero
</button>

Teknisesti ottaen konstruktorissa korvataan kenttään kasvataYhdella alunperin määritelty metodi uudella metodilla, jolla on alkuperäisen metodin koodi siten, että this on pysyvästi bindattu komponenttiin.

Ehkä paras ratkaisu this-ongelman estämiseen on käyttää tulevaan Javascript-standardiin ehdotettua class properties -ominaisuutta, jonka avulla voimme määritellä this:in suhteen hyvin käyttäytyviä metodeja seuraavasti:

class App extends React.Component {
  constructor() {
    super()
    this.state = {
      counter: 1
    }
  }

  kasvataYhdella = () => {
    this.setState({ counter: this.state.counter + 1 })
  }

  nollaa = () => {
    this.setState({ counter: 0 })
  }

  render() {
    // ...
  }

Näin jokainen App-komponentti saa kentät kasvataYhdella ja nollaa jotka ovat funktioita, joiden this on sidottu komponenttiin riippumatta siitä miten ja kenen toimesta metodia kutsutaan.

Syy miksi nuolifunktiolla määritelty metodi toimii this:in suhteen samaan tapaan kuin esim. Javassa, on se, että nuolifunktioilla on ns. leksikaalinen (lexical) this, eli nuolifunktion this määräytyy sen määrittelykontekstin this:in mukaan. Kun metodi määritellään class propertynä, on määrittelykontekstina App-komponentti. Tarkempaa selitystä esim. täällä.

Käytämme kurssilla jatkossa tätä tapaa komponenttien metodien määrittelemiseen.

class propertyt siis eivät ole vielä mukana uusimmassa javascript-standardissa eli kesäkuussa 2017 ilmestyneessä ES8:ssa. Voimme kuitenkin käyttää ominaisuutta create-react-app:illa luoduissa sovelluksissa, sillä babel osaa kääntää (eli transpiloida) ominaisuuden selainten ymmärtämään muotoon.

Node.js ei oletusarvoisesti vielä tue ominaisuutta, eli kääntämätöntä koodia joka sisältää class propertyjä ei voi vielä suorittaa Node.js:llä.

Pari huomiota funktion setState käytöstä

Käytimme metodia setState kahteen kertaan:

kasvataYhdella = () => {
  this.setState({ counter: this.state.counter + 1 })
}

nollaa = () => {
  this.setState({ counter: 0 })
}

Näistä ensimmäinen tapa this.setState({ counter: this.state.counter + 1 }) ei ole kaikissa tilanteissa suositeltava, sillä React ei takaa että metodin setState kutsut tapahtuvat siinä järjestyksessä missä ne on kirjoitettu koodiin.

Jos halutaan määritellä uusi tila olemassaolevan tilan perusteella, on varmempi kutsua setState:a seuraavasti:

this.setState((prevState) => ({
  counter: prevState.counter + 1
}));

Nyt metodin parametrina on funktio, jonka parametrina on edellinen tila prevState ja tilan päivitys tapahtuu varmuudella kutsuhetken edellisen tilan perusteella.

Emme nyt viitsi käyttää tätä monimutkaisempaa muotoa, sillä emme välitä vaikka sovelluksessamme ilmenisikin silloin tällöin pieni epäkonsistenssi (on epäselvää olisiko se sovelluksessamme edes teoriassa mahdollista).

Asia tulee kuitenkin ehdottomasti pitää mielessä, setState:n vääränlainen käyttö saattaa aiheuttaa hankalasti löydettäviä, harvoin toistuvia bugeja.

Tärkeä mielessä pidettävä seikka on myös se, että React kutsuu funktiota setState asynkroonisesti, eli jos meillä on seuraava koodi

console.log(this.state.counter)
this.setState({counter: 55})
console.log(this.state.counter)

tulostavat molemmat rivit saman arvon sillä Reactin tila ei saa uutta arvoa heti komennon this.setState jälkeen, vaan vasta sitten, kun suorituksen alla oleva metodi on suoritettu loppuun ja setState on saanut mahdollisuuden suoritukselle.

Funktio joka palauttaa funktion

Metodit kasvataYhdella ja nollaa toimivat melkein samalla tavalla, ne asettavat uuden arvon laskurille. Tehdään koodiin yksittäinen metodi, joka sopii molempiin käyttötarkoituksiin:

asetaArvoon = (arvo) => {
  this.setState({ counter: arvo })
}

render() {
  //...
  <button onClick={this.asetaArvoon(this.state.counter+1)}>
    Plus
  </button>
  <button onClick={this.asetaArvoon(0)}>
    Zero
  </button>
  //...
}

Huomaamme kuitenkin että muutos hajottaa sovelluksemme täysin:

Mistä on kyse? Tapahtumankäsittelijäksi on tarkoitus määritellä viite funktioon. Kun koodissa on

<button onClick={this.asetaArvoon(0)}>

tapahtumankäsittelijäksi tulee määriteltyä funktiokutsu. Sekin on monissa tilanteissa ok, mutta ei nyt, nimittäin kun React suorittaa metodin render, se suorittaa kutsun this.asetaArvoon(0). Kutsu aiheuttaa metodin setState kutsun. Tämä taas aiheuttaa uuden render-kutsun jne…

Tässä tilanteessa meidän onkin käytettävä yleistä Javascriptin ja yleisemminkin funktionaalisen ohjelmoinnin kikkaa, eli määritellä funktio joka palauttaa funktion:

class App extends React.Component {
  constructor() {
    super()
    this.state = {
      counter: 1
    }
  }

  asetaArvoon = (arvo) => {
    return () => {
      this.setState({ counter: arvo })
    }
  }

  render() {
    return (
      <div>
        <div>{this.state.counter}</div>
        <div>
          <button onClick={this.asetaArvoon(this.state.counter + 1)}>
            Plus
          </button>
          <button onClick={this.asetaArvoon(0)}>
            Zero
          </button>
        </div>
      </div>
    )
  }
}

Jos et ole aiemmin törmännyt tekniikkaan, siihen totutteluun voi mennä tovi.

Olemme siis määritelleet komponentin metodin asetaArvoon seuraavasti:

asetaArvoon = (arvo) => {
  return () => {
    this.setState({ counter: arvo })
  }
}

Kun render-metodissa määritellään tapahtumankäsittelijä kutsumalla this.asetaArvoon(0), on lopputuloksena

() => {
  this.setState({ counter: 0 })
}

eli juuri oikeanlainen tilan nollaamisen aiheuttava funktio!

Plus-napin tapahtumankäsittelijä määritellään kutsumalla this.asetaArvoon(this.state.counter + 1). Kun komponentti renderöidään ensimmäisen kerran, this.state.counter on saanut konstruktorissa arvon 1, eli plus-napin tapahtumankäsittelijäksi tulee metodikutsun this.asetaArvoon(1 + 1) tulos, eli funktio

() => {
  this.setState({ counter: 2 })
}

Vastaavasti, kun laskurin tila on esim 41, tulee plus-napin tapahtumakuuntelijaksi

() => {
  this.setState({ counter: 42 })
}

Tarkastellaan vielä hieman metodia asetaArvoon:

asetaArvoon = (arvo) => {
  return () => {
    this.setState({ counter: arvo })
  }
}

Koska metodi itse sisältää ainoastaan yhden komennon, eli returnin, joka palauttaa funktion, voidaan hyödyntää nuolifunktion tiiviimpää muotoa:

asetaArvoon = (arvo) =>
  () => {
    this.setState({ counter: arvo })
  }

Usein tälläisissä tilanteissa kaikki kirjoitetaan samalle riville, jolloin tuloksena on “kaksi nuolta sisältävä funktio”:

asetaArvoon = (arvo) => () => this.setState({ counter: arvo })

Kaksinuolisen funktion voi ajatella funktiona, jota lopullisen tuloksen saadakseen täytyy kutsua kaksi kertaa.

Ensimmäisellä kutsulla “konfiguroidaan” varsinainen funktio, sijoittamalla osalle parametreista arvo. Eli kutsu asetaArvoon(5) sitoo muuttujaan arvo arvon 5 ja funktiosta “jää jäljelle” seuraava funktio:

() => this.setState({ counter: 5 })

Tässä näytetty tapa soveltaa funktioita palauttavia funktioita on oleellisesti sama asia mistä funktionaalisessa ohjelmoinnissa käytetään termiä currying. Termi currying ei ole lähtöisin funktionaalisen ohjelmoinnin piiristä vaan sillä on juuret syvällä matematiikassa.

Jo muutamaan kertaan mainittu termi funktionaalinen ohjelmointi ei ole välttämättä kaikille tässä vaiheessa tuttu. Asiaa avataan hiukan kurssin kuluessa, sillä React tukee ja osin edellyttää funktionaalisen tyylin käyttöä.

HUOM: muutos, missä korvasimme metodit kasvataArvoa ja nollaa metodilla asetaArvoon ei välttämättä ole järkevä, sillä erikoistuneemmat metodit ovat paremmin nimettyjä. Teimme muutoksen oikeastaan ainoastaan demonstroidaksemme currying-tekniikan soveltamista.

Tilan vieminen alikomponenttiin

Reactissa suositaan pieniä komponentteja, joita on mahdollista uusiokäyttää monessa osissa sovellusta ja jopa useissa eri sovelluksissa. Refaktoroidaan koodiamme vielä siten, että yhden komponentin sijaan koostamme laskurin näytöstä ja kahdesta painikkeesta.

Tehdään ensin näytöstä vastaava komponentti Display.

Reactissa parhaana käytänteenä on sijoittaa tila mahdollisimman ylös komponenttihierarkiassa, mielellään sovelluksen juurikomponenttiin.

Jätetään sovelluksen tila, eli laskimen arvo komponenttiin App ja välitetään tila props:ien avulla komponentille Display:

const Display = (props) => {
  return (
    <div>{props.counter}</div>
  )
}

Kyseessä on siis todella yksinkertainen komponentti joka kannattaa ehdottomasti määritellä funktion avulla eli funktionaalisena komponenttina.

Voimme hyödyntää aiemmin mainittua destrukturointia myös metodien parametreissa. Eli koska olemme kiinnostuneita props:in kentästä counter, on edellinen mahdollista yksinkertaistaa seuraavaan muotoon:

const Display = ({ counter }) => {
  return (
    <div>{counter}</div>
  )
}

Koska komponentin määrittelevä metodi ei sisällä muuta kuin returnin, voimme ilmaista sen hyödyntäen nuolifunktioiden tiiviimpää ilmaisumuotoa

const Display = ({ counter }) => <div>{counter}</div>

Komponentin käyttö on suoraviivaista, riittää että sille välitetään laskurin tila eli this.state.counter:

class App extends React.Component {
  // ...
  render() {
    return (
      <div>
        <Display counter={this.state.counter}/>
        <div>
          <button onClick={this.asetaArvoon(this.state.counter+1)}>
            Plus
          </button>
          <button onClick={this.asetaArvoon(0)}>
            Zero
          </button>
        </div>
      </div>
    )
  }
}

Kaikki toimii edelleen. Kun nappeja painetaan ja App renderöityy uudelleen, renderöityvät myös kaikki sen alikomponentit, siis myös Display automaattisesti uudelleen.

Tehdään seuraavaksi napeille tarkoitettu komponentti Button. Napille on välitettävä propsien avulla tapahtumankäsittelijä sekä napin teksti:

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

ja hyödynnetään taas destrukturointia ottamaan props:in tarpeelliset kentät suoraan:

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

Komponentin App metodi render muuttuu nyt muotoon:

render() {
  return (
    <div>
      <Display counter={this.state.counter}/>
      <div>
        <Button
          handleClick={this.asetaArvoon(this.state.counter + 1)}
          text="Plus"
        />
        <Button
          handleClick={this.asetaArvoon(this.state.counter - 1)}
          text="Minus"
        />
        <Button
          handleClick={this.asetaArvoon(0)}
          text="Zero"
        />
      </div>
    </div>
  )
}

Koska meillä on nyt uudelleenkäytettävä nappi, sovellukselle on lisätty uutena toiminnallisuutena nappi, jolla laskurin arvoa voi vähentää.

Tapahtumankäsittelijä välitetään napeille propsin handleClick välityksellä. Propsin nimellä ei ole sinänsä merkitystä, mutta valinta ei ollut täysin sattumanvarainen, esim. Reactin tutoriaali suosittelee tätä konventiota.

Monimutkaisemman tilan päivittäminen

Tarkastellaan sovellusta, jonka tila on hieman monimutkaisempi:

class App extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      vasen: 0,
      oikea: 0
    }
  }

  klikVasen = () => {
    this.setState({
      vasen: this.state.vasen + 1
    })
  }

  klikOikea = () => {
    this.setState({
      oikea: this.state.oikea + 1
    })
  }

  render() {
    return (
      <div>
        <div>
          {this.state.vasen}
          <button onClick={this.klikVasen}>vasen</button>
          <button onClick={this.klikOikea}>oikea</button>
          {this.state.oikea}
        </div>
      </div>
    )
  }
}

Tilassa on siis kaksi kenttää, vasen ja oikea jotka laskevat vastaavien nappien painalluksia.

Kun tilaa päivitetään, riittää asettaa ainoastaan muuttuvan kentän arvo, sillä React lomittaa tai “mergeää” muutokset olemassaolevaan tilaan.

Eli kun päivitämme esim. vasemman napin painalluksia, riittää seuraava koodi

klikVasen = () => {
  this.setState({
    vasen: this.state.vasen + 1
  })
}

tilassa oleva kenttä oikea jää muutoksen yhteydessä ennalleen.

Taulukon käsittelyä

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

class App extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      vasen: 0,
      oikea: 0,
      kaikki: []
    }
  }

  klikVasen = () => {
    this.setState({
      vasen: this.state.vasen + 1,
      kaikki: this.state.kaikki.concat('v')
    })
  }

  klikOikea = () => {
    this.setState({
      oikea: this.state.oikea + 1,
      kaikki: this.state.kaikki.concat('o')
    })
  }

  render() {
    const historia = () => this.state.kaikki.join(' ')
    return (
      <div>
        <div>
          {this.state.vasen}
          <button onClick={this.klikVasen}>vasen</button>
          <button onClick={this.klikOikea}>oikea</button>
          {this.state.oikea}
          <div>{historia()}</div>
        </div>
      </div>
    )
  }
}

Kun esim. nappia vasen painetaan, lisätään tilan taulukkoon kaikki kirjain v:

klikVasen = () => {
  this.setState({
    vasen: this.state.vasen + 1,
    kaikki: this.state.kaikki.concat('v')
  })
}

Tilan kenttä kaikki saa nyt arvokseen entisen tilan, mihin on liitetty v metodilla concat, joka toimii siten, että se ei muuta olemassaolevaa taulukkoa vaan luo uuden taulukon, mihin uusi alkio on lisätty.

Javascriptissa on myös mahdollista lisätä taulukkoon metodilla push ja sovellus näyttäisi tässä tilanteessa toimivan myös jos lisäys tapahtuisi komennolla

kaikki: this.state.kaikki.push('v')

mutta älä tee niin. React-komponentin tilaa, eli muuttujaa this.state ei saa muuttaa suoraan!

Jos tilan kentissä on olioita, älä muuta niitä vaan tee muutos aina kopioon!

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

render() {
  const historia = () => this.state.kaikki.join(' ')
  return (
    <div>
      <div>
        {this.state.vasen}
        <button onClick={this.klikVasen}>vasen</button>
        <button onClick={this.klikOikea}>oikea</button>
        {this.state.oikea}
        <div>{historia()}</div>
      </div>
    </div>
  )
}

Metodiin render on nyt määritelty apufunktio:

const historia = () => this.state.kaikki.join(' ')

Taulukon join-metodilla muodostetaan taulukosta merkkijono, joka sisältää taulukon alkiot erotettuina välilyönnillä.

Ehdollinen renderöinti

Muutetaan apufunktiota hiukan:

const historia = () => {
  if (this.state.kaikki.length === 0) {
    return (
      <div>
        <em>sovellusta käytetään nappeja painelemalla</em>
      </div>
    )
  }
  return (
    <div>
      näppäilyhistoria: {this.state.kaikki.join(' ')}
    </div>
  )
}

Nyt funktion palauttama sisältö riippuu siitä, onko näppäimiä jo painettu. Jos ei, eli taulukko this.state.kaikki on tyhjä, palauttaa metodi “käyttöohjeen” sisältävän elementin

<div>
  <em>sovellusta käytetään nappeja painelemalla</em>
</div>

ja muussa tapauksessa näppäilyhistorian:

<div>
  näppäilyhistoria: {this.state.kaikki.join(' ')}
</div>

Komponentin App 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.

Näppäilyhistorian esittäminen alkaa olla jo sen verran monimutkainen operaatio, että se kannattaisi eristää omaksi komponentikseen. Jätämme sen kuitenkin tekemättä.

Funktionaalinen vai luokkasyntaksiin perustuva komponentti?

Olemme nyt esitelleet kaksi erilaista tapaa komponenttien määrittelemiseen. Kumpaa tulisi käyttää? Useimpien vastauksena on, käytä funktionaalista komponenttia aina kun se on mahdollista.

Jos komponentti tarvitsee tilaa, on luokkasyntaksin käyttäminen välttämätöntä. Kannattaa kuitenkin muistaa, että Reactin filosofian mukaista on sijoittaa tila mahdollisimman ylös komponenttihierarkiaan, mielellään ainoastaan sovelluksen juurikomponenttiin. Näin tilallisten komponenttien potentiaalinen tarvekin on vähäisempi.

Joskus komponenttien on käytettävä osassa 2 esiteltäviä lifecycle-metodeja, myös niissä tapauksissa on pakko käyttää luokkiin perustuvia komponentteja.

Yleisohjeena on siis se, että käytä funktionaalisia komponentteja ellet aivan pakosti tarvitse jotain luokkasyntaksin omaavien komponenttien ominaisuuksia.

Internetistä löytyy kyllä aiheesta päinvastaisiakin mielipiteitä, esim. 7 Reasons to Outlaw React’s Functional Components

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

ä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, en 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('propsin arvo on' + props)

erottele tulostettavat asiat pilkulla:

console.log('propsin arvo on', props)

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

propsin arvo on [Object object]

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

Koodin suorituksen voi pysäyttää chromen developer konsolin debuggeriin kirjoittamalla mihin tahansa kohtaa koodia komennon debugger.

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

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

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

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

Debuggeriin pääsee myös ilman komentoa debugger lisäämällä Source-välilehdellä sopiviin kohtiin koodia breakpointeja. Haluttujen muuttujien arvojen tarkkailu on mahdollista määrittelemällä ne Watch-osassa:

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

Uuden konsolitabin avulla voidaan tarkkailla sovelluksen React-elementtejä ja niiden tilaa (eli this.state:a) ja propseja.

Tapahtumankäsittely revisited

Pajan ja telegrammin havaintojen perusteella tapahtumankäsittely on osoittautunut haastavaksi.

Tarkastellaan asiaa vielä uudelleen.

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

class App extends React.Component {
  constructor() {
    super()
    this.state = {
      value: 10
    }
  }
  render(){
    return (
      <div>
        {this.state.value}
        <button>nollaa</button>
      </div>
    )
  }
}

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

Haluamme, että napin avulla tilassa oleva value saadaan nollattua.

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

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

Jos esim. antaisimme tapahtumankäsittelijäksi merkkijonon:

<button onClick={"roskaa"}>nappi</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)

eli esim. seuraavanlainen yritys olisi tuhoon tuomittu

<button onClick={this.state.value+1}>nappi</button>

nyt tapahtumankäsittelijäksi on yritetty laittaa this.state.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={this.state.value = 0}>nappi</button>

taaskaan tapahtumankäsittelijänä ei ole funktio vaan sijoitusoperaatio. Konsoliin tulee valitus. Tämä tapa on myös toisella tavalla väärin. Kuten on jo mainittu, reactin tilaa ei saa muuttaa suoraan, vaan ainoastaan funktion setState-avulla.

Entä seuraava:

<button onClick={console.log('nappia painettu')}>nappi</button>

konsoliin tulostuu kertaalleen nappia painettu, 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 undefined.

Funktiokutsu console.log(‘nappia painettu’) 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={this.setState({value: 0})}>nappi</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 this.setState({value: 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 tietyn funktiokutsun tapahtuvan nappia painettaessa, toimii seuraava

<button onClick={() => console.log('nappia painettu')}>nappi</button>

Nyt tapahtumankäsittelijä on nuolisyntaksilla määritelty funktio () => console.log(‘nappia painettu’). 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={() => this.setState({value: 0})}>nappi</button>

eli nyt tapahtumankäsittelijä on funktio () => this.setState({value: 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 metodin render alussa ja sijoitetaan se muuttujaan handler:

render() {
  const handler = () => console.log('nappia painettu')

  return (
    <div>
      {this.state.value}
      <button onClick={handler}>nappi</button>
    </div>
  )
}

Muuttujassa handler on nyt talletettuna viite itse funktioon. Viite annetaan napin määrittelyn yhteydessä

<button onClick={handler}>nappi</button>

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

render() {
  const handler = () => {
    console.log('nappia painettu')
    this.setState({ value: 0 })
  }

  return (
    <div>
      {this.state.value}
      <button onClick={handler}>nappi</button>
    </div>
  )
}

Joissain tilanteissa tapahtumankäsittelijät kannattaa määritellä komponentin metodeina:

class App extends React.Component {
  // ...

  handler = () => {
    console.log('nappia painettu')
    this.setState({ value: 0 })
  }

  render() {
    return (
      <div>
        {this.state.value}
        <button onClick={this.handler}>nappi</button>
      </div>
    )
  }
}

Koska handler on nyt komponentin metodi, päästään siihen käsiksi viitteen this avulla:

<button onClick={this.handler}>nappi</button>

Mennään lopuksi funktioita palauttavaan funktioon.

Muutetaan koodia seuraavasti

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

    return handler
  }

  return (
    <div>
      {this.state.value}
      <button onClick={hello()}>nappi</button>
    </div>
  )
}

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

Tapahtumankäsittelijäksi on nyt “rekisteröity” funktiokutsu:

<button onClick={hello()}>nappi</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()}>nappi</button>

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

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

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

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

Muutetaan koodia hiukan:

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

    return handler
  }

  return (
    <div>
      {this.state.value}
      <button onClick={hello('world')}>nappi</button>
      <button onClick={hello('react')}>nappi</button>
      <button onClick={hello('function')}>nappi</button>
    </div>
  )
}

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

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

<button onClick={hello('world')}>nappi</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')}>nappi</button>

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

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

eli nappi saa 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) => () => { 
    this.setState({ value: newValue })
  }

  return (
    <div>
      {this.state.value}
      <button onClick={setToValue(1000)}>tuhat</button>
      <button onClick={setToValue(0)}>nollaa</button>
      <button onClick={setToValue(this.state.value+1)}>kasvata</button>
    </div>
  )
}

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

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

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

() => { 
  this.setState({ value: 1000 })
}

Kasvatusnapin generoima rivi on seuraava

<button onClick={setToValue(this.state.value+1)}>kasvata</button>

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

() => { 
  this.setState({ value: 11 })
}

Hyödyllistä materiaalia

Internetissä on todella paljon Reactiin liittyvää materiaalia, tässä muutamia linkkejä:

  • Reactin docs kannattaa ehdottomasti käydä läpi, ei välttämättä kaikkea nyt, osa on ajankohtaista vasta kurssin myöhemmissä osissa
  • 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.

Lisää React-tehtäviä

Tee nyt tehtävät 1.6-1.14