Skip to content

Instantly share code, notes, and snippets.

@AlexMachin1997
Last active April 17, 2024 19:55
Show Gist options
  • Save AlexMachin1997/25fdc0473cb420fcf8223db17b18c340 to your computer and use it in GitHub Desktop.
Save AlexMachin1997/25fdc0473cb420fcf8223db17b18c340 to your computer and use it in GitHub Desktop.
Example usage of TanStack table and query (Allows both the use of using a meta framework e.g. nextjs pages directory which will update the url and for you to just use state if you want to just us pure client side actions)
import * as React from 'react';
import { useQuery, QueryKey } from '@tanstack/react-query';
import axios, { AxiosRequestHeaders } from 'axios';
import { newResource, Resource } from '@models/resource';
import { PaginationState, SortingState } from '@tanstack/react-table';
const INITIAL_STATE: { pagination: PaginationState; sort: SortingState } = {
pagination: {
pageIndex: 0,
pageSize: 20
},
sort: []
};
type ACTION_TYPE =
| { type: 'CHANGE_PAGE_NUMBER'; payload: { newIncomingPageNumberState: number } }
| { type: 'CHANGE_SORT'; payload: { newIncomingSortState: SortingState } }
| { type: 'CHANGE_ITEMS_PER_PAGE'; payload: { newNumberOfItemsPerPage: number } };
type APP_STATE = {
pagination: PaginationState;
sort: SortingState;
};
type AxiosResponse<TData> = {
content: TData[];
pageable: {
size: number;
number: number;
sort: Object;
};
totalSize: number;
};
type UseResourceParams<T> = {
isQueryEnabled?: boolean;
baseQueryKey: string;
defaultValues?: {
pagination: PaginationState;
sort: SortingState;
};
initialData?: Resource<T>;
url: string;
accessToken?: string | null;
useClientSideSorting?: boolean;
useClientSidePaging?: boolean;
externalState?: {
pagination: PaginationState;
sort: SortingState;
};
};
const reducer = (state: APP_STATE, action: ACTION_TYPE) => {
switch (action.type) {
case 'CHANGE_PAGE_NUMBER': {
const { newIncomingPageNumberState } = action.payload;
return {
...state,
pageIndex: newIncomingPageNumberState
};
}
case 'CHANGE_SORT': {
const { newIncomingSortState } = action.payload;
return {
...state,
sort: newIncomingSortState
};
}
case 'CHANGE_ITEMS_PER_PAGE': {
const { newNumberOfItemsPerPage } = action.payload;
return {
...state,
resultsPerPage: newNumberOfItemsPerPage,
pageIndex: 0
};
}
default: {
return { ...state };
}
}
};
const useResource = <T>({
isQueryEnabled = false,
baseQueryKey,
defaultValues,
initialData,
url,
accessToken = null,
useClientSideSorting = false,
useClientSidePaging = false,
externalState
}: UseResourceParams<T> & { [key: string]: unknown }) => {
// Used to update the useResource state e.g. page number, current sort, number of items per page etc
const [state, dispatch] = React.useReducer(
// Reducer for managing state
reducer,
// Inital state
{
// Copy the initial state, could contain additional values
...INITIAL_STATE,
pagination: {
pageSize: defaultValues?.pagination.pageSize ?? INITIAL_STATE.pagination.pageSize,
pageIndex: defaultValues?.pagination?.pageIndex ?? INITIAL_STATE.pagination.pageIndex
},
sort: defaultValues?.sort ?? INITIAL_STATE.sort
},
// A callback incase you want to do additional with the state or whatever
(existingState) => ({ ...existingState })
);
// Creates the dependencies for the TanStack query, whenever these change a request will be performend.
const dependencies = React.useMemo(
() => ({
// Page number (Pagination state)
pageIndex:
useClientSidePaging === true
? state.pagination.pageIndex
: externalState?.pagination?.pageIndex ?? INITIAL_STATE?.pagination.pageIndex,
// Number of items per page (Pagination state)
pageSize:
useClientSidePaging === true
? state.pagination.pageSize
: externalState?.pagination?.pageSize ?? INITIAL_STATE?.pagination.pageSize,
// Current sort/s applied (Sorting state)
sort: useClientSideSorting === true ? state.sort : externalState?.sort ?? INITIAL_STATE?.sort
}),
[
externalState?.pagination?.pageIndex,
externalState?.pagination?.pageSize,
externalState?.sort,
state.pagination.pageIndex,
state.pagination.pageSize,
state.sort,
useClientSidePaging,
useClientSideSorting
]
);
// Generate the query key, this is exposed as you could use it for mutations to update the cache
const generatedQueryKey: QueryKey = React.useMemo(
() => [
{
// Base key e.g. "audits", "hostlmss" etc
key: baseQueryKey,
// Define the dependencies for the query
dependencies: dependencies
}
],
[baseQueryKey, dependencies]
);
const { status, isRefetching, fetchStatus, data } = useQuery<
ReturnType<typeof newResource<T>>,
Error
>({
queryKey: generatedQueryKey,
queryFn: async () => {
try {
// Stores the requests headers
let headers: AxiosRequestHeaders = {};
// If available the accessToken is available append it to the Authiruzation Header
if (accessToken !== null) {
headers = {
...headers,
Authorization: 'Bearer ' + accessToken
};
}
// Includes the base url e.g. /audit, /hostlms
let requestUrl = url;
// TODO: Append the new data to the url to update the current resource being displayed
// NOTE: When appending the parameters read from the "dependencies" as that contains the data we need e.g. sorting or paging etc
// Perform the request and wait for the response to resolve
const response = await axios.get<AxiosResponse<T>>(requestUrl, {
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.toString()}`
);
}
},
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,
// Contains both reducer state and rerived state
// Derived state will always be available reguardless of the mode (externalData or not), so if your using externalData you can still read properties like totalNumberOfPages
state: {
// Copy any existing state, this is available reguardless of the mode, just don't use it when you rely on externalData
...state,
// Calculate the total number of pages available e.g. 1000/20 = 50 items per page
totalNumberOfPages: Math.ceil((data?.meta?.total ?? 0) / dependencies.pageSize)
},
dispatch
};
};
export default useResource;
import * as React from 'react';
import { FontAwesomeIcon, FontAwesomeIconProps } from '@fortawesome/react-fontawesome';
import { faSort, faSortDown, faSortUp } from '@fortawesome/free-solid-svg-icons';
import {
ColumnDef,
getCoreRowModel,
useReactTable,
flexRender,
OnChangeFn,
PaginationState,
TableState,
SortingState,
getPaginationRowModel
} from '@tanstack/react-table';
import { Button, Table } from 'react-bootstrap';
import Link from 'next/link';
import { useRouter } from 'next/router';
const SortIcon = ({
isDescending = false,
isAscending = false
}: {
isDescending: boolean;
isAscending: boolean;
}) => {
const iconProps: Partial<FontAwesomeIconProps> = {
fixedWidth: true,
size: 'xs'
};
if (isAscending === true) {
return <FontAwesomeIcon icon={faSortUp} {...iconProps} />;
}
if (isDescending === true) {
return <FontAwesomeIcon icon={faSortDown} {...iconProps} />;
}
return <FontAwesomeIcon icon={faSort} {...iconProps} />;
};
export default function TanStackTable<T extends Object>({
data = [],
columns,
pageCount,
paginationState,
onPaginationChange = undefined,
enableTableSorting = false,
useClientSideActions = false,
onSortingChange = undefined,
sortingState = undefined,
manualSort = true,
enableMultiSort = false,
manualPagination = true
}: {
data: Array<T>;
columns: ColumnDef<T, any>[]; // TODO: Find out the type def should be for the second argument of ColumnDef
onPaginationChange?: OnChangeFn<PaginationState>;
pageCount?: number;
paginationState?: PaginationState;
enableTableSorting?: boolean;
useClientSideActions?: boolean;
onSortingChange?: OnChangeFn<SortingState>;
sortingState?: SortingState;
manualSort?: boolean;
enableMultiSort?: boolean;
manualPagination?: boolean;
}) {
// TODO: Figure out a nice way to handle this instead of this hook directly consuming NextJS hook directly.
const router = useRouter();
const MemoizedState = React.useMemo(() => {
// Core table state, can be partially overriden on a per property basies e.g. just filter or sort or even both
let state: Partial<TableState> = {};
// If the paginationState exists then you must provide both the pageIndex and pageSize
if (paginationState !== undefined) {
state = {
...state,
pagination: {
pageIndex: paginationState?.pageIndex ?? 0,
pageSize: paginationState?.pageSize ?? 20
}
};
}
// If the sortingState exists then you must provide both the id and desc for each column/s you want to sort
if (sortingState !== undefined) {
state = {
...state,
sorting: sortingState ?? []
};
}
return state;
}, [paginationState, sortingState]);
// Generate the tables core logic e.g. header groups and the actual rows to genrate
const { getHeaderGroups, getRowModel } = useReactTable<T>({
// Core table functionality
data: data,
columns: columns,
getCoreRowModel: getCoreRowModel<T>(),
state: MemoizedState,
// Table pagination functionality
onPaginationChange: onPaginationChange,
manualPagination: manualPagination,
pageCount: pageCount,
getPaginationRowModel: manualPagination === true ? undefined : getPaginationRowModel<T>(), // The getPaginationRowModel() is only needed when your not manually sorting
// Table sorting functionality
enableMultiSort: enableMultiSort,
enableSorting: enableTableSorting,
onSortingChange: onSortingChange,
manualSorting: manualSort
});
return (
<Table responsive bordered hover>
<thead className='bg-light'>
{getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th key={header.id}>
<div className='d-flex align-items-center justify-content-between'>
{/* Render the header element via the flexRender util (It's not actually flexbox related at all, it's ust a fancy render function)*/}
{flexRender(header.column.columnDef.header, header.getContext())}
{/* If the column can sort then provide one of the sorting controls */}
{header.column.getCanSort() && (
<>
{/* Either use the Link component to update the query parameters or triggler the column callback to trigger the onSortingChange event */}
<>
{useClientSideActions === false ? (
<Link
className='btn btn-link'
href={{
pathname: router.pathname,
query: {
// Spread the current parameters so none of the other properties are orverriden (This includes both nextjs route ones and any arbiturary ones e.g. example='true')
...router.query,
// Assign the sort parameter the column id (Prefixed with the various prmeters e.g. locationCode)
sort: [header?.column?.columnDef?.id ?? ''],
// Assign the order parameter the current sort of the column
order: header.column.getIsSorted() === 'asc' ? 'desc' : 'asc'
}
}}
>
<SortIcon
isAscending={header.column.getIsSorted() === 'asc'}
isDescending={header.column.getIsSorted() === 'desc'}
/>
</Link>
) : (
<Button
type='button'
onClick={header.column.getToggleSortingHandler()}
variant='link'
>
<SortIcon
isAscending={header.column.getIsSorted() === 'asc'}
isDescending={header.column.getIsSorted() === 'desc'}
/>
</Button>
)}
</>
</>
)}
</div>
</th>
))}
</tr>
))}
</thead>
<tbody>
{getRowModel().rows.map((row) => (
<tr key={row.id}>
{row.getVisibleCells().map((cell) => (
<td key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</td>
))}
</tr>
))}
</tbody>
</Table>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment