Skip to content

Commit fa7ed3f

Browse files
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.
1 parent 2321970 commit fa7ed3f

File tree

11 files changed

+68
-82
lines changed

11 files changed

+68
-82
lines changed

examples/custom-server/next.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { withPayload } from "@payloadcms/next/withPayload";
1+
import { withPayload } from '@payloadcms/next/withPayload'
22
import type { NextConfig } from 'next'
33

44
const nextConfig: NextConfig = {}

examples/custom-server/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
{
22
"name": "payload-3-custom-server",
3+
"type": "module",
34
"scripts": {
4-
"dev": "nodemon",
55
"build": "next build && tsc --project tsconfig.server.json",
6-
"start": "cross-env NODE_ENV=production node dist/server.js",
6+
"dev": "nodemon",
77
"generate:importmap": "cross-env NODE_OPTIONS=--no-deprecation payload generate:importmap",
88
"generate:types": "cross-env NODE_OPTIONS=--no-deprecation payload generate:types",
9-
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload"
9+
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
10+
"start": "cross-env NODE_ENV=production node dist/server.js"
1011
},
11-
"type": "module",
1212
"dependencies": {
1313
"@payloadcms/db-mongodb": "latest",
1414
"@payloadcms/next": "latest",
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
export default function B() {
2-
return <div>b</div>;
2+
return <div>b</div>
33
}
Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
1-
export default function RootLayout({
2-
children,
3-
}: {
4-
children: React.ReactNode;
5-
}) {
1+
export default function RootLayout({ children }: { children: React.ReactNode }) {
62
return (
73
<html lang="en">
84
<body>{children}</body>
95
</html>
10-
);
6+
)
117
}
Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1 @@
1-
2-
3-
export const importMap = {
4-
5-
}
1+
export const importMap = {}

packages/ui/src/elements/PublishButton/index.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export const PublishButton: React.FC<{ label?: string }> = ({ label: labelProp }
3131
const { submit } = useForm()
3232
const modified = useFormModified()
3333
const editDepth = useEditDepth()
34-
const { code: locale } = useLocale()
34+
const { code: localeCode } = useLocale()
3535

3636
const {
3737
localization,
@@ -40,7 +40,6 @@ export const PublishButton: React.FC<{ label?: string }> = ({ label: labelProp }
4040
} = config
4141

4242
const { i18n, t } = useTranslation()
43-
const { code } = useLocale()
4443
const label = labelProp || t('version:publishChanges')
4544

4645
const hasNewerVersions = unpublishedVersionCount > 0
@@ -54,7 +53,7 @@ export const PublishButton: React.FC<{ label?: string }> = ({ label: labelProp }
5453
return
5554
}
5655

57-
const search = `?locale=${locale}&depth=0&fallback-locale=null&draft=true`
56+
const search = `?locale=${localeCode}&depth=0&fallback-locale=null&draft=true`
5857
let action
5958
let method = 'POST'
6059

@@ -77,7 +76,7 @@ export const PublishButton: React.FC<{ label?: string }> = ({ label: labelProp }
7776
},
7877
skipValidation: true,
7978
})
80-
}, [submit, collectionSlug, globalSlug, serverURL, api, locale, id, forceDisable])
79+
}, [submit, collectionSlug, globalSlug, serverURL, api, localeCode, id, forceDisable])
8180

8281
useHotkey({ cmdCtrlKey: true, editDepth, keyCodes: ['s'] }, (e) => {
8382
e.preventDefault()
@@ -140,7 +139,8 @@ export const PublishButton: React.FC<{ label?: string }> = ({ label: labelProp }
140139
? locale.label
141140
: locale.label && locale.label[i18n?.language]
142141

143-
const isActive = typeof locale === 'string' ? locale === code : locale.code === code
142+
const isActive =
143+
typeof locale === 'string' ? locale === localeCode : locale.code === localeCode
144144

145145
if (isActive) {
146146
return (

packages/ui/src/fields/Relationship/index.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@ import { useTranslation } from '../../providers/Translation/index.js'
2727
import { mergeFieldStyles } from '../mergeFieldStyles.js'
2828
import { fieldBaseClass } from '../shared/index.js'
2929
import { createRelationMap } from './createRelationMap.js'
30-
import './index.scss'
3130
import { findOptionsByValue } from './findOptionsByValue.js'
3231
import { optionsReducer } from './optionsReducer.js'
3332
import { MultiValueLabel } from './select-components/MultiValueLabel/index.js'
3433
import { SingleValue } from './select-components/SingleValue/index.js'
34+
import './index.scss'
3535

3636
const maxResultsPerRequest = 10
3737

@@ -310,7 +310,6 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) =>
310310
// ///////////////////////////////////
311311
// Ensure we have an option for each value
312312
// ///////////////////////////////////
313-
314313
useIgnoredEffect(
315314
() => {
316315
const relationMap = createRelationMap({

packages/ui/src/forms/Form/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,7 @@ export const Form: React.FC<FormProps> = (props) => {
485485
docPermissions,
486486
docPreferences,
487487
globalSlug,
488+
locale,
488489
operation,
489490
renderAllFields: true,
490491
schemaPath: collectionSlug ? collectionSlug : globalSlug,
@@ -504,6 +505,7 @@ export const Form: React.FC<FormProps> = (props) => {
504505
getFormState,
505506
docPermissions,
506507
getDocPreferences,
508+
locale,
507509
],
508510
)
509511

packages/ui/src/providers/Locale/index.tsx

Lines changed: 41 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -21,73 +21,64 @@ export const LocaleProvider: React.FC<{ children?: React.ReactNode }> = ({ child
2121
const defaultLocale =
2222
localization && localization.defaultLocale ? localization.defaultLocale : 'en'
2323

24+
const { getPreference, setPreference } = usePreferences()
2425
const searchParams = useSearchParams()
2526
const localeFromParams = searchParams.get('locale')
2627

27-
const [localeCode, setLocaleCode] = useState<string>(localeFromParams || defaultLocale)
28-
29-
const [locale, setLocale] = useState<Locale | null>(
30-
localization && findLocaleFromCode(localization, localeCode),
31-
)
32-
33-
const { getPreference, setPreference } = usePreferences()
28+
const [localeCode, setLocaleCode] = useState<string>(defaultLocale)
3429

35-
const switchLocale = React.useCallback(
36-
async (newLocale: string) => {
37-
if (!localization) {
38-
return
39-
}
30+
const locale: Locale = React.useMemo(() => {
31+
if (!localization) {
32+
// TODO: return null V4
33+
return {} as Locale
34+
}
4035

41-
const localeToSet =
42-
localization.localeCodes.indexOf(newLocale) > -1 ? newLocale : defaultLocale
43-
44-
if (localeToSet !== localeCode) {
45-
setLocaleCode(localeToSet)
46-
setLocale(findLocaleFromCode(localization, localeToSet))
47-
try {
48-
if (user) {
49-
await setPreference('locale', localeToSet)
50-
}
51-
} catch (error) {
52-
// swallow error
53-
}
54-
}
55-
},
56-
[localization, setPreference, user, defaultLocale, localeCode],
57-
)
36+
return (
37+
findLocaleFromCode(localization, localeFromParams || localeCode) ||
38+
findLocaleFromCode(localization, defaultLocale)
39+
)
40+
}, [localeCode, localeFromParams, localization, defaultLocale])
5841

5942
useEffect(() => {
6043
async function setInitialLocale() {
61-
let localeToSet = defaultLocale
62-
63-
if (typeof localeFromParams === 'string') {
64-
localeToSet = localeFromParams
65-
} else if (user) {
66-
try {
67-
localeToSet = await getPreference<string>('locale')
68-
} catch (error) {
69-
// swallow error
44+
if (localization && user) {
45+
if (typeof localeFromParams !== 'string') {
46+
try {
47+
const localeToSet = await getPreference<string>('locale')
48+
setLocaleCode(localeToSet)
49+
} catch (_) {
50+
setLocaleCode(defaultLocale)
51+
}
52+
} else {
53+
void setPreference(
54+
'locale',
55+
findLocaleFromCode(localization, localeFromParams)?.code || defaultLocale,
56+
)
7057
}
7158
}
72-
73-
await switchLocale(localeToSet)
7459
}
7560

7661
void setInitialLocale()
77-
}, [
78-
defaultLocale,
79-
getPreference,
80-
localization,
81-
localeFromParams,
82-
setPreference,
83-
user,
84-
switchLocale,
85-
])
62+
}, [defaultLocale, getPreference, localization, localeFromParams, setPreference, user])
8663

8764
return <LocaleContext.Provider value={locale}>{children}</LocaleContext.Provider>
8865
}
8966

9067
/**
91-
* A hook that returns the current locale object.
68+
* @deprecated A hook that returns the current locale object.
69+
*
70+
* ---
71+
*
72+
* #### 🚨 V4 Breaking Change
73+
* The `useLocale` return type now reflects `null | Locale` instead of `false | Locale`.
74+
*
75+
* **Old (V3):**
76+
* ```ts
77+
* const { code } = useLocale();
78+
* ```
79+
* **New (V4):**
80+
* ```ts
81+
* const locale = useLocale();
82+
* ```
9283
*/
9384
export const useLocale = (): Locale => useContext(LocaleContext)

packages/ui/src/providers/SearchParams/index.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ import { useSearchParams as useNextSearchParams } from 'next/navigation.js'
44
import * as qs from 'qs-esm'
55
import React, { createContext, useContext } from 'react'
66

7-
import { parseSearchParams } from '../../utilities/parseSearchParams.js'
8-
97
export type SearchParamsContext = {
108
searchParams: qs.ParsedQs
119
stringifyParams: ({ params, replace }: { params: qs.ParsedQs; replace?: boolean }) => string
@@ -28,8 +26,16 @@ const Context = createContext(initialContext)
2826
*/
2927
export const SearchParamsProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
3028
const nextSearchParams = useNextSearchParams()
29+
const searchString = nextSearchParams.toString()
3130

32-
const [searchParams, setSearchParams] = React.useState(() => parseSearchParams(nextSearchParams))
31+
const searchParams = React.useMemo(
32+
() =>
33+
qs.parse(searchString, {
34+
depth: 10,
35+
ignoreQueryPrefix: true,
36+
}),
37+
[searchString],
38+
)
3339

3440
const stringifyParams = React.useCallback(
3541
({ params, replace = false }: { params: qs.ParsedQs; replace?: boolean }) => {
@@ -44,10 +50,6 @@ export const SearchParamsProvider: React.FC<{ children?: React.ReactNode }> = ({
4450
[searchParams],
4551
)
4652

47-
React.useEffect(() => {
48-
setSearchParams(parseSearchParams(nextSearchParams))
49-
}, [nextSearchParams])
50-
5153
return <Context.Provider value={{ searchParams, stringifyParams }}>{children}</Context.Provider>
5254
}
5355

0 commit comments

Comments
 (0)