Easy Server State Management in React Apps
2023
@ValentinKononov
2023
@ValentinKononov
# what
# plan
# default
A lot of code for rather small thing
# concept
Not a full replacement for state management
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 }
# 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 }
# 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
// 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
// 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... }
# 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 }
# 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 }
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 }
const { mutate } = usePatchCharacter();
<button onClick={() => mutate({...})}>Patch Character</button>
// call mutate function triggers calling underling endpoint function
# data mutation
# 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 }
# 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
{ tanstack.com/query/v4/docs/overview }
# summary
# repo
2023
@ValentinKononov
https://github.com/valentinkononov
/react-query-hooks-templates
# wrap up
2023
@ValentinKononov