fix: simplify searchParams provider, leverage next router properly (#5273)

This commit is contained in:
Jarrod Flesch
2024-03-08 11:59:37 -05:00
committed by GitHub
parent c17f2e2560
commit 0066b858d6
8 changed files with 121 additions and 141 deletions

View File

@@ -1,6 +1,7 @@
'use client' 'use client'
import * as facelessUIImport from '@faceless-ui/modal' import * as facelessUIImport from '@faceless-ui/modal'
import { getTranslation } from '@payloadcms/translations' import { getTranslation } from '@payloadcms/translations'
import { useRouter } from 'next/navigation.js'
import React, { useCallback, useState } from 'react' import React, { useCallback, useState } from 'react'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
@@ -33,7 +34,8 @@ export const DeleteMany: React.FC<Props> = (props) => {
const { count, getQueryParams, selectAll, toggleAll } = useSelection() const { count, getQueryParams, selectAll, toggleAll } = useSelection()
const { i18n, t } = useTranslation() const { i18n, t } = useTranslation()
const [deleting, setDeleting] = useState(false) const [deleting, setDeleting] = useState(false)
const { dispatchSearchParams } = useSearchParams() const router = useRouter()
const { stringifyParams } = useSearchParams()
const collectionPermissions = permissions?.collections?.[slug] const collectionPermissions = permissions?.collections?.[slug]
const hasDeletePermission = collectionPermissions?.delete?.permission const hasDeletePermission = collectionPermissions?.delete?.permission
@@ -60,11 +62,14 @@ export const DeleteMany: React.FC<Props> = (props) => {
if (res.status < 400) { if (res.status < 400) {
toast.success(json.message || t('general:deletedSuccessfully'), { autoClose: 3000 }) toast.success(json.message || t('general:deletedSuccessfully'), { autoClose: 3000 })
toggleAll() toggleAll()
dispatchSearchParams({ router.replace(
type: 'SET', stringifyParams({
browserHistory: 'replace', params: {
params: { page: selectAll ? '1' : undefined }, page: selectAll ? '1' : undefined,
}) },
replace: true,
}),
)
return null return null
} }
@@ -81,13 +86,14 @@ export const DeleteMany: React.FC<Props> = (props) => {
}, [ }, [
addDefaultError, addDefaultError,
api, api,
dispatchSearchParams,
getQueryParams, getQueryParams,
i18n.language, i18n.language,
modalSlug, modalSlug,
router,
selectAll, selectAll,
serverURL, serverURL,
slug, slug,
stringifyParams,
t, t,
toggleAll, toggleAll,
toggleModal, toggleModal,

View File

@@ -3,6 +3,7 @@ import type { FormState } from 'payload/types'
import * as facelessUIImport from '@faceless-ui/modal' import * as facelessUIImport from '@faceless-ui/modal'
import { getTranslation } from '@payloadcms/translations' import { getTranslation } from '@payloadcms/translations'
import { useRouter } from 'next/navigation.js'
import React, { useCallback, useState } from 'react' import React, { useCallback, useState } from 'react'
import type { Props } from './types.js' import type { Props } from './types.js'
@@ -102,7 +103,8 @@ export const EditMany: React.FC<Props> = (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 { dispatchSearchParams } = useSearchParams() const { stringifyParams } = useSearchParams()
const router = useRouter()
const { componentMap } = useComponentMap() const { componentMap } = useComponentMap()
const [reducedFieldMap, setReducedFieldMap] = useState([]) const [reducedFieldMap, setReducedFieldMap] = useState([])
const [initialState, setInitialState] = useState<FormState>() const [initialState, setInitialState] = useState<FormState>()
@@ -155,11 +157,11 @@ export const EditMany: React.FC<Props> = (props) => {
} }
const onSuccess = () => { const onSuccess = () => {
dispatchSearchParams({ router.replace(
type: 'SET', stringifyParams({
browserHistory: 'replace', params: { page: selectAll === SelectAllStatus.AllAvailable ? '1' : undefined },
params: { page: selectAll === SelectAllStatus.AllAvailable ? '1' : undefined }, }),
}) )
} }
return ( return (

View File

@@ -1,4 +1,5 @@
import { getTranslation } from '@payloadcms/translations' import { getTranslation } from '@payloadcms/translations'
import { useRouter } from 'next/navigation.js'
import React from 'react' import React from 'react'
import { useConfig } from '../../providers/Config/index.js' import { useConfig } from '../../providers/Config/index.js'
@@ -21,7 +22,8 @@ const Localizer: React.FC<{
const { i18n } = useTranslation() const { i18n } = useTranslation()
const locale = useLocale() const locale = useLocale()
const { dispatchSearchParams, searchParams } = useSearchParams() const { stringifyParams } = useSearchParams()
const router = useRouter()
if (localization) { if (localization) {
const { locales } = localization const { locales } = localization
@@ -34,25 +36,21 @@ const Localizer: React.FC<{
render={({ close }) => ( render={({ close }) => (
<PopupList.ButtonGroup> <PopupList.ButtonGroup>
{locales.map((localeOption) => { {locales.map((localeOption) => {
const newParams = {
...searchParams,
locale: localeOption.code,
}
const localeOptionLabel = getTranslation(localeOption.label, i18n) const localeOptionLabel = getTranslation(localeOption.label, i18n)
return ( return (
<PopupList.Button <PopupList.Button
active={locale.code === localeOption.code} active={locale.code === localeOption.code}
href={{ query: newParams }}
key={localeOption.code} key={localeOption.code}
onClick={() => { onClick={() => {
router.replace(
stringifyParams({
params: {
locale: localeOption.code,
},
}),
)
close() close()
dispatchSearchParams({
type: 'SET',
params: {
locale: searchParams.locale,
},
})
}} }}
> >
{localeOptionLabel} {localeOptionLabel}

View File

@@ -1,6 +1,7 @@
'use client' 'use client'
import * as facelessUIImport from '@faceless-ui/modal' import * as facelessUIImport from '@faceless-ui/modal'
import { getTranslation } from '@payloadcms/translations' import { getTranslation } from '@payloadcms/translations'
import { useRouter } from 'next/navigation.js'
import React, { useCallback, useState } from 'react' import React, { useCallback, useState } from 'react'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
@@ -33,7 +34,8 @@ export const PublishMany: React.FC<Props> = (props) => {
const { i18n, t } = useTranslation() const { i18n, t } = useTranslation()
const { getQueryParams, selectAll } = useSelection() const { getQueryParams, selectAll } = useSelection()
const [submitted, setSubmitted] = useState(false) const [submitted, setSubmitted] = useState(false)
const { dispatchSearchParams } = useSearchParams() const router = useRouter()
const { stringifyParams } = useSearchParams()
const collectionPermissions = permissions?.collections?.[slug] const collectionPermissions = permissions?.collections?.[slug]
const hasPermission = collectionPermissions?.update?.permission const hasPermission = collectionPermissions?.update?.permission
@@ -65,11 +67,13 @@ export const PublishMany: React.FC<Props> = (props) => {
toggleModal(modalSlug) toggleModal(modalSlug)
if (res.status < 400) { if (res.status < 400) {
toast.success(t('general:updatedSuccessfully')) toast.success(t('general:updatedSuccessfully'))
dispatchSearchParams({ router.replace(
type: 'SET', stringifyParams({
browserHistory: 'replace', params: {
params: { page: selectAll ? '1' : undefined }, page: selectAll ? '1' : undefined,
}) },
}),
)
return null return null
} }
@@ -94,7 +98,6 @@ export const PublishMany: React.FC<Props> = (props) => {
slug, slug,
t, t,
toggleModal, toggleModal,
dispatchSearchParams,
]) ])
if (!versions?.drafts || selectAll === SelectAllStatus.None || !hasPermission) { if (!versions?.drafts || selectAll === SelectAllStatus.None || !hasPermission) {

View File

@@ -1,6 +1,7 @@
'use client' 'use client'
import * as facelessUIImport from '@faceless-ui/modal' import * as facelessUIImport from '@faceless-ui/modal'
import { getTranslation } from '@payloadcms/translations' import { getTranslation } from '@payloadcms/translations'
import { useRouter } from 'next/navigation.js'
import React, { useCallback, useState } from 'react' import React, { useCallback, useState } from 'react'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
@@ -32,7 +33,8 @@ export const UnpublishMany: React.FC<Props> = (props) => {
const { i18n, t } = useTranslation() const { i18n, t } = useTranslation()
const { getQueryParams, selectAll } = useSelection() const { getQueryParams, selectAll } = useSelection()
const [submitted, setSubmitted] = useState(false) const [submitted, setSubmitted] = useState(false)
const { dispatchSearchParams } = useSearchParams() const { stringifyParams } = useSearchParams()
const router = useRouter()
const collectionPermissions = permissions?.collections?.[slug] const collectionPermissions = permissions?.collections?.[slug]
const hasPermission = collectionPermissions?.update?.permission const hasPermission = collectionPermissions?.update?.permission
@@ -61,11 +63,13 @@ export const UnpublishMany: React.FC<Props> = (props) => {
toggleModal(modalSlug) toggleModal(modalSlug)
if (res.status < 400) { if (res.status < 400) {
toast.success(t('general:updatedSuccessfully')) toast.success(t('general:updatedSuccessfully'))
dispatchSearchParams({ router.replace(
type: 'SET', stringifyParams({
browserHistory: 'replace', params: {
params: { page: selectAll ? '1' : undefined }, page: selectAll ? '1' : undefined,
}) },
}),
)
return null return null
} }
@@ -82,7 +86,6 @@ export const UnpublishMany: React.FC<Props> = (props) => {
}, [ }, [
addDefaultError, addDefaultError,
api, api,
dispatchSearchParams,
getQueryParams, getQueryParams,
i18n.language, i18n.language,
modalSlug, modalSlug,

View File

@@ -2,8 +2,6 @@
import type { Locale } from 'payload/config' import type { Locale } from 'payload/config'
// TODO: abstract the `next/navigation` dependency out from this component
import { useRouter } 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'
@@ -18,15 +16,14 @@ export const LocaleProvider: React.FC<{ children?: React.ReactNode }> = ({ child
const { localization } = useConfig() const { localization } = useConfig()
const { user } = useAuth() const { user } = useAuth()
const defaultLocale = const defaultLocale =
localization && localization.defaultLocale ? localization.defaultLocale : 'en' localization && localization.defaultLocale ? localization.defaultLocale : 'en'
const { dispatchSearchParams, searchParams } = useSearchParams() const { searchParams } = useSearchParams()
const router = useRouter() const localeFromParams = searchParams?.locale || ''
const [localeCode, setLocaleCode] = useState<string>( const [localeCode, setLocaleCode] = useState<string>(
(searchParams?.locale as string) || defaultLocale, (localeFromParams as string) || defaultLocale,
) )
const [locale, setLocale] = useState<Locale | null>( const [locale, setLocale] = useState<Locale | null>(
@@ -35,53 +32,55 @@ export const LocaleProvider: React.FC<{ children?: React.ReactNode }> = ({ child
const { getPreference, setPreference } = usePreferences() const { getPreference, setPreference } = usePreferences()
const localeFromParams = searchParams.locale const switchLocale = React.useCallback(
async (newLocale: string) => {
useEffect(() => {
async function localeChangeHandler() {
if (!localization) { if (!localization) {
return return
} }
// set locale from search param const localeToSet =
if (localeFromParams && localization.localeCodes.indexOf(localeFromParams as string) > -1) { localization.localeCodes.indexOf(newLocale) > -1 ? newLocale : defaultLocale
setLocaleCode(localeFromParams as string)
setLocale(findLocaleFromCode(localization, localeFromParams as string))
if (user) await setPreference('locale', localeFromParams)
return
}
// set locale from preferences or default if (localeToSet !== localeCode) {
let preferenceLocale: string setLocaleCode(localeToSet)
let isPreferenceInConfig: boolean setLocale(findLocaleFromCode(localization, localeToSet))
if (user) { try {
preferenceLocale = await getPreference<string>('locale') if (user) await setPreference('locale', localeToSet)
isPreferenceInConfig = } catch (error) {
preferenceLocale && localization.localeCodes.indexOf(preferenceLocale) > -1 // swallow error
if (isPreferenceInConfig) {
setLocaleCode(preferenceLocale)
setLocale(findLocaleFromCode(localization, preferenceLocale))
return
} }
await setPreference('locale', defaultLocale)
} }
setLocaleCode(defaultLocale) },
setLocale(findLocaleFromCode(localization, defaultLocale)) [localization, setPreference, user, defaultLocale, localeCode],
} )
void localeChangeHandler()
}, [defaultLocale, getPreference, localeFromParams, setPreference, user, localization, router])
useEffect(() => { useEffect(() => {
if (searchParams?.locale) { async function setInitialLocale() {
dispatchSearchParams({ let localeToSet = defaultLocale
type: 'SET',
params: { if (typeof localeFromParams === 'string') {
locale: searchParams.locale, localeToSet = localeFromParams
}, } else if (user) {
}) try {
localeToSet = await getPreference<string>('locale')
} catch (error) {
// swallow error
}
}
await switchLocale(localeToSet)
} }
}, [searchParams.locale, dispatchSearchParams])
void setInitialLocale()
}, [
defaultLocale,
getPreference,
localization,
localeFromParams,
setPreference,
user,
switchLocale,
])
return <LocaleContext.Provider value={locale}>{children}</LocaleContext.Provider> return <LocaleContext.Provider value={locale}>{children}</LocaleContext.Provider>
} }

View File

@@ -1,13 +1,13 @@
'use client' 'use client'
import { useSearchParams as useNextSearchParams, useRouter } from 'next/navigation.js' import { useSearchParams as useNextSearchParams } from 'next/navigation.js'
import qs from 'qs' import qs from 'qs'
import React, { createContext, useContext } from 'react' import React, { createContext, useContext } from 'react'
import type { Action, SearchParamsContext, State } from './types.js' import type { SearchParamsContext, State } from './types.js'
const initialContext: SearchParamsContext = { const initialContext: SearchParamsContext = {
dispatchSearchParams: () => {},
searchParams: {}, searchParams: {},
stringifyParams: () => '',
} }
const Context = createContext(initialContext) const Context = createContext(initialContext)
@@ -15,46 +15,36 @@ const Context = createContext(initialContext)
// TODO: abstract the `next/navigation` dependency out from this provider so that it can be used in other contexts // TODO: abstract the `next/navigation` dependency out from this provider so that it can be used in other contexts
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 router = useRouter() const searchString = nextSearchParams.toString()
const initialParams = qs.parse(nextSearchParams.toString(), { const initialParams = qs.parse(searchString, {
depth: 10, depth: 10,
ignoreQueryPrefix: true, ignoreQueryPrefix: true,
}) })
const [searchParams, dispatchSearchParams] = React.useReducer((state: State, action: Action) => { const [searchParams, setSearchParams] = React.useState(initialParams)
const stackAction = action.browserHistory || 'push'
let paramsToSet
switch (action.type) { const stringifyParams = React.useCallback(
case 'SET': ({ params, replace = false }: { params: State; replace?: boolean }) => {
paramsToSet = { return qs.stringify(
...state, {
...action.params, ...(replace ? {} : searchParams),
} ...params,
break },
case 'REPLACE': { addQueryPrefix: true },
paramsToSet = action.params )
break },
case 'CLEAR': [searchParams],
paramsToSet = {}
break
default:
return state
}
const newSearchString = qs.stringify(paramsToSet, { addQueryPrefix: true })
if (stackAction === 'push') {
router.push(newSearchString)
} else if (stackAction === 'replace') {
router.replace(newSearchString)
}
return paramsToSet
}, initialParams)
return (
<Context.Provider value={{ dispatchSearchParams, searchParams }}>{children}</Context.Provider>
) )
React.useEffect(() => {
const newSearchParams = qs.parse(searchString, {
depth: 10,
ignoreQueryPrefix: true,
})
setSearchParams(newSearchParams)
}, [searchString])
return <Context.Provider value={{ searchParams, stringifyParams }}>{children}</Context.Provider>
} }
export const useSearchParams = (): SearchParamsContext => useContext(Context) export const useSearchParams = (): SearchParamsContext => useContext(Context)

View File

@@ -1,27 +1,6 @@
export type SearchParamsContext = { export type SearchParamsContext = {
dispatchSearchParams: (action: Action) => void
searchParams: qs.ParsedQs searchParams: qs.ParsedQs
stringifyParams: ({ params, replace }: { params: State; replace?: boolean }) => string
} }
export type State = qs.ParsedQs export type State = qs.ParsedQs
export type Action = (
| {
params: qs.ParsedQs
type: 'REPLACE'
}
| {
params: qs.ParsedQs
type: 'SET'
}
| {
type: 'CLEAR'
}
) & {
/**
* `push` will add a new entry to the browser history stack.
* `replace` will overwrite the browser history entry.
* @default 'push'
* */
browserHistory?: 'push' | 'replace'
}