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ä jotain 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
Viime vuoden kurssin 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 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 is easy',
date: '2020-01-10T17:30:31.098Z',
important: true
},
{
id: 2,
content: 'Browser can execute only Javascript',
date: '2020-01-10T18:39:34.091Z',
important: false
},
{
id: 3,
content: 'GET and POST are the most important methods of HTTP protocol',
date: '2020-01-10T19:20:14.298Z',
important: true
}
]
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>
)
}
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 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, tulee se sijoittaa JSX-templatessa aaltosulkujen sisälle kaiken 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, tulee konsoliin ikävä varoitus
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 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 is easy',
date: '2020-01-10T17:30:31.098Z',
important: true
},
{
id: 2,
content: 'Browser can execute only Javascript',
date: '2020-01-10T18:39:34.091Z',
important: false
},
{
id: 3,
content: 'GET and POST are the most important methods of HTTP protocol',
date: '2020-01-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 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.
Parempi muotoilu ohjelmamme muistiinpanorivit tuottavalle apufunktiolle saattaakin olla seuraava useille riveille jaoteltu versio:
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-metodin callback-funktiossa myös toista parametria:
notes.map((note, i) => ...)
näin kutsuttaessa i saa arvokseen sen paikan indeksin taulukossa, missä Note sijaitsee.
Eli eräs konsoliin tulostuvaa virheilmoitusta aiheuttamaton tapa määritellä rivien generointi olisi
<ul>
{notes.map((note, i) =>
<li key={i}>
{note.content}
</li>
)}
</ul>
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>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 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 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 }) => {
return (
<div>
<h1>Notes</h1>
<ul>
{notes.map((note, i) =>
<Note key={note.id} note={note} />
)}
</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:
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, missä kääntäjä ei tarkasta minkä tyyppisiä arvoja esim. funktioiden parametreina ja paluuarvoina liikkuu.
Reactissa räjähdys näyttää esim. seuraavalta
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
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>
)
}
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.