From 169cbc1bcb909b1a7d1d59cb10b9d1b56f139f57 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 05:00:43 +0000 Subject: [PATCH] Optimize LanguageProvider context value memoization Co-authored-by: ragusa-it <196988693+ragusa-it@users.noreply.github.com> --- src/i18n/__tests__/perf.test.tsx | 50 ++++++++++++++++++++++++++++++++ src/i18n/index.tsx | 10 +++---- 2 files changed, 55 insertions(+), 5 deletions(-) create mode 100644 src/i18n/__tests__/perf.test.tsx diff --git a/src/i18n/__tests__/perf.test.tsx b/src/i18n/__tests__/perf.test.tsx new file mode 100644 index 0000000..e16433b --- /dev/null +++ b/src/i18n/__tests__/perf.test.tsx @@ -0,0 +1,50 @@ +// @vitest-environment jsdom +import React, { useState } from 'react'; +import { render, screen, act } from '@testing-library/react'; +import { LanguageProvider, useTranslation } from '../index'; +import { describe, it, expect } from 'vitest'; + +describe('LanguageProvider Performance', () => { + it('should prevent consumers from re-rendering when provider re-renders but value is unchanged', async () => { + let renderCount = 0; + + const Consumer = () => { + useTranslation(); + renderCount++; + return
Consumer
; + }; + + const MemoizedConsumer = React.memo(Consumer); + + const Wrapper = () => { + const [count, setCount] = useState(0); + return ( +
+ +
{count}
+ + + +
+ ); + }; + + render(); + + // Wait for effects to settle (language detection might trigger update) + await act(async () => { + await new Promise(resolve => setTimeout(resolve, 0)); + }); + + const initialRenderCount = renderCount; + + // Force re-render of Wrapper, which causes re-render of LanguageProvider + const button = screen.getByText('Force Render'); + await act(async () => { + button.click(); + }); + + // After optimization, renderCount should not increase. + expect(renderCount).toBe(initialRenderCount); + }); +}); diff --git a/src/i18n/index.tsx b/src/i18n/index.tsx index f51c923..7be82f7 100644 --- a/src/i18n/index.tsx +++ b/src/i18n/index.tsx @@ -1,4 +1,4 @@ -import { createContext, useContext, useState, useEffect, type ReactNode } from 'react'; +import { createContext, useContext, useState, useEffect, useMemo, useCallback, type ReactNode } from 'react'; import { de, type Translations } from './de'; import { en } from './en'; @@ -35,11 +35,11 @@ export function LanguageProvider({ children }: { children: ReactNode }) { setIsInitialized(true); }, []); - const setLanguage = (lang: Language) => { + const setLanguage = useCallback((lang: Language) => { setLanguageState(lang); localStorage.setItem(STORAGE_KEY, lang); document.documentElement.lang = lang; - }; + }, []); useEffect(() => { if (isInitialized) { @@ -47,11 +47,11 @@ export function LanguageProvider({ children }: { children: ReactNode }) { } }, [language, isInitialized]); - const value: LanguageContextType = { + const value = useMemo(() => ({ language, setLanguage, t: translations[language], - }; + }), [language, setLanguage]); return (