Siirry sisältöön

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 frontendiin 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 { 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])
  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 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()  }
  if (!token) {    return (      <>        <Notify errorMessage={errorMessage} />        <LoginForm setToken={setToken} setError={notify} />      </>    )  }
  return (
    <>
      <Notify errorMessage={errorMessage} />
      <button onClick={logout}>logout</button>      <Persons persons={result.data.allPersons} />
      <PersonForm setError={notify} />
      <PhoneForm setError={notify} />
    </>
  )
}

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 main.jsx olevaan ApolloClient-olion konfiguraatioon

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

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

client-olion muodostamisen yhteydessä ollut kenttä uri on korvattu kentällä link, joka määrittelee hieman monimutkaisimmissa tapauksissa, miten Apollo on yhteydessä palvelimeen. Palvelimen url on nyt kääritty funktion createHttpLink avulla sopivaksi httpLink-olioksi jota muokataan authLink-olion määrittelemän kontekstin avulla 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 undefined, 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 : undefined      }
    })

  // ...
  }

  // ...
}

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) => {
      const messages = error.graphQLErrors.map(e => e.message).join('\n')
      setError(messages)
    }
  })

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) => {
      const messages = error.graphQLErrors.map(e => e.message).join('\n')
      setError(messages)
    },
    update: (cache, response) => {      cache.updateQuery({ query: ALL_PERSONS }, ({ allPersons }) => {        return {          allPersons: allPersons.concat(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 päivittää funktion updateQuery avulla kyselyn ALL_PERSONS välimuistiin talletetun tilan lisäämällä henkilöiden joukkoon (jonka se saa parametrinsa välityksellä) 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-5.