b

props.children ja proptypet

Kirjautumislomakkeen näyttäminen vain tarvittaessa

Muutetaan sovellusta siten, että kirjautumislomaketta ei oletusarvoisesti näytetä:

fullstack content

Lomake aukeaa, jos käyttäjä painaa nappia login:

fullstack content

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 taas 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

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

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.

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 funktoihin sen ulkopuolelta, käytetään nyt Reactin ref-mekanismia, joka tarjoaa eräänlaisen viitteen komponenttiin.

Tehdään komponenttiin App seuraavat muutokset

const App = () => {
  // ...
  const noteFormRef = React.createRef()
  const noteForm = () => (
    <Togglable buttonLabel="new note" ref={noteFormRef}>      <NoteForm
        onSubmit={addNote}
        value={newNote}
        handleChange={handleNoteChange}
      />
    </Togglable>
  )

  // ...
}

Metodilla createRef 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 = (event) => {
    event.preventDefault()
    noteFormRef.current.toggleVisibility()    const noteObject = {
      content: newNote,
      date: new Date().toISOString(),
      important: Math.random() > 0.5
    }

    noteService
      .create(noteObject).then(returnedNote => {
        setNotes(notes.concat(returnedNote))
        setNewNote('')
      })
  }
  // ...
}

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 muuttamikseksi toimii, mutta se vaikuttaa hieman ikävältä. Saman olisi saanut aavistuksen siistimmin toteutettua "vanhan Reactin" class-perustaisilla komponenteilla, joihin tutustumme tämän osan lopussa. 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-5.

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:

fullstack content

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

fullstack content

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
}

Funktionaalisen komponentin proptypejen määrittely tapahtuu samalla tavalla kuin luokkaperustaisten.

Jos propsin tyyppi on väärä, esim. yritetään määritellä propsiksi handleSubmit merkkijono, seurauksena on varoitus:

fullstack content

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 npm 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 ."  },
  // ...
}

Sovelluksen tämänhetkinen koodi on kokonaisuudessaan [githubissa](https://github.com/fullstackopen-2019/part2-notes/tree/part5-6, branchissa part5-6.