a

Kokoelmien renderöinti ja moduulit

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ä joitain muita debuggauskeinoja.

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

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

Tapahtumankäsittely revisited

Aiempien vuosien kurssien alun kokemusten perusteella tapahtumien käsittely on osoittautunut haastavaksi.

Edellisen osan lopussa 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 kysymyksiä. Pieni kertaus aiheeseen on 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ä.

Ehkä kätevin 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 esim. Java 8:sta tutuissa streameissa, joita on käytetty jo vuosien ajan Tietojenkäsittelytieteen osaston Ohjelmoinnin perusteissa ja jatkokurssilla sekä Ohjelmoinnin MOOC:issa. Operaattoreihin tutustutaan myös Ohjelmoinnin jatkokurssin Python-versiossa, osassa 12.

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 (tiedosto App.js):

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

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

export default App

Tiedosto index.js on muuten samanlainen kuin se on ollut toistaiseksi kaikissa ohjelmissa, mutta se määrittelee taulukon, jossa on näytettävä data.

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

import App from './App'

const notes = [
  {
    id: 1,
    content: 'HTML is easy',
    important: true
  },
  {
    id: 2,
    content: 'Browser can execute only JavaScript',
    important: false
  },
  {
    id: 3,
    content: 'GET and POST are the most important methods of HTTP protocol',
    important: true
  }
]

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

Jokaiseen muistiinpanoon on merkitty tekstisisällön lisäksi yksikäsitteinen tunniste id ja boolean-arvo, joka kertoo onko muistiinpano luokiteltu tärkeäksi.

Taulukossa on kolme muistiinpanoa, ja yksittäiset muistiinpanot renderöidään 'kovakoodatusti' viittaamalla taulukon olioihin:

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

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

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

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

[
  '<li>HTML is easy</li>',
  '<li>Browser can execute only Javascript</li>',
  '<li>GET and POST are the most important methods of HTTP protocol</li>',
]

jotka voidaan sijoittaa ul-tagien sisälle:

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

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

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

Parannetaan koodin luetteloa vielä jakamalla nuolifunktion määrittely useammalle riville:

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

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

Key-attribuutti

Vaikka sovellus näyttää toimivan, konsoliin tulee 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

  return (
    <div>
      <h1>Notes</h1>
      <ul>
        {notes.map(note => 
          <li key={note.id}>            {note.content}
          </li>       
        )}
      </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 on täällä.

Map

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

Sovellus siis sisältää taulukon notes:

const notes = [
  {
    id: 1,
    content: 'HTML is easy',
    important: true
  },
  {
    id: 2,
    content: 'Browser can execute only JavaScript',
    important: false
  },
  {
    id: 3,
    content: 'GET and POST are the most important methods of HTTP protocol',
    important: true
  }
]

Tutkitaan miten map toimii.

Lisätään tiedoston loppuun seuraava koodi:

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

Konsoliin tulostuu [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 map-metodin parametrina olevan funktion

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

käyttötarkoitus on näkymäelementtien muodostaminen, tulee muuttujien note.id ja note.content 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.

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-metodin callback-funktiossa myös toista parametria:

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

Näin kutsuttaessa i saa arvokseen muistiinpanon indeksin taulukossa.

Eräs tapa päästä eroon konsoliin tulostuvasta virheilmoituksesta olisi siis:

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

Tämä ei kuitenkaan ole suositeltavaa ja voi aiheuttaa 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>Notes</h1>
      <ul>
        {notes.map(note => 
          <li key={note.id}>
            {note.content}
          </li>
        )}
      </ul>
    </div>
  )
}

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

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

const Note = ({ note }) => {  return (    <li>{note.content}</li>  )}
const App = ({ notes }) => {
  return (
    <div>
      <h1>Notes</h1>
      <ul>
        {notes.map(note =>           <Note key={note.id} note={note} />        )}      </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 ole kovin 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 index.js ensimmäiset rivit

import ReactDOM from "react-dom/client"

import App from "./App"

importtaavat eli ottavat käyttöönsä kaksi moduulia. Moduuli react-dom/client sijoitetaan muuttujaan ReactDOM ja sovelluksen pääkomponentin määrittelevä moduuli muuttujaan App.

Siirretään nyt Note-komponentti 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:

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

export default Note

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

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

import Note from './components/Note'
const App = ({ notes }) => {
  // ...
}

Moduulin eksporttaama komponentti on nyt käytettävissä muuttujassa Note kuten aiemminkin.

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.

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 main-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 usein näin käy dynaamisesti tyypitetyillä kielillä (kuten JavaScript), joissa kääntäjä ei tarkasta minkä tyyppisiä arvoja esim. funktioiden 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 yksilausekkeisena eli returnittomana funktiona, 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 aiheutuu 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>
  )
}

Jos ongelma ei vieläkään ratkea, 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.

Websovelluskehittäjän vala

Ennen tehtävien pariin palaamista on hyvä muistaa, mitä lupasimme osan yksi lopussa.

Ohjelmointi on hankalaa, ja sen takia lupaan hyödyntää kaikkia ohjelmointia helpottavia keinoja:

  • pidän selaimen konsolin koko ajan auki
  • etenen pienin askelin
  • käytän koodissa runsaasti console.log-komentoja varmistamaan sen, että varmasti ymmärrän jokaisen kirjoittamani koodirivin, sekä etsiessäni koodista mahdollisia bugin aiheuttajia
  • jos koodini ei toimi, en kirjoita enää yhtään lisää koodia, vaan alan poistamaan toiminnan rikkoneita rivejä tai palaan suosiolla tilanteeseen, missä koodi vielä toimi
  • kun kysyn apua kurssin Discord- tai Telegram-kanavalla, tai muualla internetissä, muotoilen kysymyksen järkevästi, esim. täällä esiteltyyn tapaan