DEV Community

Cover image for How to urql, authentication & multiple users
Jovi De Croock
Jovi De Croock

Posted on • Originally published at formidable.com

How to urql, authentication & multiple users

Introduction

In the last blog-post we covered the basics on how to query and mutate our data; in real-world applications, there's more to it. In this post, we'll cover setting an authentication token and handling multiple users interacting with the same data.

You can follow along by using this template.

The template above builds on the example we introduced in the previous blog post.

Authentication

Authentication is one of the most common needs in an application. When users log in, we need to provide an authentication token that we can use in requests.

First, let's build our login flow and change the behavior of our app so that users can't complete todos unless they have an authentication token.

When we navigate to Login.js, we see that there's a basic setup built for us, we have a <form> with an onSubmit, and an <input> controlling a variable called name.

We'll use the useMutation hook, which we discussed in the previous post, to log in and get a token.

import { useMutation } from 'urql';

export const Login = ({ setIsAuthenticated }) => {
  const [name, setName] = React.useState("");

  const [data, login] = useMutation(`
      mutation ($name: String!) {
          login (name: $name)
      }
  `);

  const handleSubmit = (e) => {
    e.preventDefault(); // no page reload due to submit
    login({ name }).then(({ data }) => {
      if (data.login) {
        setToken(data.login);
        setIsAuthenticated(true);
      }
    })
  }

  return (
    <form onSubmit={handleSubmit}>
      <h2>Login</h2>
      <input value={name} onChange={e => setName(e.currentTarget.value)} />
      <button disabled={data.fetching} type="sumbit">Log in!</button>
    </form>
  )
}

Once we have our token, the setToken method stores it in localStorage, and we notify the parent that we are authenticated with the setIsAuthenticated method.

After logging in we can see our todos, but we are no yet able to toggle the state of a todo. We still need to tell urql to send our authentication token to our server. The urql client has a property called fetchOptions that can be used to add data to our fetch request. This property can be set when we create the client. Let's go back to App.js and add the fetchOptions property so we can send the authentication token along with the toggleTodo request.

const client = createClient({
  ...
  fetchOptions: () => {
    const token = getToken();
    return token ? { headers: { Authorization: `Bearer ${token}` } } : {};
  },
});

The fetchOptions method can accept a function or an object. We will use a function so it will be executed every time we make a fetch request, and will always send an up-to-date authentication token to the server.

Consistent data

What if we want to build a shared todo app, and keep track of the last person to toggle each todo by means of an updatedBy field? How can we make sure our data gets updated correctly and keep our UI from getting outdated when multiple people are interacting with the same data?

A simple solution would be to add polling to our useQuery hook. Polling involves repeatedly dispatching the same query at a regular interval (specified by pollInterval). With this solution, we need to be aware of caching. If our requestPolicy is cache-first or cache-only then we'll keep hitting the cache and we won't actually refetch the data. cache-and-network is an appropriate requestPolicy for a polling solution.

Let's look at how our query looks after adding a pollInterval — let's say we want to refetch our todos every second.

const [data] = useQuery({
  query: `...`,
  requestPolicy: 'cache-and-network',
  pollInterval: 1000,
});

While refetching, data.stale will be true since we are serving a cached result while a refetch is happening.

We can test this by opening a new browser window and toggling a todo. We'll see that after the polled request completes the data will be in sync again. We can increase the pollInterval to see this more clearly.

Polling is a straight-forward solution, but dispatching network requests every second, regardless of whether anything has changed, is inefficient. Polling can also be problematic in situations where data is changing rapidly since there's still a time-window between requests where data can get out of sync. Let's remove the pollInterval and look at another option.

GraphQL contains another root field, the two we know now are query and mutation but we also have subscription, which builds on websockets. Instead of polling for changes, we can subscribe to events, like toggling the state of a todo.

In the last post, we touched on the concept of exchanges. Now we're going to add one of these exchanges to make our client support subscriptions. urql exposes the subscriptionExchange for this purpose, this is a factory function that returns an exchange.

Let's start by adding a transport-layer for our subscriptions.

npm i --save subscriptions-transport-ws
# or 
yarn add subscriptions-transport-ws

Now we can add the subscriptionExchange to the exchanges of our client!

import {
  cacheExchange,
  createClient,
  dedupExchange,
  fetchExchange,
  subscriptionExchange,
} from 'urql';
import { SubscriptionClient } from 'subscriptions-transport-ws';

const subscriptionClient = new SubscriptionClient(
  'wss://k1ths.sse.codesandbox.io/graphql',
  {},
);

const subscriptions = subscriptionExchange({
  forwardSubscription: operation => subscriptionClient.request(operation), 
});

const client = createClient({
  ...
  exchanges: [
    dedupExchange,
    cacheExchange,
    fetchExchange,
    subscriptions,
  ],
});

The ordering of exchanges is important: We want to first deduplicate our requests, then look into the cache, fetch it when it's not there, and run a subscription if it can't be fetched.

Now we are ready to alter the way we currently handle our todos data. Because we don't want to mutate the array of todos we get returned from urql we will introduce a mechanism based on useState and useEffect to save them in our own state.

This way we can have the useSubscription alter our state instead of keeping its own internal state.

import { useQuery, useSubscription } from 'urql';

const Todos = () => {
  const [todos, setTodos] = React.useState([]);
  const [todosResult] = useQuery({ query: TodosQuery }));

  // We're making a mutable reference where we'll keep the value
  // for fetching from the previous render.
  const previousFetching = React.useRef(todosResult.fetching);

  useSubscription(
    {
      query: `
        subscription {
          updateTodo {
            id
            text
            complete
            updatedBy
          }
        }
      `
    },
    // This callback will be invoked every time the subscription
    // gets notified of an updated todo.
    (_, result) => {
      const todo = todos.find(({ id }) => id === result.updateTodo.id);
      if (todo) {
        const newTodos = [...todos];
        newTodos[todos.indexOf(todo)] = result.updateTodo;
        setTodos(newTodos);
      }
    }
  );

  React.useEffect(() => {
    // When we transition from fetching to not fetching and we have
    // data we'll set these todos as our current set.
    if (previousFetching.current && !todosResult.fetching && todosResult.data) {
      setTodos(todosResult.data.todos);
    }
    // set the fetching on the mutable ref
    previousFetching.current = todosResult.fetching;
  }, [todosResult]); // When our result changes trigger this.

  return todos.map(...)
}

We use a little trick to see if we transition from fetching in the previous render to having data in the next. When a subscription triggers, we find the old todo and update state to include its new value.

Now we have introduced a consistent UI that can be used by multiple users simultaneously!

Note that we'll see a more elegant way of updating this todo when we reach the normalized caching post!

Conclusion

We've now learned how to handle authentication and keep our data consistent when there are multiple users interacting with it.

Next up we will be learning how to make our application more performant by using a normalized cache to avoid having to refetch on every mutation.

Top comments (0)