Siirry sisältöön

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.

Helpoin tapa päästä alkuun on Vite-nimisen työkalun käyttö.

Luodaan sovellus nimeltään part1, mennään sovelluksen sisältämään hakemistoon ja asennetaan sovelluksen käyttämät kirjastot:

npm create vite@latest part1 -- --template react
cd part1
npm install

Sovellus käynnistetään seuraavasti:

npm run dev

Konsoli kertoo että sovellus on käynnistynyt localhostin porttiin 5173, eli osoitteeseen http://localhost:5173/:

fullstack content

Vite käynnistää sovelluksen oletusarvoisesti porttiin 5173. Jos se ei ole vapaana, käyttää Vite seuraavaa vapaata porttinumeroa.

Avataan selain sekä 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 main.jsx sisällöksi tulee:

import ReactDOM from 'react-dom/client'

import App from './App'

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

ja tiedoston App.jsx sisällöksi

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

export default App

Tiedostot App.css ja index.css sekä hakemiston assets voi poistaa, sillä emme tarvitse niitä.

create-react-app

Voit halutessasi käyttää kurssilla Viten sijaan myös create-react-app-nimistä sovellusta. Näkyvin ero Viteen on sovelluksen aloitustiedoston nimi, joka on index.js. Myös sovelluksen käynnistämistapa eroaa, käynnistäminen tapahtuu komennolla

npm start

Komponentti

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

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

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

Tiedosto index.html on headerin määrittelyjä lukuun ottamatta oleellisesti ottaen tyhjä:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + React</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </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 tämä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.jsx 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 tämä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 tehtä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ä.

Mahdollinen virheilmoitus

Käyttämästäsi editorista riippuen saatat saada tässä vaiheessa seuraavan virheilmoituksen:

fullstack content

Kyse ei ole varsinaisesta virheestä vaan ESLint-työkalun aiheuttamasta varoituksesta. Saat hiljennettyä varoituksen react/prop-types lisäämällä tiedostoon .eslintrc.cjs seuraavan rivin

module.exports = {
  root: true,
  env: { browser: true, es2020: true },
  extends: [
    'eslint:recommended',
    'plugin:react/recommended',
    'plugin:react/jsx-runtime',
    'plugin:react-hooks/recommended',
  ],
  ignorePatterns: ['dist', '.eslintrc.cjs'],
  parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
  settings: { react: { version: '18.2' } },
  plugins: ['react-refresh'],
  rules: {
    'react-refresh/only-export-components': [
      'warn',
      { allowConstantExport: true },
    ],
    'react/prop-types': 0  },
}

Tutustumme ESLintiin tarkemmin osassa 3.

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-koodissa 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 ilmestykään 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ääriteltäessä sovelluksen juurikomponenttia tämä ei kuitenkaan ole järkevää, ja taulukko 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 tämä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 Ø, uudelleenladata tämän jälkeen 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 numeroita 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ää taulukoiden suoraa renderöintiä. Palaamme siihen seuraavassa osassa.