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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,13 @@
'use client'
import { useSearchParams as useNextSearchParams, useRouter } from 'next/navigation.js'
import { useSearchParams as useNextSearchParams } from 'next/navigation.js'
import qs from 'qs'
import React, { createContext, useContext } from 'react'
import type { Action, SearchParamsContext, State } from './types.js'
import type { SearchParamsContext, State } from './types.js'
const initialContext: SearchParamsContext = {
dispatchSearchParams: () => {},
searchParams: {},
stringifyParams: () => '',
}
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
export const SearchParamsProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
const nextSearchParams = useNextSearchParams()
const router = useRouter()
const initialParams = qs.parse(nextSearchParams.toString(), {
const searchString = nextSearchParams.toString()
const initialParams = qs.parse(searchString, {
depth: 10,
ignoreQueryPrefix: true,
})
const [searchParams, dispatchSearchParams] = React.useReducer((state: State, action: Action) => {
const stackAction = action.browserHistory || 'push'
let paramsToSet
const [searchParams, setSearchParams] = React.useState(initialParams)
switch (action.type) {
case 'SET':
paramsToSet = {
...state,
...action.params,
}
break
case 'REPLACE':
paramsToSet = action.params
break
case 'CLEAR':
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>
const stringifyParams = React.useCallback(
({ params, replace = false }: { params: State; replace?: boolean }) => {
return qs.stringify(
{
...(replace ? {} : searchParams),
...params,
},
{ addQueryPrefix: true },
)
},
[searchParams],
)
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)

View File

@@ -1,27 +1,6 @@
export type SearchParamsContext = {
dispatchSearchParams: (action: Action) => void
searchParams: qs.ParsedQs
stringifyParams: ({ params, replace }: { params: State; replace?: boolean }) => string
}
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'
}