Saltar al contenido

d

Iniciar sesión y actualizar la caché

La interfaz de nuestra aplicación muestra el directorio telefónico muy bien con el servidor actualizado. Sin embargo, si queremos agregar nuevas personas, tenemos que agregar la funcionalidad de inicio de sesión al frontend.

Inicio de sesión de usuario

Agreguemos la variable token al estado de la aplicación. Contendrá el token del usuario cuando se inicie sesión. Si token no está definido, representamos el componente LoginForm responsable del inicio de sesión del usuario. El componente recibe un controlador de errores y la función setToken como parámetros:

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

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

  return (
    // ...
  )
}

A continuación, definimos una mutación para iniciar sesión

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

El componente LoginForm funciona de forma muy similar a todos los demás componentes que realizan mutaciones que hemos creado anteriormente. Se han resaltado líneas interesantes en el código:

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

Estamos usando un hook de efectos de nuevo. Aquí se usa para guardar el valor del token en el estado del componente App y el almacenamiento local después de que el servidor haya respondido a la mutación. El uso del hook de efectos es necesario para evitar un bucle de renderizado sin fin.

Agreguemos también un botón que permite a un usuario que ha iniciado sesión cerrar la sesión. El controlador onClick del botón establece el estado token en nulo, elimina el token del almacenamiento local y restablece la caché del cliente Apollo. El último paso es importante, porque algunas consultas pueden haber obtenido datos en la caché, que solo los usuarios que iniciaron sesión deben tener acceso.

Podemos restablecer la caché usando el método resetStore de un objeto client de Apollo. Se puede acceder al cliente con el hook 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} />
    </>
  )
}

Agregar un token a un encabezado

Después de que el backend cambie, la creación de nuevas personas requiere que se envíe un token de usuario válido con la solicitud. Para enviar el token, tenemos que cambiar un poco la forma en que definimos el objeto ApolloClient en index.js.

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

El campo uri que fue usado anteriormente para crear el objeto client se ha reemplazado por el campo link, que define en un caso más complicado cómo Apollo está conectado al servidor. La url del servidor ahora está envuelta usando la función createHttpLink en un objeto httpLink adecuado. El enlace se modifica por el context definido por el objeto authLink para que un posible token en localStorage se establezca en el encabezado authorization para cada solicitud al servidor.

Crear nuevas personas y cambiar números funcionan de nuevo. Sin embargo, hay un problema restante. Si intentamos agregar una persona sin un número de teléfono, no es posible.

browser showing person validation failed

La validación falla porque el frontend envía una cadena vacía como valor de phone.

Cambiemos la función que crea nuevas personas para que establezca phone en undefined si el usuario no ha dado un valor.

const PersonForm = ({ setError }) => {
  // ...
  const submit = async (event) => {
    event.preventDefault()
    createPerson({
      variables: { 
        name, street, city,        phone: phone.length > 0 ? phone : undefined      }
    })

  // ...
  }

  // ...
}

El código actual de la aplicación se puede encontrar en Github, rama part8-6.

Actualizando caché, revisado

Tenemos que actualizar el caché del cliente Apollo al crear nuevas personas. Podemos actualizarlo usando la opción refetchQueries de la mutación para definir que la consulta ALL_PERSONS se realiza nuevamente.

const PersonForm = ({ setError }) => {
  // ...

  const [ createPerson ] = useMutation(CREATE_PERSON, {
    refetchQueries: [  {query: ALL_PERSONS} ],    onError: (error) => {
      const errors = error.graphQLErrors[0].extensions.error.errors
      const messages = Object.values(errors).map(e => e.message).join('\n')
      setError(messages)
    }
  })

Este enfoque es bastante bueno, el inconveniente es que la consulta siempre se vuelve a ejecutar con las actualizaciones.

Es posible optimizar la solución gestionando la actualización de la caché nosotros mismos. Esto se hace definiendo una llamada de devolución de actualización adecuada para la mutación, que Apollo ejecuta después la mutación:

const PersonForm = ({ setError }) => {
  // ...

  const [ createPerson ] = useMutation(CREATE_PERSON, {
    onError: (error) => {
      setError(error.graphQLErrors[0].message)
    },
    update: (cache, response) => {      cache.updateQuery({ query: ALL_PERSONS }, ({ allPersons }) => {        return {          allPersons: allPersons.concat(response.data.addPerson),        }      })    },  })
 
  // ..
}  

La función de devolución de llamada recibe una referencia al caché y los datos devueltos por la mutación como parámetros. Por ejemplo, en nuestro caso sería la persona creada.

query ALL_PERSONS in cache by adding the new person to the cached data.

Usando la función updateQuery el código actualiza la consulta ALL_PERSONS en caché agregando la nueva persona a los datos en caché.

En algunas situaciones, la única forma sensata de mantener el caché actualizado es usando la devolución de llamada update.

Cuando sea necesario, es posible deshabilitar el caché para toda la aplicación o consultas únicas configurando el campo que administra el uso del caché , fetchPolicy como sin caché.

Sea diligente con el caché. Los datos antiguos en la caché pueden causar errores difíciles de encontrar. Como sabemos, mantener el caché actualizado es un gran desafío. Según un proverbio codificador:

Solo hay dos cosas difíciles en Ciencias de la Computación: invalidación de caché y nombrar cosas. Leer más aquí.

El código actual de la aplicación se puede encontrar en Github, rama part8-7.