State management
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,
},
);
}