refactor: deprecates params and search params contexts (#9581)

As described in #9576, the `SearchParamsProvider` can become stale when
navigating routes and relying on search params during initial render.
This is because this context, along with the `ParamsProvider`, is
duplicative to the internal lifecycle of `useSearchParams` and
`useParams` from `next/navigation`– but always one render behind.
Instead, we need to use the hooks directly from `next/navigation` as
described in the jsdocs. This will also remove any abstraction over top
the web standard for `URLSearchParams`.

For this reason, these providers and their corresponding hooks have been
marked with the deprecated flag and will continue to behave as they do
now, but will be removed in the next major release. This PR replaces all
internal reliance on these hooks with `next/navigation` as suggested,
except for the `useParams` hook, which was never used in the first
place.

```diff
'use client'
- import { useSearchParams } from '@payloadcms/ui'
+ import { useSearchParams } from 'next/navigation'
+ import { parseSearchParams } from '@payloadcms/ui'

export function MyClientComponent() {
- const { searchParams } = useSearchParams()
+ const searchParams = useSearchParams()
+ const parsedParams = parseSearchParams(searchParams)

  // ...
}
```
_MyClientComponent.tsx_
This commit is contained in:
Jacob Fletcher
2024-12-02 13:57:59 -05:00
committed by GitHub
parent d04cea123c
commit edc04aec56
13 changed files with 142 additions and 89 deletions

View File

@@ -1,10 +1,9 @@
'use client' 'use client'
import type { SanitizedConfig } from 'payload' import type { SanitizedConfig } from 'payload'
import { useSearchParams } from '@payloadcms/ui'
import { formatAdminURL } from '@payloadcms/ui/shared' import { formatAdminURL } from '@payloadcms/ui/shared'
import LinkImport from 'next/link.js' import LinkImport from 'next/link.js'
import { useParams, usePathname } from 'next/navigation.js' import { useParams, usePathname, useSearchParams } from 'next/navigation.js'
import React from 'react' import React from 'react'
const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default
@@ -30,12 +29,9 @@ export const DocumentTabLink: React.FC<{
const pathname = usePathname() const pathname = usePathname()
const params = useParams() const params = useParams()
const { searchParams } = useSearchParams() const searchParams = useSearchParams()
const locale = const locale = searchParams.get('locale')
'locale' in searchParams && typeof searchParams.locale === 'string'
? searchParams.locale
: undefined
const [entityType, entitySlug, segmentThree, segmentFour, ...rest] = params.segments || [] const [entityType, entitySlug, segmentThree, segmentFour, ...rest] = params.segments || []
const isCollection = entityType === 'collections' const isCollection = entityType === 'collections'

View File

@@ -3,14 +3,14 @@ import type { ClientCollectionConfig } from 'payload'
import { Modal, useModal } from '@faceless-ui/modal' import { Modal, useModal } from '@faceless-ui/modal'
import { getTranslation } from '@payloadcms/translations' import { getTranslation } from '@payloadcms/translations'
import { useRouter } from 'next/navigation.js' import { useRouter, useSearchParams } from 'next/navigation.js'
import * as qs from 'qs-esm'
import React, { useCallback, useState } from 'react' import React, { useCallback, useState } from 'react'
import { toast } from 'sonner' import { toast } from 'sonner'
import { useAuth } from '../../providers/Auth/index.js' import { useAuth } from '../../providers/Auth/index.js'
import { useConfig } from '../../providers/Config/index.js' import { useConfig } from '../../providers/Config/index.js'
import { useRouteCache } from '../../providers/RouteCache/index.js' import { useRouteCache } from '../../providers/RouteCache/index.js'
import { useSearchParams } from '../../providers/SearchParams/index.js'
import { SelectAllStatus, useSelection } from '../../providers/Selection/index.js' import { SelectAllStatus, useSelection } from '../../providers/Selection/index.js'
import { useTranslation } from '../../providers/Translation/index.js' import { useTranslation } from '../../providers/Translation/index.js'
import { requests } from '../../utilities/api.js' import { requests } from '../../utilities/api.js'
@@ -41,7 +41,7 @@ export const DeleteMany: React.FC<Props> = (props) => {
const { i18n, t } = useTranslation() const { i18n, t } = useTranslation()
const [deleting, setDeleting] = useState(false) const [deleting, setDeleting] = useState(false)
const router = useRouter() const router = useRouter()
const { searchParams, stringifyParams } = useSearchParams() const searchParams = useSearchParams()
const { clearRouteCache } = useRouteCache() const { clearRouteCache } = useRouteCache()
const collectionPermissions = permissions?.collections?.[slug] const collectionPermissions = permissions?.collections?.[slug]
@@ -58,7 +58,7 @@ export const DeleteMany: React.FC<Props> = (props) => {
const queryWithSearch = mergeListSearchAndWhere({ const queryWithSearch = mergeListSearchAndWhere({
collectionConfig: collection, collectionConfig: collection,
search: searchParams?.search as string, search: searchParams.get('search'),
}) })
const queryString = getQueryParams(queryWithSearch) const queryString = getQueryParams(queryWithSearch)
@@ -85,21 +85,26 @@ export const DeleteMany: React.FC<Props> = (props) => {
label: getTranslation(successLabel, i18n), label: getTranslation(successLabel, i18n),
}), }),
) )
if (json?.errors.length > 0) { if (json?.errors.length > 0) {
toast.error(json.message, { toast.error(json.message, {
description: json.errors.map((error) => error.message).join('\n'), description: json.errors.map((error) => error.message).join('\n'),
}) })
} }
toggleAll() toggleAll()
router.replace( router.replace(
stringifyParams({ qs.stringify(
params: { {
page: selectAll ? '1' : undefined, page: selectAll ? '1' : undefined,
}, },
replace: true, { addQueryPrefix: true },
}), ),
) )
clearRouteCache() clearRouteCache()
return null return null
} }
@@ -111,7 +116,7 @@ export const DeleteMany: React.FC<Props> = (props) => {
addDefaultError() addDefaultError()
} }
return false return false
} catch (e) { } catch (_err) {
return addDefaultError() return addDefaultError()
} }
}) })
@@ -128,7 +133,6 @@ export const DeleteMany: React.FC<Props> = (props) => {
serverURL, serverURL,
singular, singular,
slug, slug,
stringifyParams,
t, t,
toggleAll, toggleAll,
toggleModal, toggleModal,

View File

@@ -3,7 +3,8 @@ import type { ClientCollectionConfig, FormState } from 'payload'
import { useModal } from '@faceless-ui/modal' import { useModal } from '@faceless-ui/modal'
import { getTranslation } from '@payloadcms/translations' import { getTranslation } from '@payloadcms/translations'
import { useRouter } from 'next/navigation.js' import { useRouter, useSearchParams } from 'next/navigation.js'
import * as qs from 'qs-esm'
import React, { useCallback, useEffect, useMemo, useState } from 'react' import React, { useCallback, useEffect, useMemo, useState } from 'react'
import type { FormProps } from '../../forms/Form/index.js' import type { FormProps } from '../../forms/Form/index.js'
@@ -19,14 +20,14 @@ import { DocumentInfoProvider } from '../../providers/DocumentInfo/index.js'
import { EditDepthProvider } from '../../providers/EditDepth/index.js' import { EditDepthProvider } from '../../providers/EditDepth/index.js'
import { OperationContext } from '../../providers/Operation/index.js' import { OperationContext } from '../../providers/Operation/index.js'
import { useRouteCache } from '../../providers/RouteCache/index.js' import { useRouteCache } from '../../providers/RouteCache/index.js'
import { useSearchParams } from '../../providers/SearchParams/index.js'
import { SelectAllStatus, useSelection } from '../../providers/Selection/index.js' import { SelectAllStatus, useSelection } from '../../providers/Selection/index.js'
import { useServerFunctions } from '../../providers/ServerFunctions/index.js' import { useServerFunctions } from '../../providers/ServerFunctions/index.js'
import { useTranslation } from '../../providers/Translation/index.js' import { useTranslation } from '../../providers/Translation/index.js'
import { abortAndIgnore } from '../../utilities/abortAndIgnore.js' import { abortAndIgnore } from '../../utilities/abortAndIgnore.js'
import { mergeListSearchAndWhere } from '../../utilities/mergeListSearchAndWhere.js' import { mergeListSearchAndWhere } from '../../utilities/mergeListSearchAndWhere.js'
import { Drawer, DrawerToggler } from '../Drawer/index.js' import { parseSearchParams } from '../../utilities/parseSearchParams.js'
import './index.scss' import './index.scss'
import { Drawer, DrawerToggler } from '../Drawer/index.js'
import { FieldSelect } from '../FieldSelect/index.js' import { FieldSelect } from '../FieldSelect/index.js'
const baseClass = 'edit-many' const baseClass = 'edit-many'
@@ -125,7 +126,7 @@ export const EditMany: React.FC<EditManyProps> = (props) => {
const { count, getQueryParams, selectAll } = useSelection() const { count, getQueryParams, selectAll } = useSelection()
const { i18n, t } = useTranslation() const { i18n, t } = useTranslation()
const [selected, setSelected] = useState([]) const [selected, setSelected] = useState([])
const { searchParams, stringifyParams } = useSearchParams() const searchParams = useSearchParams()
const router = useRouter() const router = useRouter()
const [initialState, setInitialState] = useState<FormState>() const [initialState, setInitialState] = useState<FormState>()
const hasInitializedState = React.useRef(false) const hasInitializedState = React.useRef(false)
@@ -195,7 +196,7 @@ export const EditMany: React.FC<EditManyProps> = (props) => {
const queryString = useMemo(() => { const queryString = useMemo(() => {
const queryWithSearch = mergeListSearchAndWhere({ const queryWithSearch = mergeListSearchAndWhere({
collectionConfig: collection, collectionConfig: collection,
search: searchParams?.search as string, search: searchParams.get('search'),
}) })
return getQueryParams(queryWithSearch) return getQueryParams(queryWithSearch)
@@ -203,9 +204,13 @@ export const EditMany: React.FC<EditManyProps> = (props) => {
const onSuccess = () => { const onSuccess = () => {
router.replace( router.replace(
stringifyParams({ qs.stringify(
params: { page: selectAll === SelectAllStatus.AllAvailable ? '1' : undefined }, {
}), ...parseSearchParams(searchParams),
page: selectAll === SelectAllStatus.AllAvailable ? '1' : undefined,
},
{ addQueryPrefix: true },
),
) )
clearRouteCache() // Use clearRouteCache instead of router.refresh, as we only need to clear the cache if the user has route caching enabled - clearRouteCache checks for this clearRouteCache() // Use clearRouteCache instead of router.refresh, as we only need to clear the cache if the user has route caching enabled - clearRouteCache checks for this
closeModal(drawerSlug) closeModal(drawerSlug)

View File

@@ -1,14 +1,16 @@
'use client' 'use client'
import { getTranslation } from '@payloadcms/translations' import { getTranslation } from '@payloadcms/translations'
import { useSearchParams } from 'next/navigation.js'
import * as qs from 'qs-esm'
import React from 'react' import React from 'react'
import { useConfig } from '../../providers/Config/index.js' import { useConfig } from '../../providers/Config/index.js'
import { useLocale } from '../../providers/Locale/index.js' import { useLocale } from '../../providers/Locale/index.js'
import { useSearchParams } from '../../providers/SearchParams/index.js'
import { useTranslation } from '../../providers/Translation/index.js' import { useTranslation } from '../../providers/Translation/index.js'
import { parseSearchParams } from '../../utilities/parseSearchParams.js'
import { Popup, PopupList } from '../Popup/index.js' import { Popup, PopupList } from '../Popup/index.js'
import './index.scss'
import { LocalizerLabel } from './LocalizerLabel/index.js' import { LocalizerLabel } from './LocalizerLabel/index.js'
import './index.scss'
const baseClass = 'localizer' const baseClass = 'localizer'
@@ -18,10 +20,10 @@ export const Localizer: React.FC<{
const { className } = props const { className } = props
const { config } = useConfig() const { config } = useConfig()
const { localization } = config const { localization } = config
const searchParams = useSearchParams()
const { i18n } = useTranslation() const { i18n } = useTranslation()
const locale = useLocale() const locale = useLocale()
const { stringifyParams } = useSearchParams()
if (localization) { if (localization) {
const { locales } = localization const { locales } = localization
@@ -39,11 +41,13 @@ export const Localizer: React.FC<{
return ( return (
<PopupList.Button <PopupList.Button
active={locale.code === localeOption.code} active={locale.code === localeOption.code}
href={stringifyParams({ href={qs.stringify(
params: { {
...parseSearchParams(searchParams),
locale: localeOption.code, locale: localeOption.code,
}, },
})} { addQueryPrefix: true },
)}
key={localeOption.code} key={localeOption.code}
onClick={close} onClick={close}
> >

View File

@@ -3,17 +3,18 @@ import type { ClientCollectionConfig } from 'payload'
import { Modal, useModal } from '@faceless-ui/modal' import { Modal, useModal } from '@faceless-ui/modal'
import { getTranslation } from '@payloadcms/translations' import { getTranslation } from '@payloadcms/translations'
import { useRouter } from 'next/navigation.js' import { useRouter, useSearchParams } from 'next/navigation.js'
import * as qs from 'qs-esm'
import React, { useCallback, useState } from 'react' import React, { useCallback, useState } from 'react'
import { toast } from 'sonner' import { toast } from 'sonner'
import { useAuth } from '../../providers/Auth/index.js' import { useAuth } from '../../providers/Auth/index.js'
import { useConfig } from '../../providers/Config/index.js' import { useConfig } from '../../providers/Config/index.js'
import { useRouteCache } from '../../providers/RouteCache/index.js' import { useRouteCache } from '../../providers/RouteCache/index.js'
import { useSearchParams } from '../../providers/SearchParams/index.js'
import { SelectAllStatus, useSelection } from '../../providers/Selection/index.js' import { SelectAllStatus, useSelection } from '../../providers/Selection/index.js'
import { useTranslation } from '../../providers/Translation/index.js' import { useTranslation } from '../../providers/Translation/index.js'
import { requests } from '../../utilities/api.js' import { requests } from '../../utilities/api.js'
import { parseSearchParams } from '../../utilities/parseSearchParams.js'
import { Button } from '../Button/index.js' import { Button } from '../Button/index.js'
import { Pill } from '../Pill/index.js' import { Pill } from '../Pill/index.js'
import './index.scss' import './index.scss'
@@ -41,7 +42,7 @@ export const PublishMany: React.FC<PublishManyProps> = (props) => {
const { getQueryParams, selectAll } = useSelection() const { getQueryParams, selectAll } = useSelection()
const [submitted, setSubmitted] = useState(false) const [submitted, setSubmitted] = useState(false)
const router = useRouter() const router = useRouter()
const { stringifyParams } = useSearchParams() const searchParams = useSearchParams()
const collectionPermissions = permissions?.collections?.[slug] const collectionPermissions = permissions?.collections?.[slug]
const hasPermission = collectionPermissions?.update const hasPermission = collectionPermissions?.update
@@ -82,17 +83,21 @@ export const PublishMany: React.FC<PublishManyProps> = (props) => {
label: getTranslation(successLabel, i18n), label: getTranslation(successLabel, i18n),
}), }),
) )
if (json?.errors.length > 0) { if (json?.errors.length > 0) {
toast.error(json.message, { toast.error(json.message, {
description: json.errors.map((error) => error.message).join('\n'), description: json.errors.map((error) => error.message).join('\n'),
}) })
} }
router.replace( router.replace(
stringifyParams({ qs.stringify(
params: { {
...parseSearchParams(searchParams),
page: selectAll ? '1' : undefined, page: selectAll ? '1' : undefined,
}, },
}), { addQueryPrefix: true },
),
) )
clearRouteCache() // Use clearRouteCache instead of router.refresh, as we only need to clear the cache if the user has route caching enabled - clearRouteCache checks for this clearRouteCache() // Use clearRouteCache instead of router.refresh, as we only need to clear the cache if the user has route caching enabled - clearRouteCache checks for this
@@ -110,21 +115,21 @@ export const PublishMany: React.FC<PublishManyProps> = (props) => {
} }
}) })
}, [ }, [
addDefaultError, serverURL,
api, api,
slug,
getQueryParams, getQueryParams,
i18n, i18n,
toggleModal,
modalSlug, modalSlug,
plural, plural,
selectAll,
serverURL,
singular, singular,
slug,
t, t,
toggleModal,
router, router,
stringifyParams, searchParams,
selectAll,
clearRouteCache, clearRouteCache,
addDefaultError,
]) ])
if (!versions?.drafts || selectAll === SelectAllStatus.None || !hasPermission) { if (!versions?.drafts || selectAll === SelectAllStatus.None || !hasPermission) {

View File

@@ -3,7 +3,7 @@ import type { OptionObject, SanitizedCollectionConfig } from 'payload'
import { getTranslation } from '@payloadcms/translations' import { getTranslation } from '@payloadcms/translations'
// TODO: abstract the `next/navigation` dependency out from this component // TODO: abstract the `next/navigation` dependency out from this component
import { usePathname, useRouter } from 'next/navigation.js' import { usePathname, useRouter, useSearchParams } from 'next/navigation.js'
import { sortableFieldTypes } from 'payload' import { sortableFieldTypes } from 'payload'
import { fieldAffectsData } from 'payload/shared' import { fieldAffectsData } from 'payload/shared'
import * as qs from 'qs-esm' import * as qs from 'qs-esm'
@@ -18,7 +18,6 @@ export type SortComplexProps = {
import type { Option } from '../ReactSelect/index.js' import type { Option } from '../ReactSelect/index.js'
import { useSearchParams } from '../../providers/SearchParams/index.js'
import { useTranslation } from '../../providers/Translation/index.js' import { useTranslation } from '../../providers/Translation/index.js'
import { ReactSelect } from '../ReactSelect/index.js' import { ReactSelect } from '../ReactSelect/index.js'
import './index.scss' import './index.scss'
@@ -30,7 +29,7 @@ export const SortComplex: React.FC<SortComplexProps> = (props) => {
const router = useRouter() const router = useRouter()
const pathname = usePathname() const pathname = usePathname()
const { searchParams } = useSearchParams() const searchParams = useSearchParams()
const { i18n, t } = useTranslation() const { i18n, t } = useTranslation()
const [sortOptions, setSortOptions] = useState<OptionObject[]>() const [sortOptions, setSortOptions] = useState<OptionObject[]>()
@@ -58,7 +57,7 @@ export const SortComplex: React.FC<SortComplexProps> = (props) => {
handleChange(newSortValue) handleChange(newSortValue)
} }
if (searchParams.sort !== newSortValue && modifySearchQuery) { if (searchParams.get('sort') !== newSortValue && modifySearchQuery) {
const search = qs.stringify( const search = qs.stringify(
{ {
...searchParams, ...searchParams,

View File

@@ -1,13 +1,13 @@
'use client' 'use client'
import { Modal, useModal } from '@faceless-ui/modal' import { Modal, useModal } from '@faceless-ui/modal'
import { getTranslation } from '@payloadcms/translations' import { getTranslation } from '@payloadcms/translations'
import { useRouter } from 'next/navigation.js' import { useRouter, useSearchParams } from 'next/navigation.js'
import * as qs from 'qs-esm'
import React, { useCallback, useState } from 'react' import React, { useCallback, useState } from 'react'
import { useAuth } from '../../providers/Auth/index.js' import { useAuth } from '../../providers/Auth/index.js'
import { useConfig } from '../../providers/Config/index.js' import { useConfig } from '../../providers/Config/index.js'
import { useRouteCache } from '../../providers/RouteCache/index.js' import { useRouteCache } from '../../providers/RouteCache/index.js'
import { useSearchParams } from '../../providers/SearchParams/index.js'
import { SelectAllStatus, useSelection } from '../../providers/Selection/index.js' import { SelectAllStatus, useSelection } from '../../providers/Selection/index.js'
import { useTranslation } from '../../providers/Translation/index.js' import { useTranslation } from '../../providers/Translation/index.js'
import { requests } from '../../utilities/api.js' import { requests } from '../../utilities/api.js'
@@ -21,6 +21,8 @@ import type { ClientCollectionConfig } from 'payload'
import { toast } from 'sonner' import { toast } from 'sonner'
import { parseSearchParams } from '../../utilities/parseSearchParams.js'
export type UnpublishManyProps = { export type UnpublishManyProps = {
collection: ClientCollectionConfig collection: ClientCollectionConfig
} }
@@ -38,9 +40,9 @@ export const UnpublishMany: React.FC<UnpublishManyProps> = (props) => {
const { permissions } = useAuth() const { permissions } = useAuth()
const { toggleModal } = useModal() const { toggleModal } = useModal()
const { i18n, t } = useTranslation() const { i18n, t } = useTranslation()
const searchParams = useSearchParams()
const { getQueryParams, selectAll } = useSelection() const { getQueryParams, selectAll } = useSelection()
const [submitted, setSubmitted] = useState(false) const [submitted, setSubmitted] = useState(false)
const { stringifyParams } = useSearchParams()
const router = useRouter() const router = useRouter()
const { clearRouteCache } = useRouteCache() const { clearRouteCache } = useRouteCache()
@@ -86,11 +88,13 @@ export const UnpublishMany: React.FC<UnpublishManyProps> = (props) => {
}) })
} }
router.replace( router.replace(
stringifyParams({ qs.stringify(
params: { {
...parseSearchParams(searchParams),
page: selectAll ? '1' : undefined, page: selectAll ? '1' : undefined,
}, },
}), { addQueryPrefix: true },
),
) )
clearRouteCache() // Use clearRouteCache instead of router.refresh, as we only need to clear the cache if the user has route caching enabled - clearRouteCache checks for this clearRouteCache() // Use clearRouteCache instead of router.refresh, as we only need to clear the cache if the user has route caching enabled - clearRouteCache checks for this
return null return null
@@ -102,26 +106,26 @@ export const UnpublishMany: React.FC<UnpublishManyProps> = (props) => {
addDefaultError() addDefaultError()
} }
return false return false
} catch (e) { } catch (_err) {
return addDefaultError() return addDefaultError()
} }
}) })
}, [ }, [
addDefaultError, serverURL,
api, api,
slug,
getQueryParams, getQueryParams,
i18n, i18n,
toggleModal,
modalSlug, modalSlug,
plural, plural,
selectAll,
serverURL,
singular, singular,
slug,
t, t,
toggleModal,
router, router,
searchParams,
selectAll,
clearRouteCache, clearRouteCache,
stringifyParams, addDefaultError,
]) ])
if (!versions?.drafts || selectAll === SelectAllStatus.None || !hasPermission) { if (!versions?.drafts || selectAll === SelectAllStatus.None || !hasPermission) {

View File

@@ -9,8 +9,8 @@ import React, { createContext, useCallback, useContext, useEffect, useMemo, useS
import type { Column } from '../../elements/Table/index.js' import type { Column } from '../../elements/Table/index.js'
import { useListDrawerContext } from '../../elements/ListDrawer/Provider.js' import { useListDrawerContext } from '../../elements/ListDrawer/Provider.js'
import { parseSearchParams } from '../../utilities/parseSearchParams.js'
import { usePreferences } from '../Preferences/index.js' import { usePreferences } from '../Preferences/index.js'
import { createParams } from '../SearchParams/index.js'
export type ColumnPreferences = Pick<Column, 'accessor' | 'active'>[] export type ColumnPreferences = Pick<Column, 'accessor' | 'active'>[]
@@ -59,7 +59,7 @@ export const ListQueryProvider: React.FC<ListQueryProps> = ({
const router = useRouter() const router = useRouter()
const { setPreference } = usePreferences() const { setPreference } = usePreferences()
const rawSearchParams = useSearchParams() const rawSearchParams = useSearchParams()
const searchParams = useMemo(() => createParams(rawSearchParams), [rawSearchParams]) const searchParams = useMemo(() => parseSearchParams(rawSearchParams), [rawSearchParams])
const { onQueryChange } = useListDrawerContext() const { onQueryChange } = useListDrawerContext()

View File

@@ -2,13 +2,13 @@
import type { Locale } from 'payload' import type { Locale } from 'payload'
import { useSearchParams } from 'next/navigation.js'
import React, { createContext, useContext, useEffect, useState } from 'react' import React, { createContext, useContext, useEffect, useState } from 'react'
import { findLocaleFromCode } from '../../utilities/findLocaleFromCode.js' import { findLocaleFromCode } from '../../utilities/findLocaleFromCode.js'
import { useAuth } from '../Auth/index.js' import { useAuth } from '../Auth/index.js'
import { useConfig } from '../Config/index.js' import { useConfig } from '../Config/index.js'
import { usePreferences } from '../Preferences/index.js' import { usePreferences } from '../Preferences/index.js'
import { useSearchParams } from '../SearchParams/index.js'
const LocaleContext = createContext({} as Locale) const LocaleContext = createContext({} as Locale)
@@ -21,12 +21,10 @@ export const LocaleProvider: React.FC<{ children?: React.ReactNode }> = ({ child
const defaultLocale = const defaultLocale =
localization && localization.defaultLocale ? localization.defaultLocale : 'en' localization && localization.defaultLocale ? localization.defaultLocale : 'en'
const { searchParams } = useSearchParams() const searchParams = useSearchParams()
const localeFromParams = searchParams?.locale const localeFromParams = searchParams.get('locale')
const [localeCode, setLocaleCode] = useState<string>( const [localeCode, setLocaleCode] = useState<string>(localeFromParams || defaultLocale)
(localeFromParams as string) || defaultLocale,
)
const [locale, setLocale] = useState<Locale | null>( const [locale, setLocale] = useState<Locale | null>(
localization && findLocaleFromCode(localization, localeCode), localization && findLocaleFromCode(localization, localeCode),

View File

@@ -8,10 +8,25 @@ interface IParamsContext extends Params {}
const Context = createContext<IParamsContext>({} as IParamsContext) const Context = createContext<IParamsContext>({} as IParamsContext)
// TODO: abstract the `next/navigation` dependency out from this provider so that it can be used in other contexts /**
* @deprecated
* The ParamsProvider is deprecated and will be removed in the next major release. Instead, use the `useParams` hook from `next/navigation` directly. See https://github.com/payloadcms/payload/pull/9581.
* @example
* ```tsx
* import { useParams } from 'next/navigation'
* ```
*/
export const ParamsProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) => { export const ParamsProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
const params = useNextParams() const params = useNextParams()
return <Context.Provider value={params}>{children}</Context.Provider> return <Context.Provider value={params}>{children}</Context.Provider>
} }
/**
* @deprecated
* The `useParams` hook is deprecated and will be removed in the next major release. Instead, use the `useParams` hook from `next/navigation` directly. See https://github.com/payloadcms/payload/pull/9581.
* @example
* ```tsx
* import { useParams } from 'next/navigation'
* ```
*/
export const useParams = (): IParamsContext => useContext(Context) export const useParams = (): IParamsContext => useContext(Context)

View File

@@ -1,17 +1,16 @@
'use client' 'use client'
import type { ReadonlyURLSearchParams } from 'next/navigation.js'
import { useSearchParams as useNextSearchParams } from 'next/navigation.js' import { useSearchParams as useNextSearchParams } from 'next/navigation.js'
import * as qs from 'qs-esm' import * as qs from 'qs-esm'
import React, { createContext, useContext } from 'react' import React, { createContext, useContext } from 'react'
import { parseSearchParams } from '../../utilities/parseSearchParams.js'
export type SearchParamsContext = { export type SearchParamsContext = {
searchParams: qs.ParsedQs searchParams: qs.ParsedQs
stringifyParams: ({ params, replace }: { params: State; replace?: boolean }) => string stringifyParams: ({ params, replace }: { params: qs.ParsedQs; replace?: boolean }) => string
} }
export type State = qs.ParsedQs
const initialContext: SearchParamsContext = { const initialContext: SearchParamsContext = {
searchParams: {}, searchParams: {},
stringifyParams: () => '', stringifyParams: () => '',
@@ -19,23 +18,21 @@ const initialContext: SearchParamsContext = {
const Context = createContext(initialContext) const Context = createContext(initialContext)
export function createParams(params: ReadonlyURLSearchParams): State { /**
const search = params.toString() * @deprecated
* The SearchParamsProvider is deprecated and will be removed in the next major release. Instead, use the `useSearchParams` hook from `next/navigation` directly. See https://github.com/payloadcms/payload/pull/9581.
return qs.parse(search, { * @example
depth: 10, * ```tsx
ignoreQueryPrefix: true, * import { useSearchParams } from 'next/navigation'
}) * ```
} */
// TODO: this provider should likely be marked as deprecated and then removed in the next major release
export const SearchParamsProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) => { export const SearchParamsProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
const nextSearchParams = useNextSearchParams() const nextSearchParams = useNextSearchParams()
const [searchParams, setSearchParams] = React.useState(() => createParams(nextSearchParams)) const [searchParams, setSearchParams] = React.useState(() => parseSearchParams(nextSearchParams))
const stringifyParams = React.useCallback( const stringifyParams = React.useCallback(
({ params, replace = false }: { params: State; replace?: boolean }) => { ({ params, replace = false }: { params: qs.ParsedQs; replace?: boolean }) => {
return qs.stringify( return qs.stringify(
{ {
...(replace ? {} : searchParams), ...(replace ? {} : searchParams),
@@ -48,10 +45,23 @@ export const SearchParamsProvider: React.FC<{ children?: React.ReactNode }> = ({
) )
React.useEffect(() => { React.useEffect(() => {
setSearchParams(createParams(nextSearchParams)) setSearchParams(parseSearchParams(nextSearchParams))
}, [nextSearchParams]) }, [nextSearchParams])
return <Context.Provider value={{ searchParams, stringifyParams }}>{children}</Context.Provider> return <Context.Provider value={{ searchParams, stringifyParams }}>{children}</Context.Provider>
} }
/**
* @deprecated
* The `useSearchParams` hook is deprecated and will be removed in the next major release. Instead, use the `useSearchParams` hook from `next/navigation` directly. See https://github.com/payloadcms/payload/pull/9581.
* @example
* ```tsx
* import { useSearchParams } from 'next/navigation'
* ```
* If you need to parse the `where` query, you can do so with the `parseSearchParams` utility.
* ```tsx
* import { parseSearchParams } from '@payloadcms/ui'
* const parsedSearchParams = parseSearchParams(searchParams)
* ```
*/
export const useSearchParams = (): SearchParamsContext => useContext(Context) export const useSearchParams = (): SearchParamsContext => useContext(Context)

View File

@@ -1,11 +1,12 @@
'use client' 'use client'
import type { ClientUser, Where } from 'payload' import type { ClientUser, Where } from 'payload'
import { useSearchParams } from 'next/navigation.js'
import * as qs from 'qs-esm' import * as qs from 'qs-esm'
import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react' import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react'
import { parseSearchParams } from '../../utilities/parseSearchParams.js'
import { useLocale } from '../Locale/index.js' import { useLocale } from '../Locale/index.js'
import { useSearchParams } from '../SearchParams/index.js'
export enum SelectAllStatus { export enum SelectAllStatus {
AllAvailable = 'allAvailable', AllAvailable = 'allAvailable',
@@ -51,7 +52,7 @@ export const SelectionProvider: React.FC<Props> = ({ children, docs = [], totalD
const [selectAll, setSelectAll] = useState<SelectAllStatus>(SelectAllStatus.None) const [selectAll, setSelectAll] = useState<SelectAllStatus>(SelectAllStatus.None)
const [count, setCount] = useState(0) const [count, setCount] = useState(0)
const { searchParams } = useSearchParams() const searchParams = useSearchParams()
const toggleAll = useCallback( const toggleAll = useCallback(
(allAvailable = false) => { (allAvailable = false) => {
@@ -110,7 +111,7 @@ export const SelectionProvider: React.FC<Props> = ({ children, docs = [], totalD
let where: Where let where: Where
if (selectAll === SelectAllStatus.AllAvailable) { if (selectAll === SelectAllStatus.AllAvailable) {
const params = searchParams?.where as Where const params = parseSearchParams(searchParams)?.where as Where
where = params || { where = params || {
id: { not_equals: '' }, id: { not_equals: '' },

View File

@@ -0,0 +1,12 @@
import type { ReadonlyURLSearchParams } from 'next/navigation.js'
import * as qs from 'qs-esm'
export function parseSearchParams(params: ReadonlyURLSearchParams): qs.ParsedQs {
const search = params.toString()
return qs.parse(search, {
depth: 10,
ignoreQueryPrefix: true,
})
}