React
Harjoitellaan Reactin funktiopohjaisia komponentteja. Näistä tehtävistä löytyy myös vanhempi luokkapohjainen versio, mutta funktiopohjainen toteutus on helpompi ja yksinkertaisempi. Uusimmat React-sovellukset käyttävät funktiopohjaisia komponentteja.
Ennen näiden tehtävien tekemistä,katso seuraavat luennot. Huom. näissä luentomateriaaleissa käytetään vanhempaa luokkapohjaista syntaksia, mutta reactin toimintaperiaate on kuitenkin edelleen sama.
- 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 Quick Start -tutoriaali Käytä tutoriaalia apunasi seuraavissa tehtävissä.
Huom. monissa react-ohjeissa neuvotaan asentamaan node ym. työkaluja. Mitään näitä ei tarvitse tällä kurssilla asentaa vaan pääset alkuun yksinkertaisemmin tämän sivun ohjeiden avulla.- 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@18/umd/react.development.js"></script> <script crossorigin src="https://unpkg.com/react-dom@18/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"> const root = ReactDOM.createRoot( document.getElementById('root')); root.render( /* jshint ignore:start */ <h1>Hello, world!</h1>, /* jshint ignore:end */ ); </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. Lisätietoa: Add React to a Website
- Yleensä oma koodi kannattaa kirjoittaa omaan javascript-tiedostoon. Silloin se pitää linkittää pohjaan tällä tavalla:
JSHint ei tunnista JSX-koodia. JSX-koodilohkot voi sijoittaa seuraavien kommenttien sisään:<script type="text/jsx" src="oma_react_koodi.js"></script>
Huom! Debuggerissa oman koodin debuggaus ei toimi tavalliseen tapaan, koska Babel-kirjasto muuntaa koodisi lennossa javascript-koodiksi. Katso React Developer toolsin lisäämät uudet välilehdet Components ja Profiler. Näiden käyttämisestä löytyy hyvä demonstraatio. 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:/* jshint ignore:start */ Tähän väliin JSX-koodia /* jshint ignore:end */
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 (count) ja return-lauseessa
palauttaa laskurimuuttujan arvon JSX-koodin seassa.
Komponentit pitää määritellä ennen root.render-kutsua eli root.render-kutsun on aina oltava tiedostossa viimeisenä. Lisää render-kutsuun oma komponenttisi:let Counter = function(props) { // komponentin tilaan liittyvä muuttuja (count) ja sen käsittelyfunktio (setCount) // tilan alkuarvoksi asetetaan 0 // tilaan tarvitsee sijoittaa vain ne muuttujat, jotka vaikuttavat komponentin renderointiin eli sen HTML-sisältöön const [count, setCount] = React.useState(0); // JSX-koodia, jonka seassa voi käyttää muuttujia {}-merkinnän sisällä return (<div> <p>{count}</p> </div>); }
Render-funktiossa voi olla parametrina vain yksi elementti eli tässä tapauksessa on laitettava div-elementin sisään kaikki muut elementit.root.render( <div> <h1>Hello, world!</h1>, <Counter /> </div> );
- Lisää komponenttiisi varsinainen laskuriominaisuus eli painike,
joka kasvattaa laskurin arvoa. Tarvitset komponenttiin metodin, joka
kasvattaa laskuria ja return-lauseen JSX-koodiin sinun pitää lisätä
click-käsittelijä, joka sitten kutsuu metodiasi.
Komponentin tilan muuttaminen pitää aina tehdä sitä varten luodulla
funktiolla (setCount).
Komponentin tilan muuttaminen ei vaikuta count-muuttujan arvoon, kuin
vasta seuraavalla komponentin suorituskerralla (piirtokerralla).
Lisää seuraava funktio Counter-funktiosi sisään:
Nyt kasvataLaskuria-funktiota voi kutsua click-tapahtumassa. Reactissa on oma tapahtumankäsittely, joka eroaa hitusen aiemmin opitusta. Tapahtumankäsittelijät asetetaan suoraan JSX-koodissa:const kasvataLaskuria = function(e) { // muutetaan tilamuuttujan arvoa setCount( count + 1 ); console.log( count ); // count-muuttujan arvo ei nyt vielä muutu! }
return (<div> <p>{count}</p> <button onClick={kasvataLaskuria}>Lisää</button> </div>);
Kokeile toimiiko laskuri
- Luo ensin oma React-komponentti, joka sisältää yhden tilamuuttujan (count) ja return-lauseessa
palauttaa laskurimuuttujan arvon JSX-koodin seassa.
- Tutustu tarkemmin tilan muuttamiseen useState-dokumentin
avulla. Lue erityisesti Troubleshooting-osio.
- Tilan muuttaminen ei muuta tilamuuttujan sen hetkistä arvoa
- Sivu ei päivity, vaikka muutat tilaa. React ei päivitä sivua, jos uuden tilan arvo on sama kuin vanha. Tilaan liittyviä objekteja ja taulukoita ei saa suoraan muuuttaa, vaan niistä on aina tehtävä kopio, jolla uusi tila asetetaan
- Funktio suoritetaan kahdesti, jos
käytössä on Strict Mode.
Strict Modessa React suorittaa samoja funktioita kahdesti ja yrittää tällä
tavalla löytää niistä virheitä ja vääriä käytänteitä. Strict Modea kannattaa
käyttää. Strict Moden saa päälle sisällyttämällä halutut komponentit
React.StrictMode-elementin sisään:
<React.StrictMode> <Counter /> </React.StrictMode>
-
Huom!. Jos tallennat tilaan objekteja tai taulukoita, ole tarkkana tilaa kopioitaessa. Tee tarvittaessa deep copy. Niitä osia tilasta, jotka eivät muutu, ei tarvitse deep kopioida. 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 pidä sisällyttää komponentin tilaan.
- Lisää laskurin alapuolelle listaus automalleista. Luo oma
lista-komponentti jolle tuot parametrina (props.items) listassa
esitettävät automallit (Polo, Micra, Corsa, Kadett, 1MW Concept, Fiesta,
A1, Model S ).
- Luo ensin List-komponentti. 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ää JSX-koodin seassa:const List = function(props) { // return-lauseeseen ei voi sijoittaa normaaleja silmukoita // täytyy luoda taulukko etukäteen tai käyttää return-lauseessa map-funktio$ let lista = [] for( let alkio of props.items ) { lista.push( <li key={alkio}>{alkio}</li> ); } return <ul>{lista}</ul> // saman voisi tehdä suoraan return-lauseessa map-funktion avulla: // props.items.map(function(alkio) { // return <li key={alkio}>{alkio}</li> }
Ts. muuta render-kutsu seuraavanlaiseksi:<List items={["Polo", "Micra", "Corsa", "Kadett", "1MW Concept", "Fiesta", "A1", "Model S"]}/>
root.render(<div> <React.StrictMode> <Counter /> <List items={["Polo", "Micra", "Corsa", "Kadett", "1MW Concept", "Fiesta", "A1", "Model S"]}/> </React.StrictMode> </div> );
- Kokeile toimiiko listaus.
- Luo ensin List-komponentti. 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ä FilteredList-komponentissa jo aiemmin luomaasi List-komponenttia. Lisää FilteredList-komponenttiin input-elementti, jota voi käyttää suodatukseen
- Lisää FilteredList-komponenttiin filter-tilamuuttuja, jonka alustat
tyhjäksi merkkijonoksi.
const [filter, setFilter] = React.useState("");
- Tee funktio, joka päivittää suodatukseen käytettävän
merkkijonon.
let filterList = function(e) { setFilter( e.target.value ); }
- Liitä return-lauseessa edellä tekemäsi funktio input-kentän
change-tapahtumaan:
<input type="text" onChange={filterList} />
- Varsinainen suodatus pitää myös tehdä return-lauseessa
Kts.
filter()-metodi.
Lisää seuraava suodatus FilteredList-komponen return-osassa käytetyn List-komponentin esiintymän items-parametrin arvoksi:
Kokeile toimiiko suodatus. Muistithan korvata root.render-kutsussa List-komponentin FilteredList-komponentilla.props.items.filter(function(item){ return item.toLowerCase().includes( 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"}]}/>
Nyt joudut muuttamaan joko List-komponentin tai FilteredListia. Et voi enää suoraan vain tulostaa itemiä vaan sinun on tulostettava esim. item.malli jne. Kumpaa luokkaa on järkevämpi muuttaa?
Miten tekisit muutokset muuttamalla vain FilteredList-luokkaa? Käytä apunasi map-metodia.
Mallivastaus
map(function(item) { return `${item.malli} ${item.valmistaja}`; })
Tässä tapauksessa lienee järkevämpi muuttaa myös List-luokkaa, koska siihen täytyy jatkossakin lisätä ominaisuuksia.
List-luokkaa voisi muokata seuraavanlaisesti:
lista.push( <li key={alkio.key}>{alkio.malli} {alkio.valmistaja}</li> )
- Lisää komponenttihierarkiassasi ylimmälle tasolle uusi komponentti, jonka
nimi voi olla esim. Cars. Lisää tämän komponentin tilaan
automallitaulukko.
Käytä tässä komponentissa FilteredList-komponenttia,
jolle viet propseina kaiken tarvittavan tiedon eli alkuvaiheessa automallitaulukon.
Korvaa root-render-metodissa olevat FilteredList-komponentti Cars-komponentilla.
Kaikki automallilistaukseen kohdistuvat muokkaukset täytyy jatkossa tehdä Cars-komponentin metodeilla, koska automallit ovat tallessa sen tilassa.
- Lisätään automallilistauksen jokaisen rivin perään kuva, jota klikkaamalla kyseinen rivi poistetaan
kokonaan listauksesta.
Kts. Communicate
Between Components ja Expose Component
Functions
- Lisää ensimmäisenä kuva jokaiselle riville List-komponentissa
- Lisää seuraavaksi kuvalle click-tapahtumankäsittelijä, joka ei vielä tee mitään, kuin ehkä tulostaa viestin konsoliin
- 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ä
poistofunktio Cars-komponentista
attribuuttina ensin FilteredList-komponentille,
joka vie sen List-komponentille,
joka sitten pystyy käyttämään sitä props-taulukon kautta.
Cars-komponentissa pitäisi olla jotain seuraavanlaista:
<FilteredList items={autot} removeItem={removeItem} />
Samaan tapaan FilteredList-komponentissa viedään sama funktio-osoitin List-komponentille:
<List items={props.items} removeItem={props.removeItem} />
Lisää Cars-komponenttiin seuraavanlainen removeItem-metodi:
let removeItem = function(clickedItem){ // poistaminen hoituu näppärästi filterin avulla // https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Array/filter let items = autot.filter(function(auto){ return !(auto.key == clickedItem) }); setAutot(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.
Käytetään seuraavasti:
for( let alkio of props.items ) { let removeHandler = function(e) { // kutsutaan ylemmän tason komponentilta parametrina saatua removeItem-funktiota props.removeItem(alkio.key); } lista.push( <li key={alkio.key}>{alkio.malli} {alkio.valmistaja} <img src="https://appro.mit.jyu.fi/tiea2120/ohjaus/ohjaus6/remove.gif" onClick={removeHandler} /> </li> ); }
- Lisää sovellukseen mahdollisuus valita yksi tai useampi autolistausrivejä.
- 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 muodosta autolistaukseen erilainen rivi sen mukaan onko rivi valittu vai ei. Voit esim. merkitä valittujen rivien tekstit strong-elementillä.
- Vie Cars-luokan selectItem-metodi samaan tapaan parametrina FilteredList-komponentille ja edelleen List-komponentitlle, kuin teit poistamisen yhteydessä
- Aseta jokaiselle luomallesi autolistausriville onClick-käsittely,joka valitsee klikatun rivin ja korostaa sen. Rivin uudelleen klikkaaminen poistaa valinnan ja korostuksen. Poistamisen täytyy kuitenkin edelleen toimia. Älä siis lisää uutta click-käsittelyä koko lista-alkiolle vaan vain listassa oleviin teksteihin (malli ja valmistaja). Sinun täytyy sijoittaa nämä tekstit oman elementin sisään. Esim. span.
- Toimiiko? Jos ei toimi, varmista, että päivität Cars-komponentin tilan uudella taulukolla, etkä suoraan samalla autot-taulukolla, joka on jo tilassa käytössä.
- 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ä aina tarvittaessa eli suoraan Cars-komponentin rungossa ennen return-lausetta. Why you should avoid using state for computed properties.
memo
Oletuksena React renderoi komponentin uudelleen aina, kun komponentin vanhempi komponentti renderoituu uudelleen. Toimintaa voi tehostaa, jos renderointi tehdään uudelleen, vain komponentin propsien muuttuessa. Luokkapohjaisissa react-komponenteissa perittäisiin PureComponent-luokasta. Funktiopohjaisissa komponenteissa voidaan hyödyntää memoa. PureComponent ja memolla toteutettu memoized-versio funktiosta sisältävät pintapuolisen (shallow) vertailun komponenttien propsien muuttumisesta.
Tee List- ja FilteredList-komponenteista memoization-versiot seuraavaan tapaan:
const FilteredList = React.memo(function(props) {
...
});
Vieläkö sovelluksesi toimii? Jos toimii, niin hienoa :-) Mahdollisesti olet suoraan mutatoinut komponenttiesi tilaa. Olet muistanut päivittää myös tilaa, mutta ehkä et ole tehnyt muutoksia tilan kopioon vaan suoraan tilassa olevaan taulukkoon tai objekteihin. Kts. How to accidentally mutate state. And why not to.
Miksi laskurikomponentista ei kannata tehdä memoization-versiota? Mitä jos kuitenkin teet? Toimiiko komponentti edelleen? Miksi? Kts. Minimizing props changes .
Toimiiko automerkkien poistaminen? Jos toteutit poistamisen filter-metodilla niin tämä pitäisi toimia, koska filter luo aina 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. Mahdollisen deep copyn tarve täytyy aina miettiä tapauskohtaisesti.
Korjaa sovelluksesi mahdolliset bugit, että se toimii myös memoization-optimoinnin kera.
Lisätietoa: Use React.memo() wisely, Before You memo() ja When should you memoize in React
Lisäoptimointia voi yrittää tehdä useMemo ja useCallback avulla.
Mallivastaus, mallin react-koodi
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.
- 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 Kontrolloidussa lomakkeessa riittää, että asetat Select-elementille value-attribuutin arvoksi valittuna olevan vaihtoehdon. Käytä Option-elementtien value-attribuuteissa jokaiselle valinnalle uniikkia key-arvoa tai muuta tunnistetta.
- 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 eli alusta kaikki lomakkeen tilaa vastaavat muuttujat.
- Lisää myös järkevät virheenkäsittelyt. Tarkista lomakekomponentissa syötteiden oikeellisuus ja jos automerkkikenttä on tyhjä, älä tee lisäystä vaan aseta lomakkeelle virheilmoitus näkyviin. event.target on entiseen tapaan elementti, jolle tapahtuma tapahtuu. Lomake-elementtien kautta pääsee aina käsiksi kyseiseen lomakkeeseen (event.target.form) ja sitä kautta edelleen muihin lomakkeen osasiin. Virheilmoitusten asettaminen setCustomValidity-metodilla on harvoja tilanteita, jossa Reactissa täytyy tällä tavalla käsitellä sivua DOM-rajapinnan kautta. Kts. validation-esimerkki
- Käy lopuksi ajatuksella läpi Thinking in React-artikkeli.
malliratkaisu 2022 toteutettuna function-komponenteilla. React-lähdekoodi.
React, Fetch ja useEffect
Kts. esimerkki, jos haluat käyttää Fetchiä React-komponentin tilan alustamisessa ja päivittämisessä. Lähdekoodi.
Refs ja DOM-objektit
Vanhat esimerkit
malliratkaisu 2020 toteutettuna class-komponenteilla
Vanhemmat esimerkit ja vanhemmalla syntaksilla: Malliesimerkki ja Toinen malli
Lisätietoa
- Let’s learn how modern JavaScript frameworks work by building one
- Fetching data with Effects
- Getting Started with React - An Overview and Walkthrough
- Thinking in React
- More About Refs
- Getting To Know Flux, the React.js Architecture
- Creating A Simple Shopping Cart with React.js and Flux
- react-quickstart
Käyttäjien kommentit