在哪里实施 SWRs 分页来管理 url 中的分页?

Where to implement SWRs pagination for managing pagination in url?

我一直在尝试解决这个 NextJS 应用程序没有在后端处理任何分页的问题,所以我的想法是将它传递给 url 中的查询参数,所以 localhost:3000/patients?page=.

我接近这种方法:

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

但是转到下一页和上一页的事件处理程序停止工作:

 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

我想说这是因为我的 useEffect 钩子被 useSWR 钩子以某种方式破坏了,但我无法证明这一点。

我正在尝试执行以下操作:

https://swr.vercel.app/docs/pagination

这是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

这是患者 Table 组件:

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

我还应该提到全局状态进入 appReducer 像这样:

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, {}],
})

由于你的全局缓存应该考虑当前页面来管理你的数据,这个数据应该出现在键中,然后你必须像useSWRWithToken(`/patients/?page${currentPage}`)一样通过你的钩子发送它然后每次当前页面更改 SWR 将触发新的提取。 我建议不要使用字符串作为键,而是使用包含所有数据的数组,例如


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
      )
  }