Skip to content

State management

React-Query logo

We will need to store the data received in a way that allows it to be used across multiple components. To manage this state we'll use React-Query as I find it much easier to use than say other popular tools like Redux or MobX.

React-Query is installed via npm,

Run this command in frontend/

npm install --save react-query

To use React-Query a QueryClient must be provided via React-Query's QueryClientProvider, which is achieved by adding the following to frontend/src/App.tsx,

...  // Existing imports
import { QueryClient, QueryClientProvider } from "react-query";

const queryClient = new QueryClient();

const App => {
  ...  // Existing code

  return (
    <QueryClientProvider client={queryClient}>
      ... // Existing components
    </QueryClientProvider>
  );
};

Handling authentication

We need to adapt React-Query so that requests that aren't authenticated result in changes to the AuthContext. This is to handle the case where a user visits a page without logging in first. We'll also only allow retries if the server doesn't respond, or responds with a 5XX status code.

To do so we'll write a wrapper around React-Query's useQuery and useMutation by adding the following to frontend/src/query.ts,

import { AxiosError } from "axios";
import { useContext } from "react";
import {
  MutationFunction,
  QueryFunction,
  QueryFunctionContext,
  QueryKey,
  useMutation as useReactMutation,
  UseMutationOptions,
  UseMutationResult,
  useQuery as useReactQuery,
  UseQueryOptions,
  UseQueryResult,
} from "react-query";

import { AuthContext } from "src/AuthContext";

const MAX_FAILURES = 2;

export function useQuery<
  TQueryFnData = unknown,
  TData = TQueryFnData,
  TQueryKey extends QueryKey = QueryKey,
>(
  queryKey: TQueryKey,
  queryFn: QueryFunction<TQueryFnData, TQueryKey>,
  options?: UseQueryOptions<TQueryFnData, AxiosError, TData, TQueryKey>,
): UseQueryResult<TData, AxiosError> {
  const { setAuthenticated } = useContext(AuthContext);

  return useReactQuery<TQueryFnData, AxiosError, TData, TQueryKey>(
    queryKey,
    async (context: QueryFunctionContext<TQueryKey>) => {
      try {
        return await queryFn(context);
      } catch (error) {
        if (error.response && error.response.status === 401) {
          setAuthenticated(false);
        }
        throw error;
      }
    },
    {
      retry: (_: any, error: AxiosError) =>
        failureCount < MAX_FAILURES &&
        (!error.response || error.response.status >= 500),
      ...options,
    },
  );
}

export function useMutation<
  TData = unknown,
  TVariables = void,
  TContext = unknown,
>(
  mutationFn: MutationFunction<TData, TVariables>,
  options?: UseMutationOptions<TData, AxiosError, TVariables, TContext>,
): UseMutationResult<TData, AxiosError, TVariables, TContext> {
  const { setAuthenticated } = useContext(AuthContext);

  return useReactMutation<TData, AxiosError, TVariables, TContext>(
    async (variables: TVariables) => {
      try {
        return await mutationFn(variables);
      } catch (error) {
        if (error.response && error.response.status === 401) {
          setAuthenticated(false);
        }
        throw error;
      }
    },
    {
      retry: (_: any, error: AxiosError) =>
        failureCount < MAX_FAILURES &&
        (!error.response || error.response.status >= 500),
      ...options,
    },
  );
}