Skip to content

Commit

Permalink
fix(ui): stale locale value from useLocale (payloadcms#9582)
Browse files Browse the repository at this point in the history
### 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.
  • Loading branch information
JarrodMFlesch authored Dec 4, 2024
1 parent 2321970 commit fa7ed3f
Show file tree
Hide file tree
Showing 11 changed files with 68 additions and 82 deletions.
2 changes: 1 addition & 1 deletion examples/custom-server/next.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { withPayload } from "@payloadcms/next/withPayload";
import { withPayload } from '@payloadcms/next/withPayload'
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {}
Expand Down
8 changes: 4 additions & 4 deletions examples/custom-server/package.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
{
"name": "payload-3-custom-server",
"type": "module",
"scripts": {
"dev": "nodemon",
"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: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": {
"@payloadcms/db-mongodb": "latest",
"@payloadcms/next": "latest",
Expand Down
2 changes: 1 addition & 1 deletion examples/custom-server/src/app/(app)/b/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export default function B() {
return <div>b</div>;
return <div>b</div>
}
8 changes: 2 additions & 6 deletions examples/custom-server/src/app/(app)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
)
}
6 changes: 1 addition & 5 deletions examples/custom-server/src/app/(payload)/admin/importMap.js
Original file line number Diff line number Diff line change
@@ -1,5 +1 @@


export const importMap = {

}
export const importMap = {}
10 changes: 5 additions & 5 deletions packages/ui/src/elements/PublishButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const PublishButton: React.FC<{ label?: string }> = ({ label: labelProp }
const { submit } = useForm()
const modified = useFormModified()
const editDepth = useEditDepth()
const { code: locale } = useLocale()
const { code: localeCode } = useLocale()

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

const { i18n, t } = useTranslation()
const { code } = useLocale()
const label = labelProp || t('version:publishChanges')

const hasNewerVersions = unpublishedVersionCount > 0
Expand All @@ -54,7 +53,7 @@ export const PublishButton: React.FC<{ label?: string }> = ({ label: labelProp }
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 method = 'POST'

Expand All @@ -77,7 +76,7 @@ export const PublishButton: React.FC<{ label?: string }> = ({ label: labelProp }
},
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) => {
e.preventDefault()
Expand Down Expand Up @@ -140,7 +139,8 @@ export const PublishButton: React.FC<{ label?: string }> = ({ label: labelProp }
? locale.label
: 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) {
return (
Expand Down
3 changes: 1 addition & 2 deletions packages/ui/src/fields/Relationship/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ import { useTranslation } from '../../providers/Translation/index.js'
import { mergeFieldStyles } from '../mergeFieldStyles.js'
import { fieldBaseClass } from '../shared/index.js'
import { createRelationMap } from './createRelationMap.js'
import './index.scss'
import { findOptionsByValue } from './findOptionsByValue.js'
import { optionsReducer } from './optionsReducer.js'
import { MultiValueLabel } from './select-components/MultiValueLabel/index.js'
import { SingleValue } from './select-components/SingleValue/index.js'
import './index.scss'

const maxResultsPerRequest = 10

Expand Down Expand Up @@ -310,7 +310,6 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) =>
// ///////////////////////////////////
// Ensure we have an option for each value
// ///////////////////////////////////

useIgnoredEffect(
() => {
const relationMap = createRelationMap({
Expand Down
2 changes: 2 additions & 0 deletions packages/ui/src/forms/Form/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,7 @@ export const Form: React.FC<FormProps> = (props) => {
docPermissions,
docPreferences,
globalSlug,
locale,
operation,
renderAllFields: true,
schemaPath: collectionSlug ? collectionSlug : globalSlug,
Expand All @@ -504,6 +505,7 @@ export const Form: React.FC<FormProps> = (props) => {
getFormState,
docPermissions,
getDocPreferences,
locale,
],
)

Expand Down
91 changes: 41 additions & 50 deletions packages/ui/src/providers/Locale/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,73 +21,64 @@ export const LocaleProvider: React.FC<{ children?: React.ReactNode }> = ({ child
const defaultLocale =
localization && localization.defaultLocale ? localization.defaultLocale : 'en'

const { getPreference, setPreference } = usePreferences()
const searchParams = useSearchParams()
const localeFromParams = searchParams.get('locale')

const [localeCode, setLocaleCode] = useState<string>(localeFromParams || defaultLocale)

const [locale, setLocale] = useState<Locale | null>(
localization && findLocaleFromCode(localization, localeCode),
)

const { getPreference, setPreference } = usePreferences()
const [localeCode, setLocaleCode] = useState<string>(defaultLocale)

const switchLocale = React.useCallback(
async (newLocale: string) => {
if (!localization) {
return
}
const locale: Locale = React.useMemo(() => {
if (!localization) {
// TODO: return null V4
return {} as Locale
}

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],
)
return (
findLocaleFromCode(localization, localeFromParams || localeCode) ||
findLocaleFromCode(localization, defaultLocale)
)
}, [localeCode, localeFromParams, localization, defaultLocale])

useEffect(() => {
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
if (localization && user) {
if (typeof localeFromParams !== 'string') {
try {
const localeToSet = await getPreference<string>('locale')
setLocaleCode(localeToSet)
} catch (_) {
setLocaleCode(defaultLocale)
}
} else {
void setPreference(
'locale',
findLocaleFromCode(localization, localeFromParams)?.code || defaultLocale,
)
}
}

await switchLocale(localeToSet)
}

void setInitialLocale()
}, [
defaultLocale,
getPreference,
localization,
localeFromParams,
setPreference,
user,
switchLocale,
])
}, [defaultLocale, getPreference, localization, localeFromParams, setPreference, user])

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)
16 changes: 9 additions & 7 deletions packages/ui/src/providers/SearchParams/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import { useSearchParams as useNextSearchParams } from 'next/navigation.js'
import * as qs from 'qs-esm'
import React, { createContext, useContext } from 'react'

import { parseSearchParams } from '../../utilities/parseSearchParams.js'

export type SearchParamsContext = {
searchParams: qs.ParsedQs
stringifyParams: ({ params, replace }: { params: qs.ParsedQs; replace?: boolean }) => string
Expand All @@ -28,8 +26,16 @@ const Context = createContext(initialContext)
*/
export const SearchParamsProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
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(
({ params, replace = false }: { params: qs.ParsedQs; replace?: boolean }) => {
Expand All @@ -44,10 +50,6 @@ export const SearchParamsProvider: React.FC<{ children?: React.ReactNode }> = ({
[searchParams],
)

React.useEffect(() => {
setSearchParams(parseSearchParams(nextSearchParams))
}, [nextSearchParams])

return <Context.Provider value={{ searchParams, stringifyParams }}>{children}</Context.Provider>
}

Expand Down
2 changes: 1 addition & 1 deletion scripts/utils/generateReleaseNotes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export const generateReleaseNotes = async (args: Args = {}): Promise<ChangelogRe

return sections
},
{} as Record<Sections | 'breaking', GitCommit[]>,
{} as Record<'breaking' | Sections, GitCommit[]>,
)

// Sort commits by scope, unscoped first
Expand Down

0 comments on commit fa7ed3f

Please sign in to comment.