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

Kaikki tässä (ja jatkossa) annettavat merkillä $ alkavat komennot on kirjoitettu terminaaliin eli komentoriville. Merkkiä $ ei tule kirjoittaa, sillä se edustaa komentokehotetta.

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'

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

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

Tiedostot App.js, App.css, App.test.js, logo.svg ja serviceWorker.js voi poistaa sillä niitä emme sovelluksessamme nyt tarvitse.

Komponentti

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

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

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 oleellisesti ottaen tyhjä, voit kokeilla lisätä sinne 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 siis on 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, on käytössämme lyhennysmerkintä, joka vastaa oikeasti 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 ja katso mitä konsolissa tapahtuu:

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

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

  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.

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 Javascriptiä. React komponenttien palauttama JSX käännetään konepellin alla Javascriptiksi.

Käännösvaiheen jälkeen ohjelmamme näyttää seuraavalta:

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

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
    )
  )
}

ReactDOM.render(
  React.createElement(App, null),
  document.getElementById('root')
)

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 myös mahdollista kirjoittaa "suoraan Javascriptinä" käyttämättä JSX:ää. Kukaan täysijärkinen ei kuitenkaan niin tee.

Käytännössä JSX on melkein kuin HTML:ää sillä erotuksella, että mukaan voi upottaa helposti dynaamista sisältöä kirjoittamalla sopivaa Javascriptiä 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 kirjottaa HTML:ssä seuraavasti

<br>

mutta JSX:ää kirjoittaessa tagi on pakko sulkea:

<br />

Monta komponenttia

Muutetaan sovellusta seuraavasti (yläreunan importit 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>
  )
}

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

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ä, tulee se olla aaltosulkeissa.

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

const Hello = (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.

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

Kannattaa myös muistaa, että 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 se 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 myös pitää mielessä, 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ä.