Part 76: Enhancing User Authentication with a Custom React Hook

[Pages] React Query

[Pages] React Query

In modern web development, creating a seamless user experience often involves efficiently managing API requests and state updates. When building a login system, we can simplify our codebase by encapsulating common functionality into reusable components or hooks. In this blog post, we'll explore how to create a custom React hook to handle the login process and streamline our code.

The Objective: Creating a Custom Hook for Sign-In

Our goal is to refactor the login logic from the SignInPage component into a custom hook called useSignIn. This will encapsulate the login process, including the API call and cache update, making our SignInPage component cleaner and more focused.

Problem with Direct Code Movement

Initially, the SignInPage component uses React Query's useMutation to handle login requests, updating the cache with queryClient.setQueryData. However, moving this logic directly into a separate file presents a challenge: state variables like email and password are not accessible outside the component.

Solution: Passing Parameters to the Mutation Function

React Query provides a mechanism to pass parameters to the mutation function, allowing us to send email and password as arguments. This makes it possible to move the login logic into a custom hook.

Creating the Custom Hook: useSignIn

Let's create the useSignIn hook that encapsulates the login logic, handles API interaction, and updates the cache.

// File: hooks/user.js

import { useMutation, useQueryClient } from 'react-query';
import { fetchJson } from '../lib/api';

export function useSignIn() {
  const queryClient = useQueryClient();
  const mutation = useMutation(({ email, password }) => fetchJson('/api/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email, password }),
  }));
  
  return {
    signIn: async (email, password) => {
      try {
        const user = await mutation.mutateAsync({ email, password });
        queryClient.setQueryData('user', user);
        return true;
      } catch (err) {
        return false;
      }
    },
    signInError: mutation.isError,
    signInLoading: mutation.isLoading,
  };
}

Key Elements of useSignIn

  1. Mutation Function: The hook uses useMutation to define the API request logic, receiving email and password as arguments.

  2. Cache Update: After a successful login, setQueryData updates the cache with the new user data.

  3. Return Object: The hook returns an object containing:

    • signIn: An asynchronous function to execute the login.

    • signInError: A boolean indicating if there was an error.

    • signInLoading: A boolean indicating if the request is in progress.

Refactoring SignInPage

Now, let's refactor SignInPage to use useSignIn.

// File: pages/sign-in.js

import { useRouter } from 'next/router';
import { useState } from 'react';
import Button from '../components/Button';
import Field from '../components/Field';
import Input from '../components/Input';
import { useSignIn } from '../hooks/user';

function SignInPage() {
  const router = useRouter();
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const { signIn, signInError, signInLoading } = useSignIn();

  const handleSubmit = async (event) => {
    event.preventDefault();
    const valid = await signIn(email, password);
    if (valid) {
      router.push('/');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <Field label="Email">
        <Input
          type="email"
          value={email}
          onChange={(event) => setEmail(event.target.value)}
        />
      </Field>
      <Field label="Password">
        <Input
          type="password"
          value={password}
          onChange={(event) => setPassword(event.target.value)}
        />
      </Field>
      {signInError && (
        <p className="text-red-700">Invalid credentials</p>
      )}
      {signInLoading ? (
        <p>Loading...</p>
      ) : (
        <Button type="submit">Sign In</Button>
      )}
    </form>
  );
}

export default SignInPage;

Benefits of Using useSignIn

  1. Code Reusability: The login logic is encapsulated, making it reusable across different components.

  2. Cleaner Components: SignInPage is now more focused on rendering, with minimal logic.

  3. Customizable Behavior: The hook can be tailored to return specific values, improving code readability and maintainability.

Conclusion

By extracting the login logic into a custom hook, we've not only cleaned up our component code but also made our logic more reusable and maintainable. This approach is a great way to manage complex stateful logic in React applications, providing a modular and efficient way to handle API interactions.

Last updated