Tämä on 3. periodilla pidetyn, jo päättyneen kurssin sisältö.

Jos haluat suorittaa kurssin nyt, mene osoitteeseen https://fullstackopen-2019.github.io/

Ennen kun menemme uuteen asiaan, nostetaan esiin muutama edellisen osan huomiota herättänyt seikka.

console.log

Mikä erottaa kokeneen ja kokemattoman Javascript-ohjelmoijan? Kokeneet käyttävät 10-100 kertaa enemmän console.logia.

Paradoksaalista kyllä tämä näyttää olevan tilanne, vaikka kokematon ohjelmoija oikeastaan tarvitsisi console.logia (tai jotain muita debuggaustapoja) huomattavissa määrin kokenutta enemmän.

Eli kun joku ei toimi, älä arvaile vaan logaa tai käytä jotain muita debuggauskeinoja.

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 erotellessa saat tulostettavat asiat developer-konsoliin oliona, jonka sisältöä on mahdollista tarkastella.

Lue tarvittaessa lisää React-sovellusten debuggaamisesta täältä.

Tapahtumankäsittely revisited

Viime vuoden kurssin alun kokemusten perusteella tapahtumien käsittelu on osoittautunut haastavaksi.

Edellisen osan loppussa oleva kertaava osa tapahtumankäsittely revisited kannattaa käydä läpi jos osaaminen on vielä häilyvällä pohjalla.

Myös tapahtumankäsittelijöiden välittäminen komponentin App alikomponenteille on herättänyt ilmaan kysymyksiä, pieni kertaus aiheeseen täällä.

Protip: Visual Studio Coden snippetit

Visual studio codeen on helppo määritellä "snippettejä", eli Netbeansin "sout":in tapaisia oikoteitä yleisesti käytettyjen koodinpätkien generointiin. Ohje snippetien luomiseen täällä.

VS Code -plugineina löytyy myös hyödyllisiä valmiiksi määriteltyjä snippettejä, esim. tämä.

Tärkein kaikista snippeteistä on komennon console.log() nopeasti ruudulle tekevä snippet, esim. clog, jonka voi määritellä seuraavasti:

{
  "console.log": {
    "prefix": "clog",
    "body": [
      "console.log('$1')",
    ],
    "description": "Log output to console"
  }
}

Taulukkojen käyttö Javascriptissä

Tästä osasta lähtien käytämme runsaasti Javascriptin taulukkojen funktionaalisia käsittelymetodeja, kuten find, filter ja map. Periaate niissä on täysin sama kuin Java 8:sta tutuissa streameissa, joita on käytetty jo parin vuoden ajan Tietojenkäsittelytieteen osaston Ohjelmoinnin perusteissa ja jatkokurssilla sekä Ohjelmoinnin MOOC:issa.

Jos taulukon funktionaalinen käsittely tuntuu vielä vieraalta, kannattaa katsoa Youtubessa olevasta videosarjasta Functional Programming in JavaScript ainakin kolme ensimmäistä osaa

Kokoelmien renderöiminen

Tehdään nyt Reactilla osan 0 alussa käytettyä esimerkkisovelluksen Single page app -versiota vastaavan sovelluksen 'frontend' eli selainpuolen sovelluslogiikka.

Aloitetaan seuraavasta:

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

const notes = [
  {
    id: 1,
    content: 'HTML on helppoa',
    date: '2019-01-10T17:30:31.098Z',
    important: true
  },
  {
    id: 2,
    content: 'Selain pystyy suorittamaan vain javascriptiä',
    date: '2019-01-10T18:39:34.091Z',
    important: false
  },
  {
    id: 3,
    content: 'HTTP-protokollan tärkeimmät metodit ovat GET ja POST',
    date: '2019-01-10T19:20:14.298Z',
    important: true
  }
]

const App = (props) => {
  const { notes } = props

  return (
    <div>
      <h1>Muistiinpanot</h1>
      <ul>
        <li>{notes[0].content}</li>
        <li>{notes[1].content}</li>
        <li>{notes[2].content}</li>
      </ul>
    </div>
  )
}

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

Jokaiseen muistiinpanoon on merkitty tekstuaalisen sisällön ja aikaleiman lisäksi myös boolean-arvo, joka kertoo onko muistiinpano luokiteltu tärkeäksi, sekä yksikäsitteinen tunniste id.

Koodin toiminta perustuu siihen, että taulukossa on tasan kolme muistiinpanoa, yksittäiset muistiinpanot renderöidään 'kovakoodatusti' viittaamalla suoraan taulukossa oleviin olioihin:

<li>{note[1].content}</li>

Tämä ei tietenkään ole järkevää. Ratkaisu voidaan yleistää generoimalla taulukon perusteella joukko React-elementtejä käyttäen map-funktiota:

notes.map(note => <li>{note.content}</li>)

nyt tuloksena on taulukko, jonka sisältö on joukko li-elementtejä

[
  '<li>HTML on helppoa</li>',
  '<li>Selain pystyy suorittamaan vain javascriptiä</li>',
  '<li>HTTP-protokollan tärkeimmät metodit ovat GET ja POST</li>',
]

jotka voidaan sijoittaa ul-tagien sisälle:

const App = (props) => {
  const { notes } = props

  return (
    <div>
      <h1>Muistiinpanot</h1>
      <ul>        {notes.map(note => <li>{note.content}</li>)}      </ul>    </div>
  )
}

Koska li-tagit generoiva koodi on Javascriptia, tulee se sijoittaa JSX-templatessa aaltosulkujen sisälle kaiken muun Javascript-koodin tapaan.

Usein vastaavissa tilanteissa dynaamisesti generoitava sisältö eristetään omaan metodiin, jota JSX-template kutsuu:

const App = (props) => {
  const { notes } = props

  const rows = () =>    notes.map(note => <li>{note.content}</li>)
  return (
    <div>
      <h1>Muistiinpanot</h1>
      <ul>
        {rows()}      </ul>
    </div>
  )
}

Key-attribuutti

Vaikka sovellus näyttää toimivan, tulee konsoliin ikävä varoitus

fullstack content

Kuten virheilmoituksen linkittämä sivu kertoo, tulee taulukossa olevilla, eli käytännössä map-metodilla muodostetuilla elementeillä olla uniikki avain, eli attribuutti nimeltään key.

Lisätään avaimet:

const App = (props) => {
  const { notes } = props

  const rows = () =>    notes.map(note => <li key={note.id}>{note.content}</li>)
  return (
    <div>
      <h1>Muistiinpanot</h1>
      <ul>
        {rows()}
      </ul>
    </div>
  )
}

Virheilmoitus katoaa.

React käyttää taulukossa olevien elementtien key-kenttiä päätellessään miten sen tulee päivittää komponentin generoimaa näkymää silloin kun komponentti uudelleenrenderöidään. Lisää aiheesta täällä.

Map

Taulukoiden metodin map toiminnan sisäistäminen on jatkon kannalta äärimmäisen tärkeää.

Sovellus siis sisältää taulukon notes

const notes = [
  {
    id: 1,
    content: 'HTML on helppoa',
    date: '2017-12-10T17:30:31.098Z',
    important: true,
  },
  {
    id: 2,
    content: 'Selain pystyy suorittamaan vain javascriptiä',
    date: '2017-12-10T18:39:34.091Z',
    important: false,
  },
  {
    id: 3,
    content: 'HTTP-protokollan tärkeimmät metodit ovat GET ja POST',
    date: '2017-12-10T19:20:14.298Z',
    important: true,
  },
]

Pysähdytään hetkeksi tarkastelemaan miten map toimii.

Jos esim. tiedoston loppuun lisätään seuraava koodi

const result = notes.map(note => note.id)
console.log(result)

tulostuu konsoliin [1, 2, 3] eli map muodostaa uuden taulukon, jonka jokainen alkio on saatu alkuperäisen taulukon notes alkioista mappaamalla komennon parametrina olevan funktion avulla.

Funktio on

note => note.id

eli kompaktissa muodossa kirjoitettu nuolifunktio, joka on täydelliseltä kirjoitustavaltaan seuraava

(note) => {
  return note.id
}

eli funktio saa parametrikseen muistiinpano-olion ja palauttaa sen kentän id arvon.

Muuttamalla komento muotoon

const result = notes.map(note => note.content)

tuloksena on taulukko, joka koostuu muistiinpanojen sisällöistä.

Tämä on jo lähellä käyttämäämme React-koodia:

notes.map(note => <li key={note.id}>{note.content}</li>)

joka muodostaa jokaista muistiinpano-olioa vastaavan li-tagin, jonka sisään tulee muistiinpanon sisältö.

Koska metodin map parametrina olevan funktion

note => <li key={note.id}>{note.content}</li>

käyttötarkoitus on näkymäelementtien muodostaminen, tulee muuttujan arvo renderöidä aaltosulkeiden sisällä. Kokeile mitä koodi tekee, jos poistat aaltosulkeet.

Aaltosulkeiden käyttö tulee varmaan aiheuttamaan alussa pientä päänvaivaa, mutta totut niihin pian. Reactin antama visuaalinen feedback on välitön.

Tarkastellaan vielä erästä bugien lähdettä. Lisää koodiin seuraava

const result = notes.map(note => {note.content} )
console.log(result)

Tulostuu

[undefined, undefined, undefined]

Missä on vika? Koodihan on ihan sama kun äsken toiminut koodi. Paitsi ei ihan. Metodin map parametrina on nyt seuraava funktio

note => {
  note.content
}

Koska funktio koostuu nyt koodilohkosta on funktion paluuarvo määrittelemätön eli undefined. Nuolifunktiot siis palauttavat ainoan komentonsa arvon, ainoastaan jos nuolifunktio on määritelty kompaktissa muodossaan, ilman koodilohkoa:

note => note.content

huomaa, että 'oneliner'-nuolifunktioissa kaikkea ei tarvitse eikä aina kannatakaan kirjoittaa samalle riville.

Parempi muotoilu ohjelmamme muistiinpanorivit tuottavalle apufunktiolle saattaakin olla seuraava useille riveille jaoteltu versio:

const rows = () => notes.map(note =>
  <li key={note.id}>
    {note.content}
  </li>
)

Kyse on kuitenkin edelleen yhden komennon sisältävästä nuolifunktiosta, komento vain sattuu olemaan hieman monimutkaisempi.

Antipattern: taulukon indeksit avaimina

Olisimme saaneet konsolissa olevan varoituksen katoamaan myös käyttämällä avaimina taulukon indeksejä. Indeksit selviävät käyttämällä map-metodissa myös toista parametria:

notes.map((note, i) => ...)

näin kutsuttaessa i saa arvokseen sen paikan indeksin taulukossa, missä Note sijaitsee.

Eli eräs virhettä aiheuttamaton tapa määritellä rivien generointi on

const rows = () => notes.map((note, i) => 
  <li key={i}>
    {note.content}
  </li>
)

Tämä ei kuitenkaan ole suositeltavaa ja voi näennäisestä toimimisestaan aiheuttaa joissakin tilanteissa pahoja ongelmia. Lue lisää esimerkiksi täältä.

Refaktorointia - moduulit

Siistitään koodia hiukan. Koska olemme kiinnostuneita ainoastaan propsien kentästä notes, otetaan se vastaan suoraan destrukturointia hyödyntäen:

const App = ({ notes }) => {  // ...

  return (
    <div>
      <h1>Muistiinpanot</h1>
      <ul>
        {rows()}
      </ul>
    </div>
  )
}

Jos unohdit mitä destrukturointi tarkottaa ja miten se toimii, kertaa täältä.

Erotetaan yksittäisen muistiinpanon esittäminen oman komponenttinsa Note vastuulle:

const Note = ({ note }) => {  return (    <li>{note.content}</li>  )}
const App = ({ notes }) => {
  const rows = () => notes.map(note =>
    <Note       key={note.id}      note={note}    />  )

  return (
    <div>
      <h1>Muistiinpanot</h1>
      <ul>
        {rows()}
      </ul>
    </div>
  )
}

Huomaa, että key-attribuutti täytyy nyt määritellä Note-komponenteille, eikä li-tageille kuten ennen muutosta.

Koko React-sovellus on mahdollista määritellä samassa tiedostossa, mutta se ei luonnollisesti ole järkevää. Usein käytäntönä on määritellä yksittäiset komponentit omassa tiedostossaan ES6-moduuleina.

Koodissamme on käytetty koko ajan moduuleja. Tiedoston ensimmäiset rivit

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

importtaavat eli ottavat käyttöönsä kaksi moduulia. Moduuli react sijoitetaan muuttujaan React ja react-dom muuttujaan ReactDOM.

Siirretään nyt komponentti Note omaan moduuliinsa.

Pienissä sovelluksissa komponentit sijoitetaan yleensä src-hakemiston alle sijoitettavaan hakemistoon components. Konventiona on nimetä tiedosto komponentin mukaan.

Tehdään nyt sovellukseen hakemisto components ja sinne tiedosto Note.js jonka sisältö on seuraava:

import React from 'react'

const Note = ({ note }) => {
  return (
    <li>{note.content}</li>
  )
}

export default Note

Koska kyseessä on React-komponentti, tulee React importata komponentissa.

Moduulin viimeisenä rivinä eksportataan määritelty komponentti, eli muuttuja Note.

Nyt komponenttia käyttävä tiedosto index.js voi importata moduulin:

import React from 'react'
import ReactDOM from 'react-dom'
import Note from './components/Note'
const App = ({notes}) => {
  // ...
}

Moduulin eksporttaama komponentti on nyt käytettävissä muuttujassa Note täysin samalla tavalla kuin aiemmin.

Huomaa, että itse määriteltyä komponenttia importatessa komponentin sijainti tulee ilmaista suhteessa importtaavaan tiedostoon:

'./components/Note'

Piste alussa viittaa nykyiseen hakemistoon, eli kyseessä on nykyisen hakemiston alihakemisto components ja sen sisällä tiedosto Note.js. Tiedoston päätteen voi jättää pois.

Koska myös App on komponentti, eristetään sekin omaan moduuliinsa. Koska kyseessä on sovelluksen juurikomponentti, sijoitetaan se suoraan hakemistoon src. Tiedoston sisältö on seuraava:

import React from 'react'
import Note from './components/Note'

const App = ({ notes }) => {
  const rows = () => notes.map(note =>
    <Note
      key={note.id}
      note={note}
    />
  )

  return (
    <div>
      <h1>Muistiinpanot</h1>
      <ul>
        {rows()}
      </ul>
    </div>
  )
}

export default App

Tiedoston index.js sisällöksi jää:

import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
const notes = [
  // ...
]

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

Moduuleilla on paljon muutakin käyttöä kuin mahdollistaa komponenttien määritteleminen omissa tiedostoissaan, palaamme moduuleihin tarkemmin myöhemmin kurssilla.

Sovelluksen tämänhetkinen koodi on kokonaisuudessaan githubissa

Huomaa, että repositorion master-haarassa on myöhemmän vaiheen koodi, tämän hetken koodi on branchissa part2-1:

fullstack content

Jos kloonaat projektin itsellesi, suorita komento npm install ennen käynnistämistä eli komentoa npm start.

Kun sovellus hajoaa

Kun aloitat ohjelmoijan uraasi (ja allekirjoittaneella edelleen 30 vuoden ohjelmointikokemuksella) käy melko usein niin, että ohjelma hajoaa aivan totaalisesti. Erityisen usien näin käy dynaamisesti tyypitetyillä kielillä, kuten Javascript, missä kääntäjä ei tarkasta minkä tyyppisiä arvoja esim. funktioden parametreina ja paluuarvoina liikkuu.

Reactissa räjähdys näyttää esim. seuraavalta

fullstack content

Tilanteista pelastaa yleensä parhaiten console.log. Pala räjähdyksen aiheuttavaa koodia seuraavassa

const Course = ({ course }) => (
  <div>
   <Header course={course} />
  </div>
)

const App = () => {
  const course = {
    // ...
  }

  return (
    <div>
      <Course course={course} />
    </div>
  )
}

Syy toimimattomuuteen alkaa selvitä lisäilemällä koodiin console.log-komentoja. Koska ensimmäinen renderöitävä asia on komponentti App kannattaa sinne laittaa ensimmäisen tulostus:

const App = () => {
  const course = {
    // ...
  }

  console.log('App toimii...')
  return (
    // ..
  )
}

Konsoliin tulevan tulostuksen nähdäkseen on skrollattava pitkän punaisen virhematon yläpuolelle

fullstack content

Kun joku asia havaitaan toimivaksi, on aika logata syvemmältä. Jos komponentti on määritelty yksilausekkeista, eli returnittomana funktiota, on konsoliin tulostus haastavampaa:

const Course = ({ course }) => (
  <div>
   <Header course={course} />
  </div>
)

komponentti on syytä muuttaa pidemmän kaavan mukaan määritellyksi jotta tulostus päästään lisäämään:

const Course = ({ course }) => { 
  console.log(course)  return (
    <div>
    <Header course={course} />
    </div>
  )
}

Erittäin usein ongelma on siitä että propsien odotetaan olevan eri muodossa tai eri nimisiä, kuin ne todellisuudessa ovat ja destrukturointi epäonnistuu. Ongelma alkaa useimmiten ratketa kun poistetaan destrukturointi ja katsotaan mitä props oikeasti pitää sisällään:

const Course = (props) => {  console.log(props)  const { course } = props
  return (
    <div>
    <Header course={course} />
    </div>
  )
}

Ja jos ongelma ei vieläkään selviä, ei auta kuin jatkaa vianjäljitystä, eli kirjoittaa lisää console.logeja.

Lisäsin tämän luvun materiaaliin kun seuraavan tehtävän mallivastauksen koodi räjähti ihan totaalisesti (syynä väärässä muodossa ollut propsi) ja jouduin jälleen kerran debuggaamaan console.logaamalla.