Files
payloadcms/packages/ui/src/providers/ListQuery/index.tsx
Jacob Fletcher 355bd12c61 chore: infer React context providers and prefer use (#11669)
As of [React 19](https://react.dev/blog/2024/12/05/react-19), context
providers no longer require the `<MyContext.Provider>` syntax and can be
rendered as `<MyContext>` directly. This will be deprecated in future
versions of React, which is now being caught by the
[`@eslint-react/no-context-provider`](https://eslint-react.xyz/docs/rules/no-context-provider)
ESLint rule.

Similarly, the [`use`](https://react.dev/reference/react/use) API is now
preferred over `useContext` because it is more flexible, for example
they can be called within loops and conditional statements. See the
[`@eslint-react/no-use-context`](https://eslint-react.xyz/docs/rules/no-use-context)
ESLint rule for more details.
2025-03-12 15:48:20 -04:00

197 lines
5.5 KiB
TypeScript

'use client'
import { useRouter, useSearchParams } from 'next/navigation.js'
import { type ListQuery, type Where } from 'payload'
import { isNumber, transformColumnsToSearchParams } from 'payload/shared'
import * as qs from 'qs-esm'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import type { ListQueryProps } from './types.js'
import { useListDrawerContext } from '../../elements/ListDrawer/Provider.js'
import { useEffectEvent } from '../../hooks/useEffectEvent.js'
import { useRouteTransition } from '../../providers/RouteTransition/index.js'
import { parseSearchParams } from '../../utilities/parseSearchParams.js'
import { ListQueryContext } from './context.js'
export { useListQuery } from './context.js'
export const ListQueryProvider: React.FC<ListQueryProps> = ({
children,
columns,
data,
defaultLimit,
defaultSort,
listPreferences,
modifySearchParams,
onQueryChange: onQueryChangeFromProps,
}) => {
'use no memo'
const router = useRouter()
const rawSearchParams = useSearchParams()
const { startRouteTransition } = useRouteTransition()
const searchParams = useMemo<ListQuery>(
() => parseSearchParams(rawSearchParams),
[rawSearchParams],
)
const { onQueryChange } = useListDrawerContext()
const [currentQuery, setCurrentQuery] = useState<ListQuery>(() => {
if (modifySearchParams) {
return searchParams
} else {
return {}
}
})
const refineListData = useCallback(
// eslint-disable-next-line @typescript-eslint/require-await
async (query: ListQuery) => {
let page = 'page' in query ? query.page : currentQuery?.page
if ('where' in query || 'search' in query) {
page = '1'
}
const newQuery: ListQuery = {
columns: 'columns' in query ? query.columns : currentQuery.columns,
limit: 'limit' in query ? query.limit : (currentQuery?.limit ?? String(defaultLimit)),
page,
search: 'search' in query ? query.search : currentQuery?.search,
sort: 'sort' in query ? query.sort : ((currentQuery?.sort as string) ?? defaultSort),
where: 'where' in query ? query.where : currentQuery?.where,
}
if (modifySearchParams) {
startRouteTransition(() =>
router.replace(
`${qs.stringify(
{ ...newQuery, columns: JSON.stringify(newQuery.columns) },
{ addQueryPrefix: true },
)}`,
),
)
} else if (
typeof onQueryChange === 'function' ||
typeof onQueryChangeFromProps === 'function'
) {
const onChangeFn = onQueryChange || onQueryChangeFromProps
onChangeFn(newQuery)
}
setCurrentQuery(newQuery)
},
[
currentQuery?.columns,
currentQuery?.limit,
currentQuery?.page,
currentQuery?.search,
currentQuery?.sort,
currentQuery?.where,
startRouteTransition,
defaultLimit,
defaultSort,
modifySearchParams,
onQueryChange,
onQueryChangeFromProps,
router,
],
)
const handlePageChange = useCallback(
async (arg: number) => {
await refineListData({ page: String(arg) })
},
[refineListData],
)
const handlePerPageChange = React.useCallback(
async (arg: number) => {
await refineListData({ limit: String(arg), page: '1' })
},
[refineListData],
)
const handleSearchChange = useCallback(
async (arg: string) => {
const search = arg === '' ? undefined : arg
await refineListData({ search })
},
[refineListData],
)
const handleSortChange = useCallback(
async (arg: string) => {
await refineListData({ sort: arg })
},
[refineListData],
)
const handleWhereChange = useCallback(
async (arg: Where) => {
await refineListData({ where: arg })
},
[refineListData],
)
const syncQuery = useEffectEvent(() => {
let shouldUpdateQueryString = false
const newQuery = { ...(currentQuery || {}) }
// Allow the URL to override the default limit
if (isNumber(defaultLimit) && !('limit' in currentQuery)) {
newQuery.limit = String(defaultLimit)
shouldUpdateQueryString = true
}
// Allow the URL to override the default sort
if (defaultSort && !('sort' in currentQuery)) {
newQuery.sort = defaultSort
shouldUpdateQueryString = true
}
// Only modify columns if they originated from preferences
// We can assume they did if `listPreferences.columns` is defined
if (columns && listPreferences?.columns && !('columns' in currentQuery)) {
newQuery.columns = transformColumnsToSearchParams(columns)
shouldUpdateQueryString = true
}
if (shouldUpdateQueryString) {
setCurrentQuery(newQuery)
// Do not use router.replace here to avoid re-rendering on initial load
window.history.replaceState(
null,
'',
`?${qs.stringify({ ...newQuery, columns: JSON.stringify(newQuery.columns) })}`,
)
}
})
// If `defaultLimit` or `defaultSort` are updated externally, update the query
// I.e. when HMR runs, these properties may be different
useEffect(() => {
if (modifySearchParams) {
syncQuery()
}
}, [defaultSort, defaultLimit, modifySearchParams, columns])
return (
<ListQueryContext
value={{
data,
handlePageChange,
handlePerPageChange,
handleSearchChange,
handleSortChange,
handleWhereChange,
query: currentQuery,
refineListData,
}}
>
{children}
</ListQueryContext>
)
}