React
Tästä tehtävästä on uusittu versio, jossa käytetään function-komponentteja.
Ennen näiden tehtävien tekemistä niin katso seuraavat luennot:
- React Tommi Lahtonen
- React Jonne Räsänen / Nordcloud
- JSX ja virtuaalinen dom (Jari Pennanen)
- 02 - Reactin perusteita 1/2 (Jari Pennanen)
- 03 - Reactin perusteita 2/2 (Jari Pennanen)
Avaa Reactin aloitustutoriaali Voit yrittää toteuttaa tutoriaalin ohjeiden mukaan react-sovelluksen codepenissa tai voit jatkaa näillä tehtävillä ja käyttää tutoriaalia apunasi seuraavissa tehtävissä:
- Asenna Chromen React Developer Tools ja Firefoxiin React Developer Tools.
- Aloita uusi html-dokumentti ja käytä seuraavaa valmista pohjakoodia:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>React Hello World</title> <script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script> <script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script> <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script> </head> <body> <div id="root"></div> <!-- Seuraava kannattaa oikeasti laittaa ulkoiseen javascript-tiedostoon: <script type="text/jsx" src="oma_react_koodi.js"></script> --> <script type="text/jsx"> ReactDOM.render( <h1>Hello, world!</h1>, document.getElementById('root') ); </script> </body> </html>- Pohjaan on react-kirjastojen lisäksi linkitetty babel.js. babel-kirjasto muuntaa lennosta kirjoittamasi JSX-koodin tavalliseksi selainyhteensopivaksi javascriptiksi, jos skriptin tyyppi on text/jsx. Tuotantokäytössä babelin tekemä muunnos pitää suorittaa jo www-palvelimessa luennolla näytetyllä tavalla. Lisätietoa: Add React to a Website
- Yleensä oma koodi kannattaa kirjoittaa omaan javascript-tiedostoon. Silloin se pitää linkittää pohjaan tällä tavalla:
Huom! Debuggerissa oma koodi löytyy Babel-kirjaston alapuolelta. Katso myös React Developer toolsin lisäämät uudet välilehdet Components ja Profiler. Näiden käyttämisestä löytyy hyvä demonstraatio.<script type="text/jsx" src="oma_react_koodi.js"></script>Huom! Jos laitat oman koodisi erilliseen tiedostoon, niin et voi enää testata sovellustasi lokaalisti vaan se on siirrettävä www-palvelimelle. Babel-kirjasto ei pääse lokaaliin tiedostoosi käsiksi eikä kykene kääntämään jsx-koodiasi javascript-koodiksi. saat tässä tapauksessa seuraavan virheilmoituksen:
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at file:
- Avaa näkyville tutoriaalin lisäksi Top-Level API .
- Ole erityisen tarkkana, että Reactilla kirjoittamasi html on ehjää eli lopetustagit ym. pitää olla paikoillaan. Babelin JSX-tulkki kaatuu heti, jos html ei ole rakenteeltaan ehjää. React-komponenttien nimet alkavat isoilla kirjaimilla ja tavallisten html-elementtien nimet pienillä kirjaimilla.
- React/JSX antaa joskus virheilmoitukset aivan eri kohdasta koodia kuin missä varsinainen virhe sijaitsee. Virhe on todennäköisesti käytetyssä komponentissa, mutta virheilmoitus viittaa johonkin omituiseen paikkaan komponenttia käyttävässä metodissa. Oikea virhekohta löytyy, kun kokeilee mikä komponentti virheen aiheuttaa, ja sen jälkeen tutkii tarkemmin tämän komponentin koodia. Tutki myös mitä edellä asentamasi React Developer tool näyttää. Se löytyy muiden sovellustyökalujen joukosta, kun painat Chromessa F12-näppäintä.
- Toteuta tutoriaalin esimerkkien avulla yksinkertainen autolaskuri
eli painike, joka kasvattaa www-sivulla näkyvää lukua. Toteuta
autolaskuri kokonaan React.js:n avulla
jsx-kielellä. Kts. JSX in depth.
- Luo ensin oma React-komponentti, joka sisältää yhden tilamuuttujan (laskuri) ja render-metodin, joka tulostaa
laskurimuuttujan arvon.
Komponentit pitää määritellä ennen ReactDOM.render-kutsua eli ReactDOM.render-kutsun on aina oltava tiedostossa viimeisenä. Lisää render-kutsuun oma komponenttisi:class Counter extends React.Component { constructor(props) { super(props); this.state = { "laskuri" : 0 }; } render () { return <div> <p>{this.state.laskuri}</p> </div>; } }
Huomaa, että render-funktio ottaa vain yhden elementin parametrinaan eli tässä tapauksessa on laitettava div-elementin sisään kaikki muut elementit.ReactDOM.render( <div> <h1>Hello, world!</h1>, <Counter /> </div>, document.getElementById('root') ); - Lisää komponenttiisi varsinainen laskuriominaisuus eli painike, joka kasvattaa laskurin arvoa.
Tarvitset komponenttiin metodin, joka kasvattaa laskuria ja render-funktioon sinun pitää lisätä click-käsittelijä, joka sitten kutsuu
metodiasi. Komponentin tilan muuttaminen pitää construktorin ulkopuolella aina tehdä this.setstate-metodin avulla:
Huom. Koska kasvataLaskuria ei ole React-luokkien omia vakiofunktioita niin this-muuttuja ei tässä tapauksessa suoraan viittaa react-komponenttiin vaan window-luokkaan. Tämä pitää korjata lisäämällä luokan konstruktoriin rivi:kasvataLaskuria() { this.setState({laskuri: this.state.laskuri + 1}); }
Nyt kasvataLaskuria-metodin kutsuminen onnistuu helposti:this.kasvataLaskuria = this.kasvataLaskuria.bind(this);render() { return (<div> <p>{this.state.laskuri}</p> <button onClick={this.kasvataLaskuria}>Lisää</button> </div>); }Kokeile toimiiko laskuri
- Luo ensin oma React-komponentti, joka sisältää yhden tilamuuttujan (laskuri) ja render-metodin, joka tulostaa
laskurimuuttujan arvon.
- Huomioi Reactista seuraavat seikat:
- this.props sisältää komponentille attribuutteina tuodut parametrit. Näiden arvoja ei voi itse ohjelmakoodissa muuttaa.
- this.state sisältää komponentin tilaan liittyvät muuttujat. Niitä voi ja pitää muuttaa mutta muuttamisen saa tehdä vain this.setState-metodilla.
Jos muutat komponentin tilaa niin React päivittää muutokset
automaattisesti näkyville kutsumalla render-metodia aina
kun state muuttuu.
Huom!. setState-funktio toimii asynkronisesti. Kts. Why is setState fiving me the wrong value
Huom!. Jos tallennat tilaan objekteja tai taulukoita niin ole tarkkana tilaa kopioitaessa. Kts. esimerkki (lähdekoodi). Lisätietoa: How to update nested state properties in React ja Handling State in React: Four Immutable Approaches to Consider.
Jos tarvitset komponentissa muuttujia, jotka eivät vaikuta komponentin renderointiin, niin näitä ei tarvitse käyttää staten kautta vaan voit vapaasti keksiä omia. Esim. this.foobar = "omajuttu"
- Lisää autolaskurin alapuolelle listaus automalleista. Luo oma lista-komponentti jolle tuot parametrina (this.props.items) listassa esitettävät automallit (Polo, Micra, Corsa, Kadett, 1MW Concept, Fiesta, A1, Model S ).
- Luo ensin List-komponentti. this.props.items sisältää taulukon listattavista asioista.
map()-funktiolla on helppo
muodostaa haluttu lista. Tavallisia silmukoita ei voi käyttää suoraan return-lausekkeessa. Huomaa kuinka jokaiselle lista-alkiolle annetaan uniikki key-attribuutti. React tarvitsee tätä voidakseen tehokkaasti renderoida vain muuttuneet osat komponentista.
Valmista List-komponenttia voit nyt käyttää ReactDOM.render-metodissa tai muiden react-komponenttien render-metodeissa seuraavalla tavalla:class List extends React.Component { render(){ // return-lauseeseen ei voi sijoittaa normaaleja silmukoita // täytyy luoda taulukko etukäteen tai käyttää map-funktiota let lista = [] for( let i of this.props.items ) { lista.push( <li key={i}>{i}</li> ) } return ( <ul> { lista // this.props.items.map(function(item) { // return <li key={item}>{item}</li> // }) } </ul> ) } }
Ts. muuta ReactDOM.render-metodi seuraavanlaiseksi:<List items={["Polo", "Micra", "Corsa", "Kadett", "1MW Concept", "Fiesta", "A1", "Model S"]}/>ReactDOM.render( <div> <Counter /> <List items={["Polo", "Micra", "Corsa", "Kadett", "1MW Concept", "Fiesta", "A1", "Model S"]}/> </div>, document.getElementById('root') - Kokeile toimiiko listaus.
- Luo ensin List-komponentti. this.props.items sisältää taulukon listattavista asioista.
map()-funktiolla on helppo
muodostaa haluttu lista. Tavallisia silmukoita ei voi käyttää suoraan return-lausekkeessa. Huomaa kuinka jokaiselle lista-alkiolle annetaan uniikki key-attribuutti. React tarvitsee tätä voidakseen tehokkaasti renderoida vain muuttuneet osat komponentista.
- Luo uusi FilteredList-komponentti, joka käyttää edellä tekemääsi
List-komponenttia ja tee autolistauksesta suodattuva.
- Luo ensin uusi komponentti FilteredList. Reactissa ei kannata kopioida muiden komponenttien tilaa toisiin komponentteihin vaan pyritään pitämään tieto vain yhdessä paikassa.
- Käytä komponenttisi render-metodissa jo aiemmin luomaasi List-komponenttia. Lisää FilteredList-komponentin render-metodiin input-elementti jota voi käyttää suodatukseen
- Lisää FilteredList-komponenttiin konstruktori, joka alustaa
suodatusehdon tyhjäksi.
constructor(props) { super(props); this.state = {filter: "" }; } - Tee funktio, joka päivittää suodatukseen käytettävän
merkkijonon.
Muista myös bindata konstruktorissa tähän funktioon oikea thisfilterList(event) { this.setState({ "filter": event.target.value }); }this.filterList = this.filterList.bind(this); - Liitä render-metodissa edellä tekemäsi funktio input-kentän
change-tapahtumaan:
<input type="text" onChange={this.filterList} /> - Varsinainen suodatus pitää tehdä render-funktiossa.
Kts.
filter()-metodi.
Kokeile toimiiko suodatuslet self = this; let filteredList = this.props.items.filter(function(item){ return item.toLowerCase().includes( self.state.filter ) });
- Lisää automallilistaukseen myös valmistajat. Tee suodatus kuitenkin vain automallien perusteella vaikka
listauksessa näkyykin sekä malli että valmistaja.
<FilteredList items={[{key:1, malli:"Polo", valmistaja:"Volkswagen"}, {key:2, malli:"Micra",valmistaja:"Nissan"}, {key:3, malli:"Corsa",valmistaja:"Opel"}, {key:4, malli:"Kadett",valmistaja:"Opel"}, {key:5, malli:"1MW Concept",valmistaja:"Toroidion"}, {key:6, malli:"Fiesta",valmistaja:"Ford"}, {key:7, malli:"A1",valmistaja:"Audi"}, {key:8, malli:"Model S", valmistaja:"Tesla"}]}/>Huomaa, että nyt joudut muuttamaan joko myös List-komponentin render-metodia tai FilteredListia. Et voi enää suoraan vain tulostaa itemiä vaan sinun on tulostettava esim. item.malli jne. Kumpaa luokkaa on järkevämpi muuttaa?
List-luokkaa voisi muokata seuraavanlaisesti:
lista.push( <li key={i.key}>{i.malli} {i.valmistaja}</li> )Samaan tapaan sinun on muokattava myös suodatusta.
Miten tekisit muutokset muuttamalla vain FilteredList-luokkaa? Tässä tapauksessa lienee järkevämpi muuttaa List-luokkaa, koska siihen täytyy jatkossakin lisätä ominaisuuksia.
- Lisää komponenttihierarkiassasi ylimmälle tasolle uusi komponentti, jonka nimi voi olla esim. Cars. Lisää tämän komponentin tilaan automallilistausta. Käytä tämän komponentin render-metodissa FilteredList-komponentti, jolle viet propseina kaiken tarvittavan tiedon. Kaikki automallilistaukseen kohdistuvat muokkaukset täytyy jatkossa tehdä Cars-komponentin metodeilla, koska automallit ovat tallessa sen tilassa.
- Lisää automallilistauksen jokaiselle riville
kuva, jota klikkaamalla kyseinen rivi poistetaan kokonaan listauksesta.
Kts. Communicate Between Components ja
Expose Component Functions
- Haluat kutsua eri komponentissa olevaa metodia.
List-komponentin on osattava kutsua Cars-komponentissa olevaa metodia, joka
poistaa (filteroi) pois valitun alkion. Sinun täytyy viedä käytettävä metodi
attribuuttina ensin FilteredList-komponentille, joka vie sen List-komponentille,
joka sitten pystyy käyttämään sitä this.props-taulukon kautta.
Cars-komponentin render-metodissa pitäisi olla jotain seuraavanlaista:
<FilteredList items={this.state.items} removeItem={this.removeItem} />Samaan tapaan FilteredList-komponentin render-metodissa:
<List items={this.props.items} removeHandler={this.props.removeItem} />Lisää Cars-komponenttiin seuraavanlainen removeItem-metodi:
removeItem(clickedItem){ // poistaminen hoituu näppärästi filterin avulla // https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Array/filter let items = this.state.items.filter(function(item){ return !(item.key == clickedItem) }); this.setState({ "items": items }); } } - Joudut kutsumaan List-luokassa olevan img-elementin click-käsittelyssä
Cars-komponentin removeHandler-funktiota. Tässä kutsussa joudut myös viemään
tiedon klikatun objektin avaimesta, jotta Cars-komponentti osaa poistaa oikean
objektin. FilteredList-komponentti toimii välittäjänä.

Tätä kutsua ei voi suoraan laittaa generoitavaan html-koodiin, koska silloin javascript-tulkki kutsuisi suoraan tätä funktiota eikä vasta riviä klikattaessa. Ongelma kierretään luomalla erikseen anonyymifunktio, joka kutsuu halutulla parametrilla poistometodia. Asetetaan tämä funktio List-komponentissa onClick-käsittelijäksi. Tässä joudutaan myös temppuilemaan this-muuttujan kanssa:
render(){ // this täytyy ottaa talteen, koska se muuttuu suorituskontekstin mukaan let self = this; let lista = [] for( let i of this.props.items ) { // avain jolla tunnistetaan poistettava objekti let key = i.key; // apufunktio, joka vasta kutsuu propseissa annettua poistofunktiota let removeHandler = (function(event) { self.props.removeHandler(key) }); ...Käytetään seuraavasti:
<img src="https://appro.mit.jyu.fi/tiea2120/ohjaus/ohjaus6/remove.gif" onClick={removeHandler} />
- Haluat kutsua eri komponentissa olevaa metodia.
List-komponentin on osattava kutsua Cars-komponentissa olevaa metodia, joka
poistaa (filteroi) pois valitun alkion. Sinun täytyy viedä käytettävä metodi
attribuuttina ensin FilteredList-komponentille, joka vie sen List-komponentille,
joka sitten pystyy käyttämään sitä this.props-taulukon kautta.
Cars-komponentin render-metodissa pitäisi olla jotain seuraavanlaista:
- Lisää List-komponenttiin jokaiselle luomallesi autolistausriville
onClick-käsittely,joka
valitsee klikatun rivin ja korostaa sen. Rivin uudelleen klikkaaminen poistaa valinnan ja korostuksen. Huomaa, että
poistamisen pitää edelleen toimia. Älä siis lisää uutta click-käsittelyä koko lista-alkiolle vaan vain listassa oleviin
teksteihin (malli ja valmistaja).
- Lisää Cars-luokkaan selectItem-metodi, joka hoitaa rivin valinnan ja valinnan poistamisen. Voit keksiä riville (item, clickedItem) uuden attribuutin selected. Esim. item.selected = True; tarkoittaisi valittua riviä ja item.selected = False; valitsematonta riviä. Muista päivittää tila.
- Huomioi item.selected myös List-komponentissa eli palauta erilainen rivi sen mukaan onko rivi valittu vai ei. Valittu automerkki voidaan merkitä esim. strong-elementillä
- Vie Cars-luokan selectItem-metodi samaan tapaan parametrina FilteredList-komponentille ja edelleen List-komponentitlle, kuin teit poistamisen yhteydessä
- Aseta laskuri lisäämään aina yhtä monta autoa laskuriin kuin on valittuja
(korostettuja) rivejä. Jos yhtään riviä ei ole valittuna niin laskuriakaan ei
kasvateta.
Sinun on sijoitettava laskurikomponentti nyt Cars-luokan sisään, jotta pystyt viemään laskurille parametrina (attribuuttina) tiedon siitä montako autoa kulloinkin on valittuna.
On houkutteleva ajatus lisätä Cars-luokkaan uusi tilamuuttuja, joka sisältäisi valittujen rivien lukumäärän. Näin ei kuitenkaan kannata tehdä vaan parempi ratkaisu on laskea valittujen rivien lukumäärä render-metodissa tai ihan erillisessä metodissa.
React.PureComponent
Reactin komponentit voi periä joko React.Component tai React.PureComponent-luokasta. Nämä toimivat muuten samalla tavalla, mutta React.Component päivittää sivun sisällön herkemmin kuin React.PureComponent. React.PureComponentissa on valmiina tilan muutosten vertailu shouldComponentUpdate-tapahtumassa. Vertailu on kuitenkin pintapuolinen (shallow) eli se ei havaitse muutoksia, jos tilan rakenne on kompleksinen eikä sitä kopioida kunnolla. React.PureComponent on hyvin toteutetussa ohjelmassa tehokkaampi, koska se ei tee turhia päivityksiä sivun sisältöön.
Todennäköisesti olet suoraan mutatoinut komponenttiesi tilaa. Olet muistanut käyttää setState-metodia, mutta luultavimmin et ole tehnyt muutoksia tilan kopioon vaan suoraan tilassa olevaan taulukkoon tai objekteihin. Kts. How to accidentally mutate state. And why not to.
Korjataan tilan käsittelyyn liittyvät virheet:
- Peri kaikki komponenttisi React.PureComponent-luokasta. Vieläkö sovellus toimii? Mitkä osat toimivat ja mitkä eivät? Miksi?
- Toimiiko automerkkien poistaminen? Jos toteutit poistamisen filter-metodilla
niin tämä voi toimia, koska filter luo uuden taulukon. Tämä taulukko on kuitenkin sisällön
puolesta vain pinnallinen (shallow) kopio alkuperäisestä tilasta. Tässä ohjelmassa
tuo riittää, mutta ei välttämättä aina.
// shallow copy let items = this.state.items.filter(function(item){ return !(item.key == clickedItem) }); this.setState({ "items": items }); - Automerkin valitsemisessa täytyy tehdä taulukosta oikea kopio, koska
pinnallinen vertailu ei muuten havaitse objektien muuttuneita attribuutteja (selected)
selectItem(clickedItem){ // tehdään deep copy -tilasta. Tarvitaan kopioi_item-apufunktiota let items = Array.from(this.state.items, kopioi_item); for (let item of items) { if (clickedItem == item.key) { if ( item.selected ) { item.selected = false; } else { item.selected = true; } break; } } this.setState({ "items": items }); }Array.from-funktio tarvitsee objektien kopioimiseen apufunktion, joka osaa kopioida oikein kohteena olevat objektit. Jos tietorakenne on syvempi eli löytyy sisäkkäisiä objekteja ja taulukoita, täytyy jokainen taso kopioida erikseen.
// apufunktio, joka kopioi yhden itemin. Käytetään this.state.itemsin kopioinnissa function kopioi_item(item) { let uusi = { "key": item.key, "malli": item.malli, "valmistaja": item.valmistaja, "selected": item.selected }; return uusi; } - Korjaa sovellustasi siten, että se toimii, jos komponentit on peritty React.PureComponent-luokasta
Lomakkeet
- Lisää sivulle lomake jolla voi lisätä uusia automerkkejä sivulla
näkyvään listaan.
Lue Forms. Oletuksena Reactissa lomakkeet ovat
kontrolloituja komponentteja. Käytä kontrolloitua lomaketta aina kuin vain mahdollista.
Jossain tilanteessa voit tehdä lomakkeesta myös kontrolloimattoman komponentin.
Kts. esimerkit:
kontrolloitu lomake (lähdekoodi) ja
kontrolloimaton lomake (lähdekoodi).
- Tee koko lomakkeesta oma komponentti. Peri React.PureComponent-luokasta.
- Valitse merkin valmistaja valmiista listasta. Tee oma komponentti valintalistasta (select). Tallenna valmis lista lomakekomponenttiin.
- Lomakekomponentti käyttää valintalistakomponenttia ja luo muut lomakkeen tarvitsemat asiat. Huomaa erityisesti Reactin erikoisuudet Select-elementin kanssa ja defaultValue-attribuutin käyttäminen.
- Lomake ottaa vastaan tiedon valintalistan valinnasta (onChange) ym. ja tallentaa Lisää-painikkeen painalluksen jälkeen uuden automerkin Cars-komponentin tilaan.
- Varmista, että lomake nollaantuu lisäämisen jälkeen alkutilaansa. Tämä hoituu helpoiten muuttamalla lomake-elementille annetun keyn arvoa jolloin react hoksaa, että ahaa nyt on uusi lomake ja päivittää kaiken uudelleen.
- Lisää myös järkevät virheenkäsittelyt. Tarkista lomakekomponentissa syötteiden oikeellisuus ja jos automerkkikenttä on tyhjä niin älä tee lisäystä vaan aseta lomakkeelle virheilmoitus näkyviin. Kts. validation-esimerkki
- Käy lopuksi ajatuksella läpi Thinking in React-artikkeli. Yritä toteuttaa artikkelissa selitetty ohjelma.
Esimerkit
malliratkaisu 2020 toteutettuna class-komponenteilla
malliratkaisu 2022 toteutettuna function-komponenteilla
Vanhemmat esimerkit ja vanhemmalla syntaksilla: Malliesimerkki ja Toinen malli
Esimerkki reactin date-picker-komponentin käytöstä. Huom. pitää erikseen hakea ja ottaa sivulla käyttöön datepicker- ja moment-javascript-kirjastot.

Käyttäjien kommentit