b
props.children ja proptypet
Kirjautumislomakkeen näyttäminen vain tarvittaessa
Muutetaan sovellusta siten, että kirjautumislomaketta ei oletusarvoisesti näytetä:
Lomake aukeaa, jos käyttäjä painaa nappia login:
Napilla cancel käyttäjä saa tarvittaessa suljettua lomakkeen.
Aloitetaan eristämällä kirjautumislomake omaksi komponentikseen:
const LoginForm = ({
handleSubmit,
handleUsernameChange,
handlePasswordChange,
username,
password
}) => {
return (
<div>
<h2>Login</h2>
<form onSubmit={handleSubmit}>
<div>
username
<input
value={username}
onChange={handleUsernameChange}
/>
</div>
<div>
password
<input
type="password"
value={password}
onChange={handlePasswordChange}
/>
</div>
<button type="submit">login</button>
</form>
</div>
)
}
export default LoginForm
Tila ja tilaa käsittelevät funktiot on kaikki määritelty komponentin ulkopuolella ja ne välitetään komponentille propseina.
Huomaa, että propsit otetaan vastaan destrukturoimalla, eli sen sijaan että määriteltäisiin
const LoginForm = (props) => {
return (
<div>
<h2>Login</h2>
<form onSubmit={props.handleSubmit}>
<div>
username
<input
value={props.username}
onChange={props.handleChange}
name="username"
/>
</div>
// ...
<button type="submit">login</button>
</form>
</div>
)
}
jolloin muuttujan props kenttiin on viitattava muuttujan kautta esim. props.handleSubmit, otetaan kentät suoraan vastaan omiin muuttujiinsa.
Nopea tapa toiminnallisuuden toteuttamiseen on muuttaa komponentin App käyttämä funktio loginForm seuraavaan muotoon:
const App = () => {
const [loginVisible, setLoginVisible] = useState(false)
// ...
const loginForm = () => {
const hideWhenVisible = { display: loginVisible ? 'none' : '' }
const showWhenVisible = { display: loginVisible ? '' : 'none' }
return (
<div>
<div style={hideWhenVisible}>
<button onClick={() => setLoginVisible(true)}>log in</button>
</div>
<div style={showWhenVisible}>
<LoginForm
username={username}
password={password}
handleUsernameChange={({ target }) => setUsername(target.value)}
handlePasswordChange={({ target }) => setPassword(target.value)}
handleSubmit={handleLogin}
/>
<button onClick={() => setLoginVisible(false)}>cancel</button>
</div>
</div>
)
}
// ...
}
Komponentin App tilaan on nyt lisätty totuusarvo loginVisible, joka määrittelee sen, näytetäänkö kirjautumislomake.
Näkyvyyttä säätelevää tilaa vaihdellaan kahden napin avulla, joihin molempiin on kirjoitettu tapahtumankäsittelijän koodi suoraan:
<button onClick={() => setLoginVisible(true)}>log in</button>
<button onClick={() => setLoginVisible(false)}>cancel</button>
Komponenttien näkyvyys on määritelty asettamalla komponentille inline-tyyleinä CSS-määrittely, jossa display-propertyn arvoksi asetetaan none jos komponentin ei haluta näkyvän:
const hideWhenVisible = { display: loginVisible ? 'none' : '' }
const showWhenVisible = { display: loginVisible ? '' : 'none' }
<div style={hideWhenVisible}>
// nappi
</div>
<div style={showWhenVisible}>
// lomake
</div>
Käytössä on kysymysmerkkioperaattori eli jos loginVisible on true, tulee napin CSS-määrittelyksi
display: 'none';
Jos loginVisible on false, ei display saa mitään napin näkyvyyteen liittyvää arvoa.
Komponentin lapset eli props.children
Kirjautumislomakkeen näkyvyyttä ympäröivän koodin voi ajatella olevan oma looginen kokonaisuutensa, ja se onkin hyvä eristää pois komponentista App omaksi komponentikseen.
Tavoitteena on luoda komponentti Togglable, jota käytetään seuraavalla tavalla:
<Togglable buttonLabel='login'>
<LoginForm
username={username}
password={password}
handleUsernameChange={({ target }) => setUsername(target.value)}
handlePasswordChange={({ target }) => setPassword(target.value)}
handleSubmit={handleLogin}
/>
</Togglable>
Komponentin käyttö poikkeaa aiemmin näkemistämme siinä, että käytössä on nyt avaava ja sulkeva tagi, joiden sisällä määritellään toinen komponentti eli LoginForm. Reactin terminologiassa LoginForm on nyt komponentin Togglable lapsi.
Togglablen avaavan ja sulkevan tagin sisälle voi sijoittaa lapsiksi mitä tahansa React-elementtejä, esim.:
<Togglable buttonLabel="paljasta">
<p>tämä on aluksi piilossa</p>
<p>toinen salainen rivi</p>
</Togglable>
Komponentin koodi on tällainen:
import { useState } from 'react'
const Togglable = (props) => {
const [visible, setVisible] = useState(false)
const hideWhenVisible = { display: visible ? 'none' : '' }
const showWhenVisible = { display: visible ? '' : 'none' }
const toggleVisibility = () => {
setVisible(!visible)
}
return (
<div>
<div style={hideWhenVisible}>
<button onClick={toggleVisibility}>{props.buttonLabel}</button>
</div>
<div style={showWhenVisible}>
{props.children} <button onClick={toggleVisibility}>cancel</button>
</div>
</div>
)
}
export default Togglable
Mielenkiintoista ja meille uutta on props.children, jonka avulla koodi viittaa komponentin lapsiin eli avaavan ja sulkevan tagin sisällä määriteltyihin React-elementteihin.
Tällä kertaa lapset ainoastaan renderöidään komponentin oman renderöivän koodin seassa:
<div style={showWhenVisible}>
{props.children}
<button onClick={toggleVisibility}>cancel</button>
</div>
Toisin kuin "normaalit" propsit, children on Reactin automaattisesti määrittelemä, aina olemassa oleva propsi. Jos komponentti määritellään automaattisesti suljettavalla eli /> loppuvalla tagilla, esim.
<Note
key={note.id}
note={note}
toggleImportance={() => toggleImportanceOf(note.id)}
/>
on props.children tyhjä taulukko.
Komponentti Togglable on uusiokäytettävä, ja voimme käyttää sitä tekemään myös uuden muistiinpanon luomisesta huolehtivan formin vastaavalla tavalla tarpeen mukaan näytettäväksi.
Eristetään ensin muistiinpanojen luominen omaksi komponentiksi
const NoteForm = ({ onSubmit, handleChange, value }) => {
return (
<div>
<h2>Create a new note</h2>
<form onSubmit={onSubmit}>
<input
value={value}
onChange={handleChange}
/>
<button type="submit">save</button>
</form>
</div>
)
}
export default NoteForm
ja määritellään lomakkeen näyttävä koodi komponentin Togglable sisällä
<Togglable buttonLabel="new note">
<NoteForm
onSubmit={addNote}
value={newNote}
handleChange={handleNoteChange}
/>
</Togglable>
Sovelluksen tämänhetkinen koodi on kokonaisuudessaan GitHubissa, branchissa part5-4.
Lomakkeiden tila
Koko sovelluksen tila on nyt sijoitettu komponenttiin App.
Reactin dokumentaatio antaa seuraavan ohjeen tilan sijoittamisesta:
Often, several components need to reflect the same changing data. We recommend lifting the shared state up to their closest common ancestor.
Jos mietitään lomakkeiden tilaa eli esimerkiksi uuden muistiinpanon sisältöä sillä hetkellä kun muistiinpanoa ei vielä ole luotu, ei komponentti App oikeastaan tarvitse niitä mihinkään, ja voisimme aivan hyvin siirtää lomakkeisiin liittyvän tilan niitä vastaaviin komponentteihin.
Muistiinpanon luomisesta huolehtiva komponentti muuttuu seuraavasti:
import { useState } from 'react'
const NoteForm = ({ createNote }) => {
const [newNote, setNewNote] = useState('')
const addNote = (event) => {
event.preventDefault()
createNote({
content: newNote,
important: true
})
setNewNote('')
}
return (
<div>
<h2>Create a new note</h2>
<form onSubmit={addNote}>
<input
value={newNote}
onChange={event => setNewNote(event.target.value)}
/>
<button type="submit">save</button>
</form>
</div>
)
}
export default NoteForm
HUOM muutimme samalla sovelluksen toimintaa siten, että uudet muistiinpanot ovat oletusarvoisesti tärkeitä, eli important saa arvon true.
Tilan muuttuja newNote ja sen muutoksesta huolehtiva tapahtumankäsittelijä on siirretty komponentista App lomakkeesta huolehtivaan komponenttiin.
Propseja on enää yksi eli funktio createNote, jota lomake kutsuu kun uusi muistiinpano luodaan.
Komponentti App yksinkertaistuu, koska tilasta newNote ja sen käsittelijäfunktiosta on päästy eroon. Uuden muistiinpanon luomisesta huolehtiva funktio addNote saa suoraan parametriksi uuden muistiinpanon ja funktio on ainoa props, joka välitetään lomakkeelle:
const App = () => {
// ...
const addNote = (noteObject) => { noteService
.create(noteObject)
.then(returnedNote => {
setNotes(notes.concat(returnedNote))
})
}
// ...
return (
<div>
<h1>Notes</h1>
// ...
<Togglable buttonLabel="new note">
<NoteForm createNote={addNote} /> </Togglable>
// ...
<Footer />
</div>
)
}
Vastaava muutos voitaisiin tehdä myös kirjautumislomakkeelle, mutta jätämme sen vapaaehtoiseksi harjoitustehtäväksi.
Sovelluksen tämänhetkinen koodi on kokonaisuudessaan GitHubissa, branchissa part5-5.
ref eli viite komponenttiin
Ratkaisu on melko hyvä, mutta haluamme kuitenkin parantaa sitä. Kun uusi muistiinpano luodaan, olisi loogista jos luomislomake menisi piiloon. Nyt lomake pysyy näkyvillä. Lomakkeen piilottamiseen sisältyy kuitenkin pieni ongelma, sillä näkyvyyttä kontrolloidaan Togglable-komponentin tilassa olevalla muuttujalla visible. Miten pääsemme tilaan käsiksi komponentin ulkopuolelta?
On useita erilaisia tapoja toteuttaa pääsy komponentin funktioihin sen ulkopuolelta. Käytetään nyt Reactin ref-mekanismia, joka tarjoaa eräänlaisen viitteen komponenttiin.
Tehdään komponenttiin App seuraavat muutokset:
import { useState, useEffect, useRef } from 'react'
const App = () => {
// ...
const noteFormRef = useRef()
return (
// ...
<Togglable buttonLabel='new note' ref={noteFormRef}> <NoteForm createNote={addNote} />
</Togglable>
// ...
)
}
useRef hookilla luodaan ref noteFormRef, joka kiinnitetään muistiinpanojen luomislomakkeen sisältävälle Togglable-komponentille. Nyt siis muuttuja noteFormRef toimii viitteenä komponenttiin.
Komponenttia Togglable laajennetaan seuraavasti
import { useState, useImperativeHandle, forwardRef } from 'react'
const Togglable = forwardRef((props, ref) => { const [visible, setVisible] = useState(false)
const hideWhenVisible = { display: visible ? 'none' : '' }
const showWhenVisible = { display: visible ? '' : 'none' }
const toggleVisibility = () => {
setVisible(!visible)
}
useImperativeHandle(ref, () => { return { toggleVisibility } })
return (
<div>
<div style={hideWhenVisible}>
<button onClick={toggleVisibility}>{props.buttonLabel}</button>
</div>
<div style={showWhenVisible}>
{props.children}
<button onClick={toggleVisibility}>cancel</button>
</div>
</div>
)
})
export default Togglable
Komponentin luova funktio on kääritty funktiokutsun forwardRef sisälle, jolloin komponentti pääsee käsiksi sille määriteltyyn refiin.
Komponentti tarjoaa useImperativeHandle -hookin avulla sisäisesti määritellyn funktionsa toggleVisibility ulkopuolelta kutsuttavaksi.
Voimme nyt piilottaa lomakkeen kutsumalla noteFormRef.current.toggleVisibility() samalla kun uuden muistiinpanon luominen tapahtuu:
const App = () => {
// ...
const addNote = (noteObject) => {
noteFormRef.current.toggleVisibility() noteService
.create(noteObject)
.then(returnedNote => {
setNotes(notes.concat(returnedNote))
})
}
// ...
}
Käyttämämme useImperativeHandle on siis React hook, jonka avulla komponentille voidaan määrittää funktioita, joita on mahdollista kutsua sen ulkopuolelta.
Käyttämämme kikka komponentin tilan muuttamiseksi toimii, mutta se vaikuttaa hieman ikävältä. Saman olisi saanut aavistuksen siistimmin toteutettua "vanhan Reactin" class-komponenteilla, joihin tutustumme osassa 7. Tämä on toistaiseksi ainoa tapaus, jossa Reactin hook-syntaksiin nojaava ratkaisu on aavistuksen likaisemman oloinen kuin class-komponenttien tarjoama ratkaisu.
Refeille on myös muita käyttötarkoituksia kuin React-komponentteihin käsiksi pääseminen.
Sovelluksen tämänhetkinen koodi on kokonaisuudessaan GitHubissa, branchissa part5-6.
Huomio komponenteista
Kun Reactissa määritellään komponentti
const Togglable = () => {
// ...
}
ja otetaan se käyttöön
<div>
<Togglable buttonLabel="1" ref={togglable1}>
ensimmäinen
</Togglable>
<Togglable buttonLabel="2" ref={togglable2}>
toinen
</Togglable>
<Togglable buttonLabel="3" ref={togglable3}>
kolmas
</Togglable>
</div>
syntyy kolme erillistä komponenttiolioa, joilla on kaikilla oma tilansa:
ref-attribuutin avulla on talletettu viite jokaiseen komponentin muuttujaan togglable1, togglable2 ja togglable3.
Full stack -sovelluskehittäjän päivitetty vala
Liikkuvien osien määrä nousee. Samalla kasvaa myös todennäköisyys sille, että päädymme tilanteeseen, missä etsimme vikaa täysin väärästä paikasta. Systemaattisuutta on siis lisättävä vielä pykälän verran.
Full stack -ohjelmointi on todella hankalaa, ja sen takia lupaan hyödyntää kaikkia ohjelmointia helpottavia keinoja:
- pidän selaimen konsolin koko ajan auki
- tarkkailen säännöllisesti selaimen network-välilehdeltä, että frontendin ja backendin välinen kommunikaatio tapahtuu oletusteni mukaan
- tarkkailen säännöllisesti palvelimella olevan datan tilaa, ja varmistan että frontendin lähettämä data siirtyy sinne kuten oletin
- pidän silmällä tietokannan tilaa: varmistan että backend tallentaa datan sinne oikeaan muotoon
- etenen pienin askelin
- kun epäinen että bugi on frontendissa, varmistan että backend toimii varmasti
- kun epäinen että bugi on backendissa, varmistan että frontend toimii varmasti
- käytän koodissa ja testeissä runsaasti console.log-komentoja varmistamaan sen, että varmasti ymmärrän jokaisen kirjoittamani rivin, sekä etsiessäni koodista tai testeistä mahdollisia ongelman aiheuttajia
- jos koodini ei toimi, en kirjoita enää yhtään lisää koodia, vaan alan poistamaan toiminnan rikkoneita rivejä tai palaan suosiolla tilanteeseen, missä koodi vielä toimi
- jos testit eivät mene läpi, varmistan että testien testaama toiminnallisuus varmasti toimii sovelluksessa
- kun kysyn apua kurssin Discord- tai Telegram-kanavalla, tai muualla internetissä, muotoilen kysymyksen järkevästi, esim. täällä esiteltyyn tapaan
PropTypes
Komponentti Togglable olettaa, että sille määritellään propsina buttonLabel napin teksti. Jos määrittely unohtuu,
<Togglable> buttonLabel unohtui... </Togglable>
sovellus kyllä toimii, mutta selaimeen renderöityy hämäävästi nappi, jolla ei ole mitään tekstiä.
Haluaisimmekin varmistaa, että jos Togglable-komponenttia käytetään, on propsille "pakko" antaa arvo.
Komponentin olettamat ja edellyttämät propsit ja niiden tyypit voidaan määritellä kirjaston prop-types avulla. Asennetaan kirjasto:
npm install prop-types
buttonLabel voidaan määritellä pakolliseksi string-tyyppiseksi propsiksi seuraavasti:
import PropTypes from 'prop-types'
const Togglable = React.forwardRef((props, ref) => {
// ..
}
Togglable.propTypes = {
buttonLabel: PropTypes.string.isRequired
}
Jos propsia ei määritellä, seurauksena on konsoliin tulostuva virheilmoitus:
Koodi kuitenkin toimii edelleen, eli mikään ei pakota määrittelemään propseja PropTypes-määrittelyistä huolimatta. On kuitenkin erittäin epäammattimaista jättää konsoliin mitään punaisia tulosteita.
Määritellään Proptypet myös LoginForm-komponentille:
import PropTypes from 'prop-types'
const LoginForm = ({
handleSubmit,
handleUsernameChange,
handlePasswordChange,
username,
password
}) => {
// ...
}
LoginForm.propTypes = {
handleSubmit: PropTypes.func.isRequired,
handleUsernameChange: PropTypes.func.isRequired,
handlePasswordChange: PropTypes.func.isRequired,
username: PropTypes.string.isRequired,
password: PropTypes.string.isRequired
}
Jos propsin tyyppi on väärä, eli jos esimerkiksi yritetään määritellä propsiksi handleSubmit merkkijono, seurauksena on varoitus:
ESLint
Konfiguroimme osassa 3 koodin tyylistä huolehtivan ESLintin backendiin. Otetaan nyt ESLint käyttöön myös frontendissa.
Create-react-app on asentanut projektille ESLintin valmiiksi, joten ei tarvita muuta kuin sopiva konfiguraatio tiedostoon .eslintrc.js.
HUOM: Älä suorita komentoa eslint --init. Se asentaa ESLintistä uuden version, joka on epäsopiva create-react-app:in konfiguraatioiden kanssa!
Aloitamme seuraavaksi testaamisen, ja jotta pääsemme eroon testeissä olevista turhista huomautuksista asennetaan plugin eslint-jest-plugin:
npm install --save-dev eslint-plugin-jest
Luodaan tiedosto .eslintrc.js ja kopioidaan sinne seuraava sisältö:
module.exports = {
"env": {
"browser": true,
"es6": true,
"jest/globals": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended"
],
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 2018,
"sourceType": "module"
},
"plugins": [
"react", "jest"
],
"rules": {
"indent": [
"error",
2
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"never"
],
"eqeqeq": "error",
"no-trailing-spaces": "error",
"object-curly-spacing": [
"error", "always"
],
"arrow-spacing": [
"error", { "before": true, "after": true }
],
"no-console": 0,
"react/prop-types": 0,
"react/react-in-jsx-scope": "off"
},
"settings": {
"react": {
"version": "detect"
}
}
}
Tehdään projektin juureen tiedosto .eslintignore ja sille seuraava sisältö:
node_modules
build
.eslintrc.js
Näin ainoastaan sovelluksessa oleva itse kirjoitettu koodi huomioidaan linttauksessa.
Tehdään lintausta varten npm-skripti:
{
// ...
{
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"eslint": "eslint ." },
// ...
}
Komponentti Togglable aiheuttaa ikävän näköisen varoituksen Component definition is missing display name:
Komponentin "nimettömyys" käy ilmi myös React Development Toolsilla:
Korjaus on onneksi hyvin helppo tehdä:
import { useState, useImperativeHandle } from 'react'
import PropTypes from 'prop-types'
const Togglable = React.forwardRef((props, ref) => {
// ...
})
Togglable.displayName = 'Togglable'
export default Togglable
Sovelluksen tämänhetkinen koodi on kokonaisuudessaan GitHubissa, branchissa part5-7.
Kannattaa huomata, että create-react-appilla on myös oletusarvoinen ESLint-konfiguraatio, jonka korvasimme nyt kokonaan omalla konfiguraatiolla. Dokumentaatio toteaa, että oletusarvoisen konfiguraation korvaaminen on ok, mutta suosittelee mielummin laajentamaan oletusarvoista konfiguraatiota: We highly recommend extending the base config, as removing it could introduce hard-to-find issues.