a

Reactin alkeet

Alamme nyt tutustua kurssin ehkä tärkeimpään teemaan, React-kirjastoon. Tehdään heti yksinkertainen React-sovellus ja tutustutaan samalla Reactin peruskäsitteistöön.

Ehdottomasti helpoin tapa päästä alkuun on create-react-app-nimisen työkalun käyttö. create-react-app on mahdollista asentaa omalle koneelle, mutta asennukseen ei ole tarvetta jos Noden mukana asentunut npm-työkalu on versioltaan vähintään 5.3. Tällöin npm:n mukana asentuu komento npx, joka mahdollistaa create-react-app:in käytön asentamatta sitä erikseen. Npm:n version saa selville komennolla npm -v.

Luodaan sovellus nimeltään part1 ja mennään sovelluksen sisältämään hakemistoon:

npx create-react-app part1
cd part1

Sovellus käynnistetään seuraavasti:

npm start

Sovellus käynnistyy oletusarvoisesti localhostin porttiin 3000, eli osoitteeseen http://localhost:3000.

Chromen pitäisi aueta automaattisesti. Avaa konsoli välittömästi. Avaa myös tekstieditori siten, että näet koodin ja web-sivun samaan aikaan ruudulla:

fullstack content

Sovelluksen koodi on hakemistossa src. Yksinkertaistetaan valmiina olevaa koodia siten, että tiedoston index.js sisällöksi tulee:

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

import App from './App'

ReactDOM.createRoot(document.getElementById('root')).render(<App />)

ja tiedoston App.js sisällöksi

const App = () => (
  <div>
    <p>Hello world</p>
  </div>
)

export default App

Tiedostot App.css, App.test.js, index.css, logo.svg, reportWebVitals.js ja setupTests.js voi poistaa, sillä emme tarvitse niitä.

Komponentti

Tiedosto App.js määrittelee nyt React-komponentin nimeltään App. Tiedoston index.js viimeisen rivin komento

ReactDOM.createRoot(document.getElementById('root')).render(<App />)

renderöi komponentin sisällön tiedoston public/index.html määrittelemään div-elementtiin, jonka id:n arvona on 'root'.

Tiedosto public/index.html on headerin määrittelyjä lukuunottamatta oleellisesti ottaen tyhjä:

<!DOCTYPE html>
<html lang="en">
  <head>
      content not shown ...
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>

Voit kokeilla lisätä tiedostoon HTML:ää. Reactilla ohjelmoitaessa yleensä kuitenkin kaikki renderöitävä sisältö määritellään Reactin komponenttien avulla.

Tarkastellaan vielä tarkemmin komponentin määrittelevää koodia:

const App = () => (
  <div>
    <p>Hello world</p>
  </div>
)

Kuten arvata saattaa, komponentti renderöityy div-tagina, jonka sisällä on p-tagin sisällä oleva teksti Hello world.

Teknisesti ottaen komponentti on määritelty JavaScript-funktiona. Seuraava on siis funktio (joka ei saa yhtään parametria):

() => (
  <div>
    <p>Hello world</p>
  </div>
)

joka sijoitetaan vakioarvoiseen muuttujaan App

const App = ...

JavaScriptissa on muutama tapa määritellä funktioita. Käytämme nyt JavaScriptin hieman uudemman version ECMAScript 6:n eli ES6:n nuolifunktiota (arrow functions).

Koska funktio koostuu vain yhdestä lausekkeesta, käytössämme on lyhennysmerkintä, joka vastaa seuraavaa koodia:

const App = () => {
  return (
    <div>
      <p>Hello world</p>
    </div>
  )
}

eli funktio palauttaa sisältämänsä lausekkeen arvon.

Komponentin määrittelevä funktio voi sisältää mitä tahansa JavaScript-koodia. Muuta komponenttisi seuraavaan muotoon:

const App = () => {
  console.log('Hello from komponentti')
  return (
    <div>
      <p>Hello world</p>
    </div>
  )
}

export default App

ja katso mitä selaimen konsolissa tapahtuu:

fullstack content

Web-sovelluskehityksen sääntö numero yksi on

pidä konsoli koko ajan auki

Toistetaan tämä vielä yhdessä: pidän konsolin koko ajan auki tamän kurssin ja koko loppuelämäni ajan tehdessäni web-sovelluskehitystä.

Komponenttien sisällä on mahdollista renderöidä myös dynaamista sisältöä.

Muuta komponentti muotoon:

const App = () => {
  const now = new Date()
  const a = 10
  const b = 20
  console.log(now, a+b)

  return (
    <div>
      <p>Hello world, it is {now.toString()}</p>
      <p>
        {a} plus {b} is {a + b}
      </p>
    </div>
  )
}

Aaltosulkeiden sisällä oleva JavaScript-koodi evaluoidaan ja evaluoinnin tulos upotetaan määriteltyyn kohtaan komponentin tuottamaa HTML-koodia.

Huom: älä poista tiedoston lopusta riviä

export default App

Kyseistä riviä ei useimmiten näytetä materiaalin esimerkeissä mutta ilman sitä komponentti ja koko ohjelma hajoaa.

Muistitko pitää konsolin auki? Mitä sinne tulostui?

JSX

Näyttää siltä, että React-komponentti palauttaa HTML-koodia. Näin ei kuitenkaan ole. React-komponenttien ulkoasu kirjoitetaan yleensä JSX:ää käyttäen. Vaikka JSX näyttää HTML:ltä, kyseessä on kuitenkin tapa kirjoittaa JavaScriptia. React-komponenttien palauttama JSX käännetään konepellin alla JavaScriptiksi.

Käännösvaiheen jälkeen komponentin määrittelevä koodi näyttää seuraavalta:

const App = () => {
  const now = new Date()
  const a = 10
  const b = 20
  return React.createElement(
    'div',
    null,
    React.createElement(
      'p', null, 'Hello world, it is ', now.toString()
    ),
    React.createElement(
      'p', null, a, ' plus ', b, ' is ', a + b
    )
  )
}

Käännöksen hoitaa Babel. Create-react-app:illa luoduissa projekteissa käännös on konfiguroitu tapahtumaan automaattisesti. Tulemme tutustumaan aiheeseen tarkemmin kurssin osassa 7.

Reactia olisi mahdollista kirjoittaa myös "suoraan JavaScriptinä" käyttämättä JSX:ää, mutta tämä ei ole järkevää.

Käytännössä JSX on melkein kuin HTML:ää sillä erotuksella, että mukaan voi upottaa helposti dynaamista sisältöä kirjoittamalla sopivaa JavaScriptia aaltosulkeiden sisälle. Idealtaan JSX on melko lähellä monia palvelimella käytettäviä templating-kieliä kuten Java Springin yhteydessä käytettävää Thymeleafia.

JSX on "XML:n kaltainen", eli jokainen tagi tulee sulkea. Esimerkiksi rivinvaihto on tyhjä elementti, joka voidaan kirjoittaa HTML:ssä seuraavasti

<br>

mutta JSX:ää kirjoittaessa tagi on pakko sulkea:

<br />

Monta komponenttia

Muutetaan tiedostoa App.js seuraavasti (muista, että alimman rivin export jätetään esimerkeistä nyt ja jatkossa pois, niiden on kuitenkin oltava koodissa jotta ohjelma toimisi):

const Hello = () => {  return (    <div>      <p>Hello world</p>    </div>  )}
const App = () => {
  return (
    <div>
      <h1>Greetings</h1>
      <Hello />    </div>
  )
}

Olemme määritelleet uuden komponentin Hello, jota käytetään komponentista App. Komponenttia voidaan luonnollisesti käyttää monta kertaa:

const App = () => {
  return (
    <div>
      <h1>Greetings</h1>
      <Hello />
      <Hello />      <Hello />    </div>
  )
}

Komponenttien tekeminen Reactissa on helppoa ja komponentteja yhdistelemällä monimutkaisempikin sovellus on mahdollista pitää kohtuullisesti ylläpidettävänä. Reactissa filosofiana onkin koostaa sovellus useista, pieneen asiaan keskittyvistä uudelleenkäytettävistä komponenteista.

Vahva konventio on myös se, että sovelluksen ylimpänä oleva juurikomponentti on nimeltään App. Tosin kuten osassa 6 tulemme näkemään, on tilanteita, joissa komponentti App ei ole suoraan juuressa, vaan se kääritään sopivan apukomponentin sisään.

props: tiedonvälitys komponenttien välillä

Komponenteille on mahdollista välittää dataa propsien avulla.

Muutetaan komponenttia Hello seuraavasti:

const Hello = (props) => {  return (
    <div>
      <p>Hello {props.name}</p>    </div>
  )
}

Komponentin määrittelevällä funktiolla on nyt parametri props. Parametri saa arvokseen olion, jonka kenttinä ovat kaikki eri "propsit", jotka komponentin käyttäjä määrittelee.

Propsit määritellään seuraavasti:

const App = () => {
  return (
    <div>
      <h1>Greetings</h1>
      <Hello name="Maya" />      <Hello name="Pekka" />    </div>
  )
}

Propseja voi olla mielivaltainen määrä ja niiden arvot voivat olla "kovakoodattuja" merkkijonoja tai JavaScript-lausekkeiden tuloksia. Jos propsin arvo muodostetaan JavaScriptillä, sen tulee olla aaltosulkeissa.

Muutetaan koodia siten, että komponentti Hello käyttää kahta propsia:

const Hello = (props) => {
  console.log(props)  return (
    <div>
      <p>
        Hello {props.name}, you are {props.age} years old      </p>
    </div>
  )
}

const App = () => {
  const nimi = 'Pekka'  const ika = 10
  return (
    <div>
      <h1>Greetings</h1>
      <Hello name="Maya" age={26 + 10} />      <Hello name={nimi} age={ika} />    </div>
  )
}

Komponentti App lähettää propseina muuttujan arvoja, summalausekkeen evaluoinnin tuloksen ja normaalin merkkijonon.

Komponentti Hello myös tulostaa props-olion arvon konsoliin.

Toivottavasti konsolisi on auki, jos ei ole, muista yhteinen lupauksemme:

pidän konsolin koko ajan auki tamän kurssin ja koko loppuelämäni ajan tehdessäni web-sovelluskehitystä

Ohjemistokehitys on haastavaa, ja erityisen haastavaksi se muuttuu, jos jokainen mahdollinen apukeino kuten web-konsoli sekä komennolla console.log tahtävät aputulostukset eivät ole käytössä. Ammattilaiset käyttävät näitä aina. Ei ole yhtään syytä miksi aloittelijan pitäisi jättää nämä fantastiset apuvälineet hyödyntämättä.

Muutamia huomioita

React on konfiguroitu antamaan varsin hyviä virheilmoituksia. Kannattaa kuitenkin edetä ainakin alussa todella pienin askelin ja varmistaa, että jokainen muutos toimii halutulla tavalla.

Konsolin tulee olla koko ajan auki. Jos selain ilmoittaa virheestä, ei kannata kirjoittaa sokeasti lisää koodia ja toivoa ihmettä tapahtuvaksi, vaan tulee yrittää ymmärtää virheen syy ja esim. palata edelliseen toimivaan tilaan:

fullstack content

Kuten jo todettiin, myös React-koodissakin on mahdollista ja kannattavaa lisätä koodin sekaan sopivia konsoliin tulostavia console.log()-komentoja. Tulemme hieman myöhemmin tutustumaan muutamiin muihinkin tapoihin debugata Reactia.

Kannattaa pitää mielessä, että React-komponenttien nimien tulee alkaa isolla kirjaimella. Jos yrität määritellä komponentin seuraavasti:

const footer = () => {
  return (
    <div>
      greeting app created by 
      <a href="https://github.com/mluukkai">mluukkai</a>
    </div>
  )
}

ja ottaa sen käyttöön

const App = () => {
  return (
    <div>
      <h1>Greetings</h1>
      <Hello name="Maya" age={26 + 10} />
      <footer />    </div>
  )
}

sivulle ei kuitenkaan ilmesty näkyviin Footer-komponentissa määriteltyä sisältöä, vaan React luo sivulle ainoastaan tyhjän footer-elementin. Jos muutat komponentin nimen alkamaan isolla kirjaimella, React luo sivulle div-elementin, joka määriteltiin Footer-komponentissa.

Kannattaa pitää mielessä myös, että React-komponentin sisällön tulee (yleensä) sisältää yksi juurielementti. Eli jos yrittäisimme määritellä komponentin App ilman uloimmaista div-elementtiä

const App = () => {
  return (
    <h1>Greetings</h1>
    <Hello name="Maya" age={26 + 10} />
    <Footer />
  )
}

seurauksena on virheilmoitus:

fullstack content

Juurielementin käyttö ei ole ainoa toimiva vaihtoehto, myös taulukollinen komponentteja on validi tapa:

const App = () => {
  return [
    <h1>Greetings</h1>,
    <Hello name="Maya" age={26 + 10} />,
    <Footer />
  ]
}

Määritellessä sovelluksen juurikomponenttia, tämä ei kuitenkaan ole järkevää ja näyttää koodissakin pahalta.

Juurielementin pakollisesta käytöstä on se seuraus, että sovelluksen DOM-puuhun tulee "ylimääräisiä" div-elementtejä. Tämä on mahdollista välttää käyttämällä fragmentteja, eli ympäröimällä komponentin palauttamat elementit tyhjällä elementillä:

const App = () => {
  const name = 'Pekka'
  const age = 10

  return (
    <>
      <h1>Greetings</h1>
      <Hello name="Maya" age={26 + 10} />
      <Hello name={name} age={age} />
      <Footer />
    </>
  )
}

Nyt käännös menee läpi, ja Reactin generoimaan DOM:iin ei tule ylimääräistä div-elementtiä.

Älä renderöi olioita

Tarkastellaan sovellusta, joka tulostaa ruudulle ystäviemme nimet ja iät:

const App = () => {
  const friends = [
    { name: 'Leevi', age: 4 },
    { name: 'Venla', age: 10 },
  ]

  return (
    <div>
      <p>{friends[0]}</p>
      <p>{friends[1]}</p>
    </div>
  )
}

export default App

Mitään ei kuitenkaan tule ruudulle. Yritän etsiä koodista 15 minuutin ajan ongelmaa, mutta en keksi missä vika voisi olla.

Vihdoin mieleeni palaa antamamme lupaus

pidän konsolin koko ajan auki tamän kurssin ja koko loppuelämäni ajan tehdessäni web-sovelluskehitystä

Konsoli huutaakin punaisena:

fullstack content

Ongelman ydin on Objects are not valid as a React child eli sovellus yrittää renderöidä olioita ja se taas ei onnistu.

Koodissa yhden ystävän tiedot yritetään renderöidä seuraavasti

<p>{friends[0]}</p>

ja tämä aiheuttaa ongelman sillä aaltosulkeissa oleva renderöitävä asia on olio

{ name: 'Leevi', age: 4 }

Yksittäisten aaltosulkeissa renderöitävien asioiden tulee Reactissa olla primitiivisiä arvoja, kuten lukuja tai merkkijonoja.

Korjaus on seuraava

const App = () => {
  const friends = [
    { name: 'Leevi', age: 4 },
    { name: 'Venla', age: 10 },
  ]

  return (
    <div>
      <p>{friends[0].name} {friends[0].age}</p>
      <p>{friends[1].name} {friends[1].age}</p>
    </div>
  )
}

export default App

Eli nyt aaltosulkeiden sisällä renderöidään erikseen ystävän nimi

{friends[0].name}

ja ikä

{friends[0].age}

Virheen korjauksen jälkeen kannattaa konsolin virheilmoitukset tyhjentää painamalla Ø ja tämän jälkeen uudelleenladata sivun sisältö ja varmistua että virheilmoituksia ei näy.

Pieni lisähuomio edelliseen. React sallii myös taulukoiden renderöimisen jos taulukko sisältää arvoja, jotka kelpaavat renderöitäviksi (kuten nimeroita tai merkkijonoja). Eli seuraava ohjelma kyllä toimisi, vaikka tulos ei ole kenties se mitä haluamme:

const App = () => {
  const friends = [ 'Leevi', 'Venla']

  return (
    <div>
      <p>{friends}</p>
    </div>
  )
}

Tässä osassa ei kannata edes yrittää hyödyntää taulukkojen suoraa renderöintiä, palaamme siihen seuraavassa osassa.