Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions components/Layout/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import NavBar from '../Navigation';
import MetaBar from '../Metabar';
import SideBar from '../Sidebar';
import Footer from '../Footer';
import LocaleProvider from '../providers/LocaleProvider';

/**
* @typedef {Object} Props
Expand All @@ -20,7 +21,7 @@ import Footer from '../Footer';
* @param {Props} props
*/
export default ({ metadata, headings, readingTime, children }) => (
<>
<LocaleProvider>
<Analytics basePath="/learn/_vercel" />
<SpeedInsights basePath="/learn/_vercel" />
<NavBar metadata={metadata} />
Expand All @@ -40,5 +41,5 @@ export default ({ metadata, headings, readingTime, children }) => (
</div>
</Article>
<Footer metadata={metadata} />
</>
</LocaleProvider>
);
12 changes: 10 additions & 2 deletions components/Navigation/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,31 @@ import ThemeToggle from '@node-core/ui-components/Common/ThemeToggle';
import NavBar from '@node-core/ui-components/Containers/NavBar';
import styles from '@node-core/ui-components/Containers/NavBar/index.module.css';
import GitHubIcon from '@node-core/ui-components/Icons/Social/GitHub';

import SearchBox from '@node-core/doc-kit/src/generators/web/ui/components/SearchBox';
import { useTheme } from '@node-core/doc-kit/src/generators/web/ui/hooks/useTheme.mjs';

import { navigation } from '../../site.json' with { type: 'json' };
import Logo from '#theme/Logo';
import { useLocale } from '../providers/LocaleProvider';
import { useMemo } from 'preact/hooks';

/**
* NavBar component that displays the headings, search, etc.
*/
export default ({ metadata }) => {
const [themePreference, setThemePreference] = useTheme();
const { localizeLink } = useLocale();

const navigationItems = useMemo(
() => navigation.map(item => ({ ...item, link: localizeLink(item.link) })),
[localizeLink]
);

return (
<NavBar
Logo={Logo}
sidebarItemTogglerAriaLabel="Toggle navigation menu"
navItems={navigation}
navItems={navigationItems}
>
<SearchBox pathname={metadata.path} />
<ThemeToggle
Expand Down
66 changes: 66 additions & 0 deletions components/providers/LocaleProvider.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { createContext } from 'preact';
import { useContext, useState, useEffect } from 'preact/hooks';

import { defaultLocale } from '../../site.json' with { type: 'json' };

const LOCALE_COOKIE = 'NEXT_LOCALE';

/**
* Replaces the default locale prefix in a link with the given locale.
*
* @param {string} link
* @param {string} locale
* @returns {string}
*/
const localizeLink = (link, locale) =>
link.replace(new RegExp(`^/${defaultLocale}(?=/|$)`), `/${locale}`);

/**
* Detects the locale from the NEXT_LOCALE cookie.
* Falls back to the default locale when the cookie is missing.
*
* @returns {string}
*/
export const detectLocaleFromCookies = () => {
if (typeof document === 'undefined') {
return defaultLocale;
}

const match = document.cookie
.split(';')
.map(cookie => cookie.trim())
.find(cookie => cookie.startsWith(`${LOCALE_COOKIE}=`));

return match
? decodeURIComponent(match.slice(LOCALE_COOKIE.length + 1))
: defaultLocale;
};

const LocaleContext = createContext({
locale: defaultLocale,
localizeLink: link => link,
});

export const useLocale = () => useContext(LocaleContext);

/**
* Provides locale and a pre-bound localizeLink fn to the component tree.
*
* @param {{ locale?: string, children: import('preact').ComponentChildren }} props
*/
export default function LocaleProvider({ locale, children }) {
const [detectedLocale, setDetectedLocale] = useState(defaultLocale);
Comment thread
canerakdas marked this conversation as resolved.

useEffect(() => {
setDetectedLocale(locale ?? detectLocaleFromCookies());
}, [locale]);

const value = {
locale: detectedLocale,
localizeLink: link => localizeLink(link, detectedLocale),
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unmemoized context value defeats downstream useMemo

Medium Severity

The value object passed to LocaleContext.Provider is recreated on every render without useMemo, producing a new localizeLink function reference each time. This causes all context consumers to re-render on every parent render and completely defeats the useMemo in the Navigation component, which lists localizeLink as its sole dependency. The memoization in Navigation is effectively a no-op. The value needs to be wrapped in useMemo with [detectedLocale] as the dependency.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f168ee3. Configure here.


return (
<LocaleContext.Provider value={value}>{children}</LocaleContext.Provider>
);
}
5 changes: 3 additions & 2 deletions site.json
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
{
"defaultLocale": "en",
"navigation": [
{
"link": "/learn",
"text": "Learn"
},
{
"link": "/about",
"link": "/en/about",
"text": "About"
},
{
"link": "/en/download",
"text": "Download"
},
{
"link": "/blog",
"link": "/en/blog",
"text": "Blog"
},
{
Expand Down