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:
import React from 'react'
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 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, 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 seuraavassa:
import React, { 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
import React from 'react'
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.
Muistiinpanosta huolehtiva komponentti muuttuu seuraavasti:
import React, {useState} from 'react'
const NoteForm = ({ createNote }) => {
const [newNote, setNewNote] = useState('')
const handleChange = (event) => {
setNewNote(event.target.value)
}
const addNote = (event) => {
event.preventDefault()
createNote({
content: newNote,
important: Math.random() > 0.5,
})
setNewNote('')
}
return (
<div>
<h2>Create a new note</h2>
<form onSubmit={addNote}>
<input
value={newNote}
onChange={handleChange}
/>
<button type="submit">save</button>
</form>
</div>
)
}
export default NoteForm
Tilan muuttuja newNote ja sen muutoksesta huolehtiva tapahtumankäsittelijä on siirretty komponentista App lomakkeesta huolehtivaan komponenttiin.
Propseja on enää yksi, funktio createNote, jota lomake kutsuu kun uusi muistiinpano luodaan.
Komponentti App yksintertaistuu, 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))
})
}
// ...
const noteForm = () => (
<Togglable buttonLabel='new note'>
<NoteForm createNote={addNote} />
</Togglable>
)
// ...
}
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ä, haluaisimme kuitenkin parantaa sitä erään seikan osalta.
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 React, { useState, useRef } from 'react'
const App = () => {
// ...
const noteFormRef = useRef()
const noteForm = () => (
<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 React, { useState, useImperativeHandle } from 'react'
const Togglable = React.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, näin 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 funktiona määritellylle 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-perustaisilla 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 seuraavasti,
<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.
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 --save 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ä, esim. 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 uuden version eslintistä 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
}
}
Tehdään projektin juureen tiedosto .eslintignore ja sille seuraava sisältö
node_modules
build
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",
"server": "json-server -p3001 db.json",
"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-devtoolsilla:
Korjaus on onneksi hyvin helppo tehdä
import React, { 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.