'Where to implement SWRs pagination for managing pagination in url?
I have been trying to solve the problem that this NextJS application does not have any pagination being handled on the backend so the idea being to pass it to query params in the url, so localhost:3000/patients?page=
.
I came close with this approach:
import React, { useEffect } from 'react'
import PatientsTable from 'components/patients/PatientsTable'
import useSWRWithToken from 'hooks/useSWRWithToken'
import Feedback from 'components/feedback'
import { useRouter } from 'next/router'
function Patients(props) {
const { data: patientsList, error: patientsListError } =
useSWRWithToken('/patients')
const router = useRouter()
const { page, rowsPerPage, onPageChange, query } = props
useEffect(() => {
const { pg } = props
const nextPage = parseInt(pg)
if (page !== nextPage) {
router.replace({
query: {
...router.query,
pg: page,
},
})
}
}, [page, query, router, router.replace])
return (
<>
<Feedback />
<PatientsTable
patientsList={patientsList}
patientsListError={patientsListError}
/>
</>
)
}
Patients.layout = 'fullScreen'
Patients.auth = true
export default Patients
but the event handlers to go to the next and previous pages stopped working:
import React from 'react'
import { IconButton } from '@mui/material'
import KeyboardArrowLeft from '@mui/icons-material/KeyboardArrowLeft'
import KeyboardArrowRight from '@mui/icons-material/KeyboardArrowRight'
import { styled, useTheme } from '@mui/system'
const Root = styled('div')(({ theme }) => ({
flexShrink: 0,
marginLeft: theme.spacing(2.5),
}))
const TablePaginationActions = (props) => {
const theme = useTheme()
const { count, page, rowsPerPage, onPageChange } = props
const handleFirstPageButtonClick = (event) => {
onPageChange(event, 0)
}
const handleBackButtonClick = (event) => {
onPageChange(event, page - 1)
}
const handleNextButtonClick = (event) => {
onPageChange(event, page + 1)
}
const handleLastPageButtonClick = (event) => {
onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1))
}
return (
<Root>
<IconButton
onClick={handleBackButtonClick}
disabled={page === 0}
aria-label="previous page"
data-cy={'table-pagination-actions-icon-button-prev'}
>
{theme.direction === 'rtl' ? (
<KeyboardArrowRight />
) : (
<KeyboardArrowLeft />
)}
</IconButton>
<IconButton
onClick={handleNextButtonClick}
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="next page"
data-cy={'table-pagination-actions-icon-button-next'}
>
{theme.direction === 'rtl' ? (
<KeyboardArrowLeft />
) : (
<KeyboardArrowRight />
)}
</IconButton>
</Root>
)
}
export default TablePaginationActions
I want to say it's because my useEffect
hook was being somehow clobbered by the useSWR
hook but I cannot prove it.
I am trying to implement the following:
https://swr.vercel.app/docs/pagination
This is the useSWRWithToken:
import useSWR from 'swr'
import fetchWithToken from 'libs/fetchWithToken'
import { useAppContext } from 'context'
function useSWRWithToken(endpoint: any, dependency: Boolean = true) {
const { state } = useAppContext()
const {
token: { accessToken },
} = state
const { data, error, mutate } = useSWR<any>(
// Make sure there is an accessToken and null does not appear in the uri
accessToken && endpoint.indexOf(null) === -1 && dependency
? endpoint
: null,
(url: string, params: object = null) =>
fetchWithToken(url, accessToken, params)
)
return {
data,
isLoading: !error && !data,
error,
mutate,
}
}
export default useSWRWithToken
This is the Patients Table component:
import React, {
useEffect,
useState,
useMemo,
useCallback,
ReactChild,
} from 'react'
import { Paper } from '@mui/material'
import { LinearLoader } from 'components/loaders'
import { useRouter } from 'next/router'
import GeneralTable from 'components/table/GeneralTable'
import { parseInt, isNil } from 'lodash'
import MultiSelectChip, {
ChipCommonFilter,
} from 'components/forms/MultiSelectChip'
import usePermissions from 'hooks/usePermissions'
import { differenceInDays } from 'date-fns'
import { useAppContext } from 'context'
import { PatientsTableColumns } from 'components/table/patients/PatientsTableColumns'
import ProviderSelect from 'components/patients/ProviderSelect'
// TODO: declare interface types
interface IPatientList {
patientsList: any
patientsListError?: any
}
function PatientsTable({
patientsList = null,
patientsListError,
}: IPatientList) {
const router = useRouter()
const { state, dispatch } = useAppContext()
const { controlLevelFilters, selectedProviderFilter } = state.patientSearch
const [providerList, setProviderList] = useState<Array<string>>([])
const [dataParsed, setDataParsed] = useState([])
const [controlFilterOptions, setControlFilterOptions] = useState(['all'])
const [scroll, setScroll] = useState(false)
const { permissionPublicUserId }: { permissionPublicUserId: boolean } =
usePermissions()
const setControlLevelFilters = (value: string[]) => {
dispatch({
type: 'SET_CONTROL_LEVEL_FILTERS',
payload: {
controlLevelFilters: value,
},
})
}
const setSelectedProviderFilter = (provider: string) => {
console.log('provider: ', provider)
dispatch({
type: 'SET_SELECTED_PROVIDER_FILTER',
payload: {
selectedProviderFilter: provider,
},
})
}
const setSortState = (memoColumn: string, memoDirection: string) => {
dispatch({
type: 'SET_PATIENT_TABLE_SORT',
payload: {
columnName: memoColumn,
direction: !memoDirection ? 'asc' : 'desc',
},
})
}
const handleChangeControlLevelFilter = (
event: any,
child: ReactChild,
deleteValue: string
) => {
ChipCommonFilter(
event,
child,
deleteValue,
setControlLevelFilters,
controlLevelFilters
)
}
useEffect(() => {
dispatch({
type: 'SET_PAGE_HEADING',
payload: {
pageHeading1: 'Patients',
pageHeading2: `${
patientsList?.length ? `(${patientsList.length})` : ''
}`,
},
})
}, [patientsList])
useEffect(() => {
// Build up a list of patient objects which our table can traverse
const dataParsed = patientsList?.map((row) => {
!isNil(row.doctorDto) &&
setProviderList((previousProviderList) => [
...previousProviderList,
`${row.doctorDto.firstName} ${row.doctorDto.lastName}`,
])
const reportedDate = row.scalarReports.filter(
(obj) => obj.name === 'lastUseInDays'
)[0]?.reportedOn
const diffDate: number = Math.abs(
differenceInDays(new Date(reportedDate), new Date())
)
const lastUsed: string | number =
diffDate > 7
? diffDate
: diffDate === 0 && !isNil(reportedDate)
? 'Today'
: isNil(reportedDate)
? '--'
: diffDate
return {
pui: row.pui,
provider: !isNil(row.doctorDto)
? `${row.doctorDto.firstName} ${row.doctorDto.lastName}`
: '',
name: `${row.firstName} ${row.lastName}`,
dob: row.dob,
asthmaControl: row.scalarReports.filter(
(obj) => obj.name === 'asthmaControl'
)[0]?.value,
lastUsed,
fev1Baseline: row.scalarReports.filter(
(obj) => obj.name === 'fevBaseLine'
)[0]?.value,
}
})
setDataParsed(dataParsed)
}, [patientsList])
useEffect(() => {
window.addEventListener('scroll', () => {
setScroll(window.scrollY > 50)
})
}, [])
useEffect(() => {
if (dataParsed) {
setControlFilterOptions([
'all',
...(dataParsed.find((patient) => patient.asthmaControl === 'good')
? ['good']
: []),
...(dataParsed.find((patient) => patient.asthmaControl === 'poor')
? ['poor']
: []),
...(dataParsed.find((patient) => patient.asthmaControl === 'veryPoor')
? ['veryPoor']
: []),
])
}
}, [dataParsed])
const handleSelectProvider = (provider) => setSelectedProviderFilter(provider)
const isToday = (val) => val === 'Today' || val === 'today'
const isInactive = (val) => val === 'Inactive' || val === 'inactive'
const isDash = (val) => isNil(val) || val === '--'
const isAsthmaControl = (val) => {
const _val = val?.toString().toLowerCase()
let result: Number | boolean = false
switch (_val) {
case 'verypoor':
result = 1
break
case 'poor':
result = 2
break
case 'good':
result = 3
break
default:
result = false
}
return result
}
const CustomSortBy = useCallback(
(rowA, rowB, colId, direction) => {
const convertValue = (val) => {
if (isToday(val)) return 0 //Today != 1
if (isInactive(val)) return direction === false ? -2 : 29999
if (isDash(val)) return direction === false ? -3 : 30000
const acResult = isAsthmaControl(val) //so we don't call it twice
if (acResult) return acResult
return parseInt(val)
}
const v1 = convertValue(rowA.values[colId])
const v2 = convertValue(rowB.values[colId])
return v1 >= v2 ? 1 : -1 // Direction var doesn't matter.
},
[patientsList, isToday, isInactive, isDash]
)
const columns = useMemo(() => PatientsTableColumns(CustomSortBy), [])
const rowLinkOnClick = (patient) => {
router.push(`/patients/${patient.pui}`)
}
// TODO put better error here
if (patientsListError) return <div>Failed to load patients list</div>
if (!patientsList) return <LinearLoader />
return (
<Paper elevation={0} square>
{patientsList && dataParsed && (
<GeneralTable
retainSortState={true}
sortStateRecorder={setSortState}
columns={columns}
data={dataParsed}
checkRows={false}
rowLinkOnClick={rowLinkOnClick}
filters={[
{
column: 'asthmaControl',
options: controlLevelFilters,
},
{
column: 'provider',
options: selectedProviderFilter,
},
]}
initialState={{
sortBy: [
{
id: state.patientTableSort.columnName,
desc: state.patientTableSort.direction === 'desc',
},
],
hiddenColumns: false && !permissionPublicUserId ? [] : ['pui'], //For now, intentionally always hide
}}
leftContent={
<div style={{ display: 'flex' }}>
<MultiSelectChip
labelText="Asthma Control"
options={controlFilterOptions}
selectedValues={controlLevelFilters}
handleChange={handleChangeControlLevelFilter}
/>
<ProviderSelect
providers={Array.from(new Set(providerList))}
providerFilter={selectedProviderFilter}
handleChange={handleSelectProvider}
/>
</div>
}
/>
)}
</Paper>
)
}
export default PatientsTable
I should also mention that global state comes into an appReducer
like so:
import combineReducers from 'react-combine-reducers'
import { ReactElement } from 'react'
enum ActionName {
SET_PAGE_HEADING = 'SET_PAGE_HEADING',
SET_TIMER_RUNNING = 'SET_TIMER_RUNNING',
SET_PATIENT_DATA = 'SET_PATIENT_DATA',
SET_CHART_PERIOD = 'SET_CHART_PERIOD',
SET_TOKEN = 'SET_TOKEN',
}
enum ChartPeriod {
WEEK = 'week',
THREE_MONTH = '1m',
ONE_MONTH = 'week',
}
type Action = {
type: string
payload: any
}
// Types for global App State
type PageHeadingState = {
pageHeading1: string
pageHeading2?: string
component?: ReactElement
}
// TODO: fill this in or reference existing type
// types like this need to be centralized
type Patient = object
type PatientDataState = [
{
[key: string]: Patient
}
]
type PatientSearch = {
controlLevelFilters: string[]
selectedProviderFilter: string[]
}
type TimerState = {
running: boolean
visitId?: string | null
stopTimer?: () => void
}
type AppState = {
pageHeading: PageHeadingState
timer: TimerState
// TODO: flesh out what the shape of this data is and type it
// once the swagger definition is complete (state: PatientDataState)
patientData: PatientDataState
chartPeriod: ChartPeriod
patientSearch: PatientSearch
patientTableSort: PatientTableSort
token: object
}
// A reducer type to aggregate (n) reducers
type AppStateReducer = (state: AppState, action: Action) => AppState
// Initial State for the app
const initialPageHeading = {
pageHeading1: '',
pageHeading2: '',
}
const initialTimer = {
running: false,
}
const initialChartPeriod = ChartPeriod.WEEK
const initialPatientData: PatientDataState = [{}]
const initialPatientSearch: PatientSearch = {
controlLevelFilters: ['all'],
selectedProviderFilter: ['All Providers'],
}
type PatientTableSort = { columnName: string; direction: 'asc' | 'desc' }
const initialPatientTableSort: PatientTableSort = {
columnName: 'asthmaControl',
direction: 'desc',
}
// Perhaps can make this more explicit with STOP_PATIENT_CARE_TIMER
// and STOP_PATIENT_CARE_TIMER action cases
// I have kept the CRUD to a minimum for this first POC
const timerReducer = (state: TimerState, action: Action) => {
switch (action.type) {
case ActionName.SET_TIMER_RUNNING:
return { ...state, ...action.payload }
// case ActionName.STOP_PATIENT_CARE_TIMER:
// return { running: false }
default:
return state
}
}
const pageHeadingReducer = (state: PageHeadingState, action: Action) => {
switch (action.type) {
case ActionName.SET_PAGE_HEADING: {
return {
...state,
...action.payload,
}
}
default:
return state
}
}
// TODO: flesh out what the shape of this data is and type it
// once the swagger definition is complete (state: PatientDataState)
const patientDataReducer = (state: PatientDataState, action) => {
switch (action.type) {
case ActionName.SET_PATIENT_DATA: {
return action.payload
}
default:
return state
}
}
const chartPeriodReducer = (state: ChartPeriod, action: Action) => {
switch (action.type) {
case ActionName.SET_CHART_PERIOD: {
return action.payload
}
default:
return state
}
}
const patientSearchReducer = (state: PatientSearch, action: Action) => {
switch (action.type) {
case 'SET_SELECTED_PROVIDER_FILTER': {
return { ...state, ...action.payload }
}
case 'SET_CONTROL_LEVEL_FILTERS': {
return { ...state, ...action.payload }
}
default:
return state
}
}
const patientTableSortReducer = (state: PatientTableSort, action: Action) => {
switch (action.type) {
case 'SET_PATIENT_TABLE_SORT': {
return { ...state, ...action.payload }
}
case 'CLEAR_PATIENT_TABLE_SORT': {
const update = { columnName: '', direction: 'asc' }
return { ...state, ...update }
}
default:
return state
}
}
const tokenReducer = (state: object, action: Action) => {
switch (action.type) {
case 'SET_TOKEN': {
return action.payload
}
default:
return state
}
}
// This is exposed for use in AppContext.tsx so bootstrap our store/state
export const [AppReducer, initialState] = combineReducers<AppStateReducer>({
pageHeading: [pageHeadingReducer, initialPageHeading],
timer: [timerReducer, initialTimer],
patientData: [patientDataReducer, initialPatientData],
chartPeriod: [chartPeriodReducer, initialChartPeriod],
patientSearch: [patientSearchReducer, initialPatientSearch],
patientTableSort: [patientTableSortReducer, initialPatientTableSort],
token: [tokenReducer, {}],
})
Solution 1:[1]
Since your global cache should consider the current page to manage your data, this data should be present in the key, then you have to send it through your hook like useSWRWithToken(`/patients/?page${currentPage}`)
and then every time the current page changes SWR will trigger a new fetch.
Instead of using string as key, I recommend using an array with all your data like
const fetcher = async (key: string, id: string, pagination: number) => {
//He in your fetch you'll receive the key ordered by the array
fetchWithToken(key, accessToken, [id, page: pagination])
}
const useSWRWithToken = (key, id, pagination) = {
const { data, error, isValidating } = useSWR(
[`patients/${id}`, id, pagination], fetcher, options
)
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | TheDev |