React Query Hooks

Easy Server State Management in React Apps

2023

@ValentinKononov

2023

@ValentinKononov

  • Fullstack software engineer
  • Typescript / Javascript
  • Golang, SQL, ... 
  • Speaker
  • Enthusiast of simple approach

Valentin Kononov

# what

Use Case

  • Load server data to web app
  • Transform and Cache
  • Track loading process
  • Handle errors
  • Update UI
  • Handle data change

What we'll cover today

# plan
  • Concept
  • Getting data
  • Work with hooks in components
  • Dependent and Sequential hooks
  • Data changing
  • Cache management
  • Samples and Demo
# default

Why not Redux?

  • Define state
  • Action constants
  • Code actions
  • Implement reducers
  • Test all of that
  • useSelector and useReducer
  • Repeat in every case!

A lot of code for rather small thing

# concept

Why RQ hooks?

  • Simplification for base use cases

 

Not a full replacement for state management

Fetch API GET endpoint

import axios from "../api";

export const getAll = async (): Promise<any> => {
  console.log("[API]: data is loading");
  const { data } = await axios.get(`/api/entity/type`);
  console.log("[API]: data is loaded");
  return data;
};
# data fetch
import { useQuery } from "react-query";

export const useServerData = (enityId: number): UseQueryResult<Entity[]> =>
  useQuery(['entity', 'cache', enityId], () => getAll(entityId));

{ can be any fetch call }

{ call to useQuery function }

Hook usage in component

# data fetch
export const HookedComponent = () => {
  const { data, isLoading, isSuccess } = useServerData();

  return (
    <>
      <h1>Data</h1>
      <div>IsLoading: {isLoading.toString()}</div>
      <div>
        {isSuccess &&
          data.map((item) => <div key={item.id}>name: {item.name}</div>)}
      </div>
    </>
  );
};

{ tools and flags to use in component }

Query Client Setup

# data fetch
import { QueryClient } from "react-query";

export const queryClient = new QueryClient();

const App = () => {
  return (
    <div>
      <QueryClientProvider client={queryClient}>
        <HookedComponent />
      </QueryClientProvider>
    </div>
  );
};

{ Triggers component rendering }

# tools

Hook returns ...

// what is returned by query
interface QueryObserverBaseResult<TData, TError> {
    data: TData | undefined;
    dataUpdatedAt: number;
    error: TError | null;
    isError: boolean;
    isIdle: boolean;
    isLoading: boolean;
    isLoadingError: boolean;
    isPlaceholderData: boolean;
    isPreviousData: boolean;
    isRefetchError: boolean;
    isRefetching: boolean;
    isStale: boolean;
    isSuccess: boolean;
    refetch: (options?: RefetchOptions) 
=> Promise<QueryObserverResult<TData, TError>>;
    remove: () => void;
    status: QueryStatus;
}

export declare type QueryStatus = 'idle' 
  | 'loading' | 'error' | 'success';
# tools

Hook takes ...

// what can be configured in hook
interface QueryObserverOptions {
    enabled?: boolean;
    // time to keep cache in ms
    staleTime?: number;
    keepPreviousData: boolean,
    refetchInterval?: number | ...;
    onSuccess?: (data: TData) => void;
    onError?: (err: TError) => void;
    // transform returned data
    select?: (data: TQueryData) => TData;
    placeholderData?: TQueryData | ...;
}
const { data, refetch } = useServerData({
    enabled: hasSomething === true,
  });

{ Which gives possibilities... }

Data Transformation

# select
export const useCharacters = (
  props: PagingProps,
  options: UseQueryOptions<Character[], unknown, Character[], string[]> = {}
): UseQueryResult<Character[]> =>
  useQuery(characterQueryIds.useCharacters(), () => getAll(props), {
    staleTime: TIME_UNITS.MINUTE * 30,
    select: React.useCallback((data: any): Character[] =>
      data.map(
        (item: Record<string, any>, index: number): Character => ({
          id: index,
          name: item.name || "who knows",
          gender: item.gender,
          culture: item.culture,
          nickName: item.aliases[0],
        })
      ), []),
    ...options,
  });

{ select function to transform data  }

{ add useCallback for better caching }

Load dependent data

# sequential hooks
// Game of Thrones entities
// HOOK #1 - loaded every time
const {
    data: housesData,
    isSuccess: housesIsSuccess,
  } = useHouses();


// HOOK #2 - loaded when #1 is successful
const {
    data: characterData,
    isSuccess: characterIsSuccess,
    refetch,
  } = useCharacters(
    {
      house: housesData[0]?.id,
    },
    {
      enabled: housesIsSuccess,
    }
  );

{ Hooks are added all at once }

{ Fetch is called only when hook is enabled }

Call API POST/... endpoint

import axios from "../api";

export const patchOne = async (payload: Character): Promise<Result> => {
  console.log("[API]: Ready to patch Character!");
  const { data } = await axios.patch(`/api/characters/${payload.id}`, payload);
  return data;
};
# data mutation
import { useMutation } from "react-query";

export const usePatchCharacter = (): UseMutationResult<...> =>
  useMutation((payload: Character) => patchOne(payload), {
    onSuccess: () => {
      console.log("Character Patched!!");
    },
    onError: (error) => {
      console.log(error);
    },
  });

{ can be any http lib }

{ call to useMutation function }

Usage in component

const { mutate } = usePatchCharacter();

<button onClick={() => mutate({...})}>Patch Character</button>
                              
// call mutate function triggers calling underling endpoint function
# data mutation

Cache management

# caching
import { useMutation } from "react-query";
import { queryClient } from "../your-code";

export const usePatchCharacter = (): UseMutationResult<...> =>
  useMutation((payload: Character) => patchOne(payload), {
    onSuccess: () => {
      console.log("Character Patched!!");
      // refresh the data
      queryClient.invalidateQueries(['entity', 'cache', payload.id]);
      // queryClient.setQueriesData(...)
    },
    onError: (error) => {
      console.log(error);
    },
  });

{ call to invalidateQueries function }

List of Queries

# array of calls with rate limit
import { useQuery, useQueries } from 'queries/react-query';
import pLimit from 'p-limit';

const serviceRateLimit = pLimit(10);

export const useDataList = (paramsList, options = {}) =>
    useQueries(paramsList.map((params) => ({
        queryKey: ['data', 'list', params.id],
        queryFn: () => serviceRateLimit(() => getAll({ ...params })),
        ...options,})));

{ perform multiple api calls }

{ with rate limitation }

{ returns array of queries }

# information

More info

{ tanstack.com/query/v4/docs/overview }

Outputs

# summary
  • Less code for basic scenarios
  • Load and cache data
  • Track loading state and error
  • Transform data
  • Mutate data and manage cache
  • NOT for client state management
  • NOT for complex dependent loading
# repo

Samples added to repo

2023

@ValentinKononov

https://github.com/valentinkononov
/react-query-hooks-templates
# wrap up

Thanks! Have a good coding!

2023

@ValentinKononov

Made with Slides.com