Skip to content

Instantly share code, notes, and snippets.

@AlexMachin1997
Last active July 22, 2023 19:09
Show Gist options
  • Save AlexMachin1997/6462731447b82eaf15575043184151e0 to your computer and use it in GitHub Desktop.
Save AlexMachin1997/6462731447b82eaf15575043184151e0 to your computer and use it in GitHub Desktop.
A.ts uses generics without inferring from a utility function response. B.ts uses generics with infering the result from the newResource utility function response.
import * as React from 'react';
import { useQuery, QueryKey } from '@tanstack/react-query';
import axios, { AxiosRequestHeaders } from 'axios';
import { newResource, Resource } from '@models/resource';
const INITIAL_STATE = {
page: 1,
resultsPerPage: 20,
sort: '',
order: ''
};
type ACTION_TYPE =
| { type: 'CHANGE_PAGE_NUMBER'; payload: { newPageNumber: number } }
| { type: 'CHANGE_SORT'; payload: { newSort: string } }
| { type: 'CHANGE_ORDER'; payload: { newOrder: string } }
| { type: 'CHANGE_ITEMS_PER_PAGE'; payload: { newNumberOfItemsPerPage: number } };
type APP_STATE = {
page: number;
resultsPerPage: number;
sort: string;
order: string;
};
type AxiosResponse<TData> = {
content: TData[];
pageable: {
size: number;
number: number;
sort: Object;
};
totalSize: number;
};
type UseResourceParams<T> = {
isQueryEnabled: boolean;
baseQueryKey: string;
defaultValues?: APP_STATE;
initialData?: Resource<T>;
url: string;
accessToken: string | null;
};
const reducer = (state: APP_STATE, action: ACTION_TYPE) => {
switch (action.type) {
case 'CHANGE_PAGE_NUMBER': {
const { newPageNumber } = action.payload;
return {
...state,
page: newPageNumber
};
}
case 'CHANGE_SORT': {
const { newSort } = action.payload;
return {
...state,
sort: newSort
};
}
case 'CHANGE_ORDER': {
const { newOrder } = action.payload;
return {
...state,
order: newOrder
};
}
case 'CHANGE_ITEMS_PER_PAGE': {
const { newNumberOfItemsPerPage } = action.payload;
return {
...state,
resultsPerPage: newNumberOfItemsPerPage
};
}
default: {
return { ...state };
}
}
};
type A<T> = {
content: T[];
meta: {
from: number;
to: number;
total: number;
size: number;
last_page: number;
current_page: number;
};
totalSize: number;
};
const useResource = <T>({
isQueryEnabled,
baseQueryKey,
defaultValues,
initialData,
url,
accessToken
}: UseResourceParams<T> & { [key: string]: unknown }) => {
const [state, dispatch] = React.useReducer(
// Reducer for managing state
reducer,
// Inital state
{
// Copy the initial state, could contain additional values
...INITIAL_STATE,
// Override certain state properties, as there could initial pieces of state from NextJS
page: defaultValues?.page ?? INITIAL_STATE.page,
resultsPerPage: defaultValues?.resultsPerPage ?? INITIAL_STATE.resultsPerPage,
sort: defaultValues?.sort ?? INITIAL_STATE.sort,
order: defaultValues?.order ?? INITIAL_STATE.order
},
// A callback incase you want to do additional with the state or whatever
(existingState) => ({ ...existingState })
);
// Generate the qu ery key, this is exposed as you could use it for mutations
const generatedQueryKey: QueryKey = React.useMemo(
() => [
{
// Base key e.g. "audits", "hostlmss" etc
key: baseQueryKey,
// When should the query re-run automatically ??
dependencies: {
page: state.page,
resultsPerPage: state.resultsPerPage,
sort: state.sort,
order: state.order
}
}
],
[baseQueryKey, state.order, state.page, state.resultsPerPage, state.sort]
);
const { status, isRefetching, fetchStatus, data } = useQuery<A<T>, Error>({
queryKey: generatedQueryKey,
queryFn: async () => {
try {
let headers: AxiosRequestHeaders = {};
// If a token is provided attach it to the Axios Headers
if (accessToken !== null) {
headers = {
...headers,
Authorization: 'Bearer ' + accessToken
};
}
// Perform the request and wait for the response to resolve
const response = await axios.get<AxiosResponse<T>>(url, {
headers: { ...headers }
});
// If the status isn't 200 then throw an error to put the query in rejected mode
if (response.status !== 200) {
throw Error();
}
// Return the data in the approproiate format
return Promise.resolve(
newResource<T>(response.data.content, response.data.pageable, response.data.totalSize)
);
} catch {
return Promise.reject(`Failed to perform a fetch request for ${generatedQueryKey}`);
}
},
enabled: isQueryEnabled,
initialData: initialData,
keepPreviousData: true
});
return {
// Return the actual data
resource: data,
// Return the key incase it's needed for a mutation
queryKey: generatedQueryKey,
// Status contains error or success
status: status,
// In TanStack query v4 the idle status is found in the fetchStatus, this will probably change in v5 or may not who knows but Typescript will let us know
isIdle: fetchStatus === 'idle',
// A small bool flag to indicate if a refetch is appearing, can happen automatically in the background
isRefetching,
// Used for interacting with the react hook and re-running queries e.g. updating the page number will fetch the next page
state,
dispatch
};
};
export default useResource;
import * as React from 'react';
import { useQuery, QueryKey } from '@tanstack/react-query';
import axios, { AxiosRequestHeaders } from 'axios';
import { newResource, Resource } from '@models/resource';
const INITIAL_STATE = {
page: 1,
resultsPerPage: 20,
sort: '',
order: ''
};
type ACTION_TYPE =
| { type: 'CHANGE_PAGE_NUMBER'; payload: { newPageNumber: number } }
| { type: 'CHANGE_SORT'; payload: { newSort: string } }
| { type: 'CHANGE_ORDER'; payload: { newOrder: string } }
| { type: 'CHANGE_ITEMS_PER_PAGE'; payload: { newNumberOfItemsPerPage: number } };
type APP_STATE = {
page: number;
resultsPerPage: number;
sort: string;
order: string;
};
type AxiosResponse<TData> = {
content: TData[];
pageable: {
size: number;
number: number;
sort: Object;
};
totalSize: number;
};
type UseResourceParams<T> = {
isQueryEnabled: boolean;
baseQueryKey: string;
defaultValues?: APP_STATE;
initialData?: Resource<T>;
url: string;
accessToken: string | null;
};
const reducer = (state: APP_STATE, action: ACTION_TYPE) => {
switch (action.type) {
case 'CHANGE_PAGE_NUMBER': {
const { newPageNumber } = action.payload;
return {
...state,
page: newPageNumber
};
}
case 'CHANGE_SORT': {
const { newSort } = action.payload;
return {
...state,
sort: newSort
};
}
case 'CHANGE_ORDER': {
const { newOrder } = action.payload;
return {
...state,
order: newOrder
};
}
case 'CHANGE_ITEMS_PER_PAGE': {
const { newNumberOfItemsPerPage } = action.payload;
return {
...state,
resultsPerPage: newNumberOfItemsPerPage
};
}
default: {
return { ...state };
}
}
};
type A<T> = {
content: T[];
meta: {
from: number;
to: number;
total: number;
size: number;
last_page: number;
current_page: number;
};
totalSize: number;
};
const useResource = <T>({
isQueryEnabled,
baseQueryKey,
defaultValues,
initialData,
url,
accessToken
}: UseResourceParams<T> & { [key: string]: unknown }) => {
const [state, dispatch] = React.useReducer(
// Reducer for managing state
reducer,
// Inital state
{
// Copy the initial state, could contain additional values
...INITIAL_STATE,
// Override certain state properties, as there could initial pieces of state from NextJS
page: defaultValues?.page ?? INITIAL_STATE.page,
resultsPerPage: defaultValues?.resultsPerPage ?? INITIAL_STATE.resultsPerPage,
sort: defaultValues?.sort ?? INITIAL_STATE.sort,
order: defaultValues?.order ?? INITIAL_STATE.order
},
// A callback incase you want to do additional with the state or whatever
(existingState) => ({ ...existingState })
);
// Generate the qu ery key, this is exposed as you could use it for mutations
const generatedQueryKey: QueryKey = React.useMemo(
() => [
{
// Base key e.g. "audits", "hostlmss" etc
key: baseQueryKey,
// When should the query re-run automatically ??
dependencies: {
page: state.page,
resultsPerPage: state.resultsPerPage,
sort: state.sort,
order: state.order
}
}
],
[baseQueryKey, state.order, state.page, state.resultsPerPage, state.sort]
);
const { status, isRefetching, fetchStatus, data } = useQuery<
ReturnType<typeof newResource<T>>,
Error
>({
queryKey: generatedQueryKey,
queryFn: async () => {
try {
let headers: AxiosRequestHeaders = {};
// If a token is provided attach it to the Axios Headers
if (accessToken !== null) {
headers = {
...headers,
Authorization: 'Bearer ' + accessToken
};
}
// Perform the request and wait for the response to resolve
const response = await axios.get<AxiosResponse<T>>(url, {
headers: { ...headers }
});
// If the status isn't 200 then throw an error to put the query in rejected mode
if (response.status !== 200) {
throw Error();
}
// Return the data in the approproiate format
return Promise.resolve(
newResource<T>(response.data.content, response.data.pageable, response.data.totalSize)
);
} catch {
return Promise.reject(`Failed to perform a fetch request for ${generatedQueryKey}`);
}
},
enabled: isQueryEnabled,
initialData: initialData,
keepPreviousData: true
});
return {
// Return the actual data
resource: data,
// Return the key incase it's needed for a mutation
queryKey: generatedQueryKey,
// Status contains error or success
status: status,
// In TanStack query v4 the idle status is found in the fetchStatus, this will probably change in v5 or may not who knows but Typescript will let us know
isIdle: fetchStatus === 'idle',
// A small bool flag to indicate if a refetch is appearing, can happen automatically in the background
isRefetching,
// Used for interacting with the react hook and re-running queries e.g. updating the page number will fetch the next page
state,
dispatch
};
};
export default useResource;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment