d

Kirjautuminen ja välimuistin päivitys

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 virheenkäsittelijän sekä funktion setToken:

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

  if (!token) {
    return (
      <div>
        <Notify errorMessage={errorMessage} />
        <h2>Login</h2>
        <LoginForm
          setToken={setToken}
          setError={notify}
        />
      </div>
    )
  }

  return (
    // ...
  )
}

Määritellään kirjautumisen suorittava mutaatio

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

Kirjautumisesta huolehtiva komponentti LoginForm toimii melko samalla tavalla kuin aiemmat mutaatioista huolehtivat komponentit. Mielenkiintoiset rivit on korostettu koodissa:

import React, { useState, useEffect } from 'react'
import { useMutation } from '@apollo/client'
import { LOGIN } from '../queries'

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

  const [ login, result ] = useMutation(LOGIN, {    onError: (error) => {
      setError(error.graphQLErrors[0].message)
    }
  })

  useEffect(() => {    if ( result.data ) {      const token = result.data.login.value      setToken(token)      localStorage.setItem('phonenumbers-user-token', token)    }  }, [result.data]) // eslint-disable-line
  const submit = async (event) => {
    event.preventDefault()

    login({ variables: { username, password } })
  }

  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

Käytössä on jälleen efektihookki, jonka avulla asetetaan tokenin arvo komponentin App tilaan sekä local storageen siinä vaiheessa kun palvelin on vastannut mutaatioon. Efektihookki on tarpeen, jotta sovellus ei joutuisi ikuiseen renderöintilooppiin.

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.

Välimuistin nollaaminen tapahtuu Apollon client-objektin metodilla resetStore, clientiin taas päästään käsiksi hookilla useApolloClient:

const App = () => {
  const [token, setToken] = useState(null)
  const [errorMessage, setErrorMessage] = useState(null)
  const result = useQuery(ALL_PERSONS)
  const client = useApolloClient()  
  if (result.loading)  {
    return <div>loading...</div>
  }

  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.

Tämä edellyttää pientä muutosta tiedostossa index.js olevaan ApolloClient-olion konfiguraatioon

import { setContext } from 'apollo-link-context'
const authLink = setContext((_, { headers }) => {  const token = localStorage.getItem('phonenumbers-user-token')  return {    headers: {      ...headers,      authorization: token ? `bearer ${token}` : null,    }  }})
const httpLink = new HttpLink({ uri: 'http://localhost:4000' })
const client = new ApolloClient({
  cache: new InMemoryCache(),
  link: authLink.concat(httpLink)})

client-olion muodostamisen yhteydessä oleva toinen parametri link määrittelee, miten apollo on yhteydessä palvelimeen. Nyt normaalia httpLink-yhteyttä muokataan siten, että pyyntöjen mukaan asetetaan headerille authorization arvoksi localStoragessa mahdollisesti oleva token.

Asennetaan vielä muutoksen tarvitsema kirjasto

npm install apollo-link-context

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 = ({ setError }) => {
  // ...
  const submit = async (event) => {
    event.preventDefault()
    createPerson({
      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 PersonForm = ({ setError }) => {
  // ...

  const [ createPerson ] = useMutation(CREATE_PERSON, {
    refetchQueries: [  {query: ALL_PERSONS} ],    onError: (error) => {
      setError(error.graphQLErrors[0].message)
    }
  })

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 PersonForm = ({ setError }) => {
  // ...

  const [ createPerson ] = useMutation(CREATE_PERSON, {
    onError: (error) => {
      setError(error.graphQLErrors[0].message)
    },
    update: (store, response) => {      const dataInStore = store.readQuery({ query: ALL_PERSONS })      store.writeQuery({        query: ALL_PERSONS,        data: {          ...dataInStore,          allPersons: [ ...dataInStore.allPersons, response.data.addPerson ]        }      })    }  })
 
  // ..
}  

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.

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.