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. And in case you missed it, here's How to urql, Part 1.