[Part 1] Setting up an authentication workflow with React Query

[Part 1] Setting up an authentication workflow with React Query

Most React projects need some sort of user authentication workflow. If you're not thoughtful about this process at the outset, things can become messy quickly.

I recently discovered react-query and want to demonstrate how it can be used to make the authentication process a lot cleaner.

In this series we'll cover...

  • Login and registration with useMutation
  • Querying the user data on load with useQuery
  • Defining private routes

Prerequisites

  • A React project (I use CRA )
  • Packages: react-router-dom, react-query, axios, (optional) @chakra-ui/react and (optional) react-hook-form
  • Configure your app to use react-query
  • A backend service for handling requests

Registration

The first thing we want to do is create a form that registers a new user. I'm using Chakra for components and React Hook Form to manage form data, but you can use whatever you want.

With react-query, whenever you're creating, editing or deleting a resource with a server-side request, you use useMutation.

This hooks allows you to define which endpoint you're requesting, and what happens on either success or failure. It also exposes the request's internal state, such as idle, success, error, etc.

Let's look at some code.

import { Link as RouteLink, useHistory } from 'react-router-dom';
import { useMutation, useQueryClient } from 'react-query';
import { useForm } from 'react-hook-form';
import { Input, Button, Heading, VStack, Box, Flex, Link, Text } from '@chakra-ui/react';

import Api from 'api';
import { ROUTES } from 'const';
import AuthLayout from './AuthLayout';

function Register() {
  const queryClient = useQueryClient();
  const history = useHistory();
  const { register, handleSubmit } = useForm();
  const registerAction = useMutation(data => Api.user.register(data), {
    onSuccess: data => {
      Api.setToken(data.token);
      queryClient.setQueryData('user', data.user);
      history.push(ROUTES.DASHBOARD);
    }
  });

  const onSubmit = data => registerAction.mutate(data);

  return (
      <AuthLayout>
        <Heading>
          Create Account
        </Heading>
        <form onSubmit={handleSubmit(onSubmit)}>
          <VStack>
            <Box>
              <Heading>Email</Heading>
              <Input placeholder="john@muir.com"
                     {...register('email', { required: true })}
              />
            </Box>
            <Box>
              <Flex>
                <Heading>Create Password</Heading>
              </Flex>
              <Input type="password"
                     placeholder="•••••••"
                     {...register('password', { required: true })}
              />
            </Box>
            <Button type="submit" isLoading={registerAction.isLoading}>
              Create Account
            </Button>
          </VStack>
        </form>
      </AuthLayout>
  );
}

export default Register;

For our purposes, we're going to focus on the code that uses react-query.

The first thing you'll notice is that we're using the useQueryClient hook: const queryClient = useQueryClient();. This gives us access to react-query's store.

Next, we define our registration action with the useMutation hook. The first argument passed to the hook is a callback function that handles the request. I've created a custom Api class that abstracts the details away, but you might use axios or fetch to make the request.

The second argument in the hook is the options object, within which we have an onSuccess callback that fires on success. In my case, I'm using a JWT token for authentication, which is returned from the server.

Api.setToken(data.token); is, again, an abstraction. My Api class uses setToken to store the token in local storage and updates axios's headers to pass the token along with its requests. (I may dig deeper into this setup in a future article.)

Again, within the success callback, we call queryClient.setQueryData('user', data.user);. setQueryData saves our data in react-query's store using a key, in this case 'user'. This is what we will use to see if a user has access to a protected route (we'll go further into this in part 3).

Lastly, in the callback, we redirect the user to the dashboard.

In a real-world example, you would also want to handle a failed request using the onError callback within the options object.

Initiating the request

So now we have our registration logic in place, but we need a way to actually make the request. Fortunately, useMutation exposes a mutate method: const onSubmit = data => registerAction.mutate(data);

The strategy above uses react-hook-form's flow for submitting the form. Basically, it runs its validators before calling onSubmit. Your setup may look different, but the important thing to know is that you pass your form data to the request using the mutate method returned from react-query's useMutate hook.

Monitoring the request

The last thing I'll note is how easy it is to monitor the request's status with react-query.

<Button type="submit" isLoading={registerAction.isLoading}>

We don't have to write our own logic to capture the request's progression. Instead, we can simply tap into the mutation's state: registerAction.isLoading. To see a full list of states, check out the docs.

Conclusion

Logging a user in, rather than registering them, is a very similar process on the frontend. The request is different and there may be different inputs, but the authentication process doesn't change.

To be honest, I wasn't a fan of react-query at first because it seemed like overkill. And for some projects, it might be. However, once you get the hang of it, you realize that it is extremely flexible and has the potential to remove a ton of boilerplate code.

In the next part of this series, we'll dig into the process of authenticating a user when the app loads, if a token is available.

In the third, and final, part, I'll show you how easy it is to create private routes with react-query and the architecture laid out above.