'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