This course is now available in English.Click here to change the language.

d

Kirjautuminen ja välimuistin päivitys

HUOM osan 8 sisältöä muutettiin Apollo hookien osalta 28.6. noin klo 21. Jos olet aloittanut osan 8 tekemisen tätä aiemmin tarkista täältä että konfiguraatiosi ovat oikeat!

Sovelluksen frontend toimii puhelinluettelon näyttämisen osalta päivitetyn palvelimen kanssa. Jotta luetteloon voitaisiin lisätä henkilöitä, tulee backendiin toteuttaa kirjautuminen.

Käyttäjän kirjautuminen

Lisätään sovelluksen tilaan muuttuja token, joka tallettaa tokenin siinä vaiheessa kun käyttäjä on kirjautunut. Jos token ei ole määritelty, näytetään kirjautumisesta huolehtiva komponentti LoginForm, joka saa parametriksi mutaation tekevän funktion login:

const LOGIN = gql`
  mutation login($username: String!, $password: String!) {
    login(username: $username, password: $password)  {
      value
    }
  }
`

const App = () => {
  const [token, setToken] = useState(null)

  // ...

  const [login] = useMutation(LOGIN, {
    onError: handleError
  })

  const errorNotification = () => errorMessage &&
    <div style={{ color: 'red' }}>
      {errorMessage}
    </div>

  if (!token) {
    return (
      <div>
        {errorNotification()}
        <h2>Login</h2>
        <LoginForm
          login={login}
          setToken={(token) => setToken(token)}
        />
      </div>
    )
  }

  return (
    // ...
  )
}

Jos kirjautuminen onnistuu, eli funktio login ei heitä poikkeusta, talletetaan funktion palauttama token komponentin App tilaan. Token talletetaan myös local storageen, näin siihen on helpompi päästä käsiksi siinä vaiheessa kun haluamme asettaa tokenin Authorization-headeriin.

Jos operaatio epäonnistuu, kutsutaan propsina saatua funktiota, joka asettaa komponentin App tilaan käyttäjälle näytettävän virheilmoituksen:

import React, { useState } from 'react'

const LoginForm = (props) => {
  const [username, setUsername] = useState('')
  const [password, setPassword] = useState('')

  const submit = async (event) => {
    event.preventDefault()

    const result = await props.login({
      variables: { username, password }
    })

    if (result) {
      const token = result.data.login.value
      props.setToken(token)
      localStorage.setItem('phonenumbers-user-token', token)
    }
  }

  return (
    <div>
      <form onSubmit={submit}>
        <div>
          username <input
            value={username}
            onChange={({ target }) => setUsername(target.value)}
          />
        </div>
        <div>
          password <input
            type='password'
            value={password}
            onChange={({ target }) => setPassword(target.value)}
          />
        </div>
        <button type='submit'>login</button>
      </form>
    </div>
  )
}

export default LoginForm

Lisätään sovellukselle myös nappi, jonka avulla kirjautunut käyttäjä voi kirjautua ulos. Napin klikkauskäsittelijässä asetetaan token tilaan null, poistetaan token local storagesta ja resetoidaan Apollo clientin välimuisti. Tämä on tärkeää, sillä joissain kyselyissä välimuistiin on saatettu hakea dataa, johon vain kirjaantuneella käyttäjällä on oikeus päästä käsiksi.

const App = () => {
  const client = useApolloClient()

  // ...

  const logout = () => {
    setToken(null)
    localStorage.clear()
    client.resetStore()
  }

  // ...
}

Sovelluksen tämän vaiheen koodi githubissa, branchissa part8-6.

Tokenin lisääminen headeriin

Backendin muutosten jälkeen uusien henkilöiden lisäys puhelinluetteloon vaatii sen, että käyttäjän token lähetetään pyynnön mukana. Jotta saamme tokenin lähetettyä pyyntöjen mukana, joudumme hieman muuttamaan tapaa, jonka avulla määrittelemme ApolloClient-olion tiedostossa index.js

import React from 'react'
import ReactDOM from 'react-dom'
import ApolloClient from 'apollo-boost'import { ApolloProvider } from "@apollo/react-hooks"
import App from './App'

const client = new ApolloClient({  uri: "http://localhost:4000/graphql"})
ReactDOM.render(
  <ApolloProvider client={client}>
    <App />
  </ApolloProvider>,
  document.getElementById('root')
)

Määrittely käyttää apunaan apollo-boost-kirjastoa, joka dokumentaationsa mukaan

Apollo Boost is a zero-config way to start using Apollo Client. It includes some sensible defaults, such as our recommended InMemoryCache and HttpLink, which come configured for you with our recommended settings.

Eli apollo-boost tarjoaa helpon tavan konfiguroida ApolloClient useisiin tilanteisiin riittävillä oletusasetuksilla.

Vaikka apollo-boostilla olisi myös mahdollista konfiguroida pyyntöihin asetettavat headerit, luovutaan nyt apollo-boostin käytöstä ja tehdään konfiguraatio kokonaan itse.

Konfiguraatio on seuraavassa:

import { ApolloClient } from 'apollo-client'
import { createHttpLink } from 'apollo-link-http'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { setContext } from 'apollo-link-context'

const httpLink = createHttpLink({
  uri: 'http://localhost:4000/graphql',
})

const authLink = setContext((_, { headers }) => {
  const token = localStorage.getItem('phonenumbers-user-token')
  return {
    headers: {
      ...headers,
      authorization: token ? `bearer ${token}` : null,
    }
  }
})

const client = new ApolloClient({
  link: authLink.concat(httpLink),
  cache: new InMemoryCache()
})

Konfiguraatio edellyttää kahden kirjaston asentamista:

npm install --save apollo-link apollo-link-context

client muodostetaan nyt kirjaston apollo-link tarjoaman konstruktorifunktion ApolloClient. Parametreja on kaksi, link ja cache. Näistä jälkimmäinen määrittelee, että sovelluksen käyttöön tulee keskusmuistissa toimiva välimuisti InMemoryCache.

Ensimmäinen parametri link määrittelee sen, miten client ottaa yhteyttä palvelimeen, jonka pohjalla on httpLink, eli normaali HTTP:n yli tapahtuva yhteys, jota on höystetty siten, että pyyntöjen mukaan asetetaan headerille authorization arvoksi localStoragessa mahdollisesti oleva token.

Uusien henkilöiden lisäys ja numeroiden muuttaminen toimii taas. Sovellukseen jää kuitenkin yksi ongelma. Jos yritämme lisätä puhelinnumerotonta henkilöä, se ei onnistu.

fullstack content

Validointi epäonnistuu, sillä frontend lähettää kentän phone arvona tyhjän merkkijonon.

Muutetaan uuden henkilön luovaa funktiota siten, että se asettaa kentälle phone arvon null, jos käyttäjä ei ole syöttänyt kenttään mitään:

const PersonForm = (props) => {
  // ...
  const submit = async (e) => {
    e.preventDefault()

    await props.addPerson({ 
      variables: { 
        name, street, city,        phone: phone.length>0 ? phone : null      } 
    })

  // ...
  }

  // ...
}

Sovelluksen tämän vaiheen koodi githubissa, branchissa part8-7.

Välimuistin päivitys revisited

Uusien henkilöiden lisäyksen yhteydessä on siis päivitettävä Apollo clientin välimuisti. Päivitys tapahtuu määrittelemällä mutaation yhteydessä option refetchQueries avulla, että kysely ALL_PERSONS on suoritettava uudelleen:

const App = () => {
  // ...

  const addPerson = useMutation(CREATE_PERSON, {
    onError: handleError,
    refetchQueries: [{ query: ALL_PERSONS }]
  })

  // ..
}

Lähestymistapa on kohtuullisen toimiva, ikävänä puolena on toki se, että päivityksen yhteydessä suoritetaan aina myös kysely.

Ratkaisua on mahdollista optimoida hoitamalla välimuistin päivitys itse. Tämä tapahtuu määrittelemällä mutaatiolle sopiva update-callback, jonka Apollo suorittaa mutaation päätteeksi:

const App = () => {
  // ...

  const [addPerson] = useMutation(CREATE_PERSON, {
    onError: handleError,
    update: (store, response) => {      const dataInStore = store.readQuery({ query: ALL_PERSONS })      dataInStore.allPersons.push(response.data.addPerson)      store.writeQuery({        query: ALL_PERSONS,        data: dataInStore      })    }  })
 
  // ..
}  

Callback-funktio saa parametriksi viitteen välimuistiin sekä mutaation mukana palautetun datan, eli esimerkkimme tapauksessa lisätyn käyttäjän.

Koodi lukee funktion readQuery avulla kyselyn ALL_PERSONS välimuistiin talletetun tilan ja päivittää välimuistin funktion writeQuery avulla lisäten henkilöiden joukkoon mutaation lisäämän henkilön.

On myös olemassa tilanteita, joissa ainoa järkevä tapa saada välimuisti pidettyä ajantasaisena on update-callbackillä tehtävä päivitys.

Tarvittaessa välimuisti on mahdollista kytkeä pois päältä joko koko sovelluksesta tai yksittäisiltä kyselyiltä määrittelemällä välimuistin käyttöä kontrolloivalle fetchPolicy:lle arvo no-cache.

Voisimme määritellä, että yksittäisen henkilön osoitetietoja ei tallenneta välimuistiin:

const Persons = ({ result }) => {
  // ...
  const show = async (name) => {
    const { data } = await client.query({
      query: FIND_PERSON,
      variables: { nameToSearch: name },
      fetchPolicy: 'no-cache'    })
    setPerson(data.findPerson)
  }

  // ...
}

Jätämme kuitenkin koodin ennalleen.

Välimuistin kanssa kannattaa olla tarkkana. Välimuistissa oleva epäajantasainen data voi aiheuttaa vaikeasti havaittavia bugeja. Kuten tunnettua, välimuistin ajantasalla pitäminen on erittäin haastavaa. Koodareiden joukossa kulkevan kansanviisauden mukaan

There are only two hard things in Computer Science: cache invalidation and naming things. Katso lisää täältä.

Sovelluksen tämän vaiheen koodi githubissa, branchissa part8-8.