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

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 ReactDOM from 'react-dom'
import App from './App'

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

ja tiedoston App.js sisällöksi

import React from 'react'
const App = () => (
  <div>
    <p>Hello world lo23</p>
  </div>
)

export default App

Tiedostot App.css, App.test.js, logo.svg ja reportWebVitals.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.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 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 ja katso mitä konsolissa tapahtuu:

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

Huom: älä poista tiedoston App.js viimeistä riviä

export default App

muuten komponentti ei näy tiedostoon index.js!

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 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 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 kirjoittaa HTML:ssä seuraavasti

<br>

mutta JSX:ää kirjoittaessa tagi on pakko sulkea:

<br />

Monta komponenttia

Muutetaan tiedostoa App.js seuraavasti (ylärivin import ja 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) => {
  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 muistaa myös, 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 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ä.