Last active
April 17, 2024 19:55
-
-
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)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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