The frontend of our application shows the phone directory just fine with the updated server. However if we want to add new persons, we have to add a login functionality to the frontend.

User log in

Let's add the variable token to the application's state. It saves the token when the user has logged in. If token is undefined, we show the component responsible for logging in, LoginForm. It is given the function responsible for the mutation, login, as a parameter:

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 (
    // ...
  )
}

If the login operation fails, an error message is shown in the App component thanks to the onError handler set to the login mutation.

If login is successful, the token it returns is saved to the state of the App component. The token is also saved to local storage. This way it is easier to access when we want to add it to the Authorization-header of a request.

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

Let's also add a button which enables a logged in user to log out. The buttons onClick handler sets the token state to null, removes the token from local storage and resets the cache of the Apollo client.

The last is important, because some queries might have fetched data to cache, which only logged in users should have access to.

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

  // ...

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

  // ...
}

The current code of the application can be found on Github, branch part8-6.

Adding a token to a header

After the backend changes, creating new persons requires that a valid user token is sent with the request. In order to send the token, we have to change the way we define the ApolloClient-object in index.js a little.

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

The new definition uses apollo-boost-library. According to its documentation:

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.

So apollo-boost offers an easy way to configure ApolloClient with settings suitable for most situations.

Even though it would be possible to also configure the request headers with apollo-boost, we will now abandon it and do the configuration ourselves.

The configuration is as follows:

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

It requires installing two libraries:

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

client is now configured using ApolloClient constructor function of apollo-link. It has two parameters, link and cache. The latter defines, that the application now uses a cache operating in the main memory InMemoryCache.

The first parameter link defines how the client contacts the server. The communication is based on httpLink, a normal connection over HTTP with the addition that a token from localStorage is set as the value of the authorization header if it exists.

Creating new persons and changing numbers works again. There is however one remaining problem. If we try to add a person without a phone number, it is not possible.

fullstack content

Validation fails, because frontend sends an empty string as the value of phone.

Let's change the function creating new persons so that it sets phone to null if user has not given a value.

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

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

  // ...
  }

  // ...
}

Current application code can be found on Github, branch part8-7.

Updating cache, revisited

When adding new persons, we must declare that the cache of Apollo client has to be updated. The cache can be updated by using the option refetchQueries on the mutation to force ALL_PERSONS query to be rerun.

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

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

  // ..
}

This approach is pretty good, the drawback being that the query is always rerun with any updates.

It is possible to optimize the solution by handling updating the cache ourselves. This is done by defining a suitable update-callback for the mutation, which Apollo runs after the mutation:

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

The callback function is given a reference to the cache and the data returned by the mutation as parameters. For example, in our case this would be the created person.

The code reads the cached state of ALL_PERSONS query using readQuery function and updates the cache with writeQuery function adding the new person to the cached data.

There are some situations where the only good way to keep the cache up to date is using update -callbacks.

When necessary it is possible to disable cache for the whole application or single queries by setting the field managing the use of cache, fetchPolicy as no-cache.

We could declare that the address details of a single person are not saved to cache:

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

  // ...
}

We will however leave the code as is.

Be diligent with the cache. Old data in cache can cause hard to find bugs. As we know, keeping the cache up to date is very challenging. According to a coder proverb:

There are only two hard things in Computer Science: cache invalidation and naming things. Read more here.

The current code of the application can be found on Github, branch part8-8.