fix(ui): stale locale value from useLocale (#9582)
### What? Fixes issue with stale locale from searchParams ### Why? Bad use of useEffect/useState inside our useSearchParams provider. ### How? Memoize the locale instead of relying on the useEffect which was causing unnecessary renders with stale values.
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { withPayload } from "@payloadcms/next/withPayload";
|
import { withPayload } from '@payloadcms/next/withPayload'
|
||||||
import type { NextConfig } from 'next'
|
import type { NextConfig } from 'next'
|
||||||
|
|
||||||
const nextConfig: NextConfig = {}
|
const nextConfig: NextConfig = {}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "payload-3-custom-server",
|
"name": "payload-3-custom-server",
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nodemon",
|
|
||||||
"build": "next build && tsc --project tsconfig.server.json",
|
"build": "next build && tsc --project tsconfig.server.json",
|
||||||
"start": "cross-env NODE_ENV=production node dist/server.js",
|
"dev": "nodemon",
|
||||||
"generate:importmap": "cross-env NODE_OPTIONS=--no-deprecation payload generate:importmap",
|
"generate:importmap": "cross-env NODE_OPTIONS=--no-deprecation payload generate:importmap",
|
||||||
"generate:types": "cross-env NODE_OPTIONS=--no-deprecation payload generate:types",
|
"generate:types": "cross-env NODE_OPTIONS=--no-deprecation payload generate:types",
|
||||||
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload"
|
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
|
||||||
|
"start": "cross-env NODE_ENV=production node dist/server.js"
|
||||||
},
|
},
|
||||||
"type": "module",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@payloadcms/db-mongodb": "latest",
|
"@payloadcms/db-mongodb": "latest",
|
||||||
"@payloadcms/next": "latest",
|
"@payloadcms/next": "latest",
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export default function B() {
|
export default function B() {
|
||||||
return <div>b</div>;
|
return <div>b</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
export default function RootLayout({
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body>{children}</body>
|
<body>{children}</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1 @@
|
|||||||
|
export const importMap = {}
|
||||||
|
|
||||||
export const importMap = {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export const PublishButton: React.FC<{ label?: string }> = ({ label: labelProp }
|
|||||||
const { submit } = useForm()
|
const { submit } = useForm()
|
||||||
const modified = useFormModified()
|
const modified = useFormModified()
|
||||||
const editDepth = useEditDepth()
|
const editDepth = useEditDepth()
|
||||||
const { code: locale } = useLocale()
|
const { code: localeCode } = useLocale()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
localization,
|
localization,
|
||||||
@@ -40,7 +40,6 @@ export const PublishButton: React.FC<{ label?: string }> = ({ label: labelProp }
|
|||||||
} = config
|
} = config
|
||||||
|
|
||||||
const { i18n, t } = useTranslation()
|
const { i18n, t } = useTranslation()
|
||||||
const { code } = useLocale()
|
|
||||||
const label = labelProp || t('version:publishChanges')
|
const label = labelProp || t('version:publishChanges')
|
||||||
|
|
||||||
const hasNewerVersions = unpublishedVersionCount > 0
|
const hasNewerVersions = unpublishedVersionCount > 0
|
||||||
@@ -54,7 +53,7 @@ export const PublishButton: React.FC<{ label?: string }> = ({ label: labelProp }
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const search = `?locale=${locale}&depth=0&fallback-locale=null&draft=true`
|
const search = `?locale=${localeCode}&depth=0&fallback-locale=null&draft=true`
|
||||||
let action
|
let action
|
||||||
let method = 'POST'
|
let method = 'POST'
|
||||||
|
|
||||||
@@ -77,7 +76,7 @@ export const PublishButton: React.FC<{ label?: string }> = ({ label: labelProp }
|
|||||||
},
|
},
|
||||||
skipValidation: true,
|
skipValidation: true,
|
||||||
})
|
})
|
||||||
}, [submit, collectionSlug, globalSlug, serverURL, api, locale, id, forceDisable])
|
}, [submit, collectionSlug, globalSlug, serverURL, api, localeCode, id, forceDisable])
|
||||||
|
|
||||||
useHotkey({ cmdCtrlKey: true, editDepth, keyCodes: ['s'] }, (e) => {
|
useHotkey({ cmdCtrlKey: true, editDepth, keyCodes: ['s'] }, (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -140,7 +139,8 @@ export const PublishButton: React.FC<{ label?: string }> = ({ label: labelProp }
|
|||||||
? locale.label
|
? locale.label
|
||||||
: locale.label && locale.label[i18n?.language]
|
: locale.label && locale.label[i18n?.language]
|
||||||
|
|
||||||
const isActive = typeof locale === 'string' ? locale === code : locale.code === code
|
const isActive =
|
||||||
|
typeof locale === 'string' ? locale === localeCode : locale.code === localeCode
|
||||||
|
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -27,11 +27,11 @@ import { useTranslation } from '../../providers/Translation/index.js'
|
|||||||
import { mergeFieldStyles } from '../mergeFieldStyles.js'
|
import { mergeFieldStyles } from '../mergeFieldStyles.js'
|
||||||
import { fieldBaseClass } from '../shared/index.js'
|
import { fieldBaseClass } from '../shared/index.js'
|
||||||
import { createRelationMap } from './createRelationMap.js'
|
import { createRelationMap } from './createRelationMap.js'
|
||||||
import './index.scss'
|
|
||||||
import { findOptionsByValue } from './findOptionsByValue.js'
|
import { findOptionsByValue } from './findOptionsByValue.js'
|
||||||
import { optionsReducer } from './optionsReducer.js'
|
import { optionsReducer } from './optionsReducer.js'
|
||||||
import { MultiValueLabel } from './select-components/MultiValueLabel/index.js'
|
import { MultiValueLabel } from './select-components/MultiValueLabel/index.js'
|
||||||
import { SingleValue } from './select-components/SingleValue/index.js'
|
import { SingleValue } from './select-components/SingleValue/index.js'
|
||||||
|
import './index.scss'
|
||||||
|
|
||||||
const maxResultsPerRequest = 10
|
const maxResultsPerRequest = 10
|
||||||
|
|
||||||
@@ -310,7 +310,6 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) =>
|
|||||||
// ///////////////////////////////////
|
// ///////////////////////////////////
|
||||||
// Ensure we have an option for each value
|
// Ensure we have an option for each value
|
||||||
// ///////////////////////////////////
|
// ///////////////////////////////////
|
||||||
|
|
||||||
useIgnoredEffect(
|
useIgnoredEffect(
|
||||||
() => {
|
() => {
|
||||||
const relationMap = createRelationMap({
|
const relationMap = createRelationMap({
|
||||||
|
|||||||
@@ -485,6 +485,7 @@ export const Form: React.FC<FormProps> = (props) => {
|
|||||||
docPermissions,
|
docPermissions,
|
||||||
docPreferences,
|
docPreferences,
|
||||||
globalSlug,
|
globalSlug,
|
||||||
|
locale,
|
||||||
operation,
|
operation,
|
||||||
renderAllFields: true,
|
renderAllFields: true,
|
||||||
schemaPath: collectionSlug ? collectionSlug : globalSlug,
|
schemaPath: collectionSlug ? collectionSlug : globalSlug,
|
||||||
@@ -504,6 +505,7 @@ export const Form: React.FC<FormProps> = (props) => {
|
|||||||
getFormState,
|
getFormState,
|
||||||
docPermissions,
|
docPermissions,
|
||||||
getDocPreferences,
|
getDocPreferences,
|
||||||
|
locale,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -21,73 +21,64 @@ 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 { getPreference, setPreference } = usePreferences()
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const localeFromParams = searchParams.get('locale')
|
const localeFromParams = searchParams.get('locale')
|
||||||
|
|
||||||
const [localeCode, setLocaleCode] = useState<string>(localeFromParams || defaultLocale)
|
const [localeCode, setLocaleCode] = useState<string>(defaultLocale)
|
||||||
|
|
||||||
const [locale, setLocale] = useState<Locale | null>(
|
const locale: Locale = React.useMemo(() => {
|
||||||
localization && findLocaleFromCode(localization, localeCode),
|
if (!localization) {
|
||||||
)
|
// TODO: return null V4
|
||||||
|
return {} as Locale
|
||||||
|
}
|
||||||
|
|
||||||
const { getPreference, setPreference } = usePreferences()
|
return (
|
||||||
|
findLocaleFromCode(localization, localeFromParams || localeCode) ||
|
||||||
const switchLocale = React.useCallback(
|
findLocaleFromCode(localization, defaultLocale)
|
||||||
async (newLocale: string) => {
|
)
|
||||||
if (!localization) {
|
}, [localeCode, localeFromParams, localization, defaultLocale])
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const localeToSet =
|
|
||||||
localization.localeCodes.indexOf(newLocale) > -1 ? newLocale : defaultLocale
|
|
||||||
|
|
||||||
if (localeToSet !== localeCode) {
|
|
||||||
setLocaleCode(localeToSet)
|
|
||||||
setLocale(findLocaleFromCode(localization, localeToSet))
|
|
||||||
try {
|
|
||||||
if (user) {
|
|
||||||
await setPreference('locale', localeToSet)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// swallow error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[localization, setPreference, user, defaultLocale, localeCode],
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function setInitialLocale() {
|
async function setInitialLocale() {
|
||||||
let localeToSet = defaultLocale
|
if (localization && user) {
|
||||||
|
if (typeof localeFromParams !== 'string') {
|
||||||
if (typeof localeFromParams === 'string') {
|
try {
|
||||||
localeToSet = localeFromParams
|
const localeToSet = await getPreference<string>('locale')
|
||||||
} else if (user) {
|
setLocaleCode(localeToSet)
|
||||||
try {
|
} catch (_) {
|
||||||
localeToSet = await getPreference<string>('locale')
|
setLocaleCode(defaultLocale)
|
||||||
} catch (error) {
|
}
|
||||||
// swallow error
|
} else {
|
||||||
|
void setPreference(
|
||||||
|
'locale',
|
||||||
|
findLocaleFromCode(localization, localeFromParams)?.code || defaultLocale,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await switchLocale(localeToSet)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void setInitialLocale()
|
void setInitialLocale()
|
||||||
}, [
|
}, [defaultLocale, getPreference, localization, localeFromParams, setPreference, user])
|
||||||
defaultLocale,
|
|
||||||
getPreference,
|
|
||||||
localization,
|
|
||||||
localeFromParams,
|
|
||||||
setPreference,
|
|
||||||
user,
|
|
||||||
switchLocale,
|
|
||||||
])
|
|
||||||
|
|
||||||
return <LocaleContext.Provider value={locale}>{children}</LocaleContext.Provider>
|
return <LocaleContext.Provider value={locale}>{children}</LocaleContext.Provider>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A hook that returns the current locale object.
|
* @deprecated A hook that returns the current locale object.
|
||||||
|
*
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* #### 🚨 V4 Breaking Change
|
||||||
|
* The `useLocale` return type now reflects `null | Locale` instead of `false | Locale`.
|
||||||
|
*
|
||||||
|
* **Old (V3):**
|
||||||
|
* ```ts
|
||||||
|
* const { code } = useLocale();
|
||||||
|
* ```
|
||||||
|
* **New (V4):**
|
||||||
|
* ```ts
|
||||||
|
* const locale = useLocale();
|
||||||
|
* ```
|
||||||
*/
|
*/
|
||||||
export const useLocale = (): Locale => useContext(LocaleContext)
|
export const useLocale = (): Locale => useContext(LocaleContext)
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ 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: qs.ParsedQs; replace?: boolean }) => string
|
stringifyParams: ({ params, replace }: { params: qs.ParsedQs; replace?: boolean }) => string
|
||||||
@@ -28,8 +26,16 @@ const Context = createContext(initialContext)
|
|||||||
*/
|
*/
|
||||||
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 searchString = nextSearchParams.toString()
|
||||||
|
|
||||||
const [searchParams, setSearchParams] = React.useState(() => parseSearchParams(nextSearchParams))
|
const searchParams = React.useMemo(
|
||||||
|
() =>
|
||||||
|
qs.parse(searchString, {
|
||||||
|
depth: 10,
|
||||||
|
ignoreQueryPrefix: true,
|
||||||
|
}),
|
||||||
|
[searchString],
|
||||||
|
)
|
||||||
|
|
||||||
const stringifyParams = React.useCallback(
|
const stringifyParams = React.useCallback(
|
||||||
({ params, replace = false }: { params: qs.ParsedQs; replace?: boolean }) => {
|
({ params, replace = false }: { params: qs.ParsedQs; replace?: boolean }) => {
|
||||||
@@ -44,10 +50,6 @@ export const SearchParamsProvider: React.FC<{ children?: React.ReactNode }> = ({
|
|||||||
[searchParams],
|
[searchParams],
|
||||||
)
|
)
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
setSearchParams(parseSearchParams(nextSearchParams))
|
|
||||||
}, [nextSearchParams])
|
|
||||||
|
|
||||||
return <Context.Provider value={{ searchParams, stringifyParams }}>{children}</Context.Provider>
|
return <Context.Provider value={{ searchParams, stringifyParams }}>{children}</Context.Provider>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ export const generateReleaseNotes = async (args: Args = {}): Promise<ChangelogRe
|
|||||||
|
|
||||||
return sections
|
return sections
|
||||||
},
|
},
|
||||||
{} as Record<Sections | 'breaking', GitCommit[]>,
|
{} as Record<'breaking' | Sections, GitCommit[]>,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Sort commits by scope, unscoped first
|
// Sort commits by scope, unscoped first
|
||||||
|
|||||||
Reference in New Issue
Block a user