Optimize LanguageProvider context value memoization
Co-authored-by: ragusa-it <196988693+ragusa-it@users.noreply.github.com>
This commit is contained in:
50
src/i18n/__tests__/perf.test.tsx
Normal file
50
src/i18n/__tests__/perf.test.tsx
Normal file
@@ -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 <div data-testid="consumer">Consumer</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MemoizedConsumer = React.memo(Consumer);
|
||||||
|
|
||||||
|
const Wrapper = () => {
|
||||||
|
const [count, setCount] = useState(0);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button onClick={() => setCount(c => c + 1)}>Force Render</button>
|
||||||
|
<div data-testid="count">{count}</div>
|
||||||
|
<LanguageProvider>
|
||||||
|
<MemoizedConsumer />
|
||||||
|
</LanguageProvider>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
render(<Wrapper />);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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 { de, type Translations } from './de';
|
||||||
import { en } from './en';
|
import { en } from './en';
|
||||||
|
|
||||||
@@ -35,11 +35,11 @@ export function LanguageProvider({ children }: { children: ReactNode }) {
|
|||||||
setIsInitialized(true);
|
setIsInitialized(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const setLanguage = (lang: Language) => {
|
const setLanguage = useCallback((lang: Language) => {
|
||||||
setLanguageState(lang);
|
setLanguageState(lang);
|
||||||
localStorage.setItem(STORAGE_KEY, lang);
|
localStorage.setItem(STORAGE_KEY, lang);
|
||||||
document.documentElement.lang = lang;
|
document.documentElement.lang = lang;
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isInitialized) {
|
if (isInitialized) {
|
||||||
@@ -47,11 +47,11 @@ export function LanguageProvider({ children }: { children: ReactNode }) {
|
|||||||
}
|
}
|
||||||
}, [language, isInitialized]);
|
}, [language, isInitialized]);
|
||||||
|
|
||||||
const value: LanguageContextType = {
|
const value = useMemo<LanguageContextType>(() => ({
|
||||||
language,
|
language,
|
||||||
setLanguage,
|
setLanguage,
|
||||||
t: translations[language],
|
t: translations[language],
|
||||||
};
|
}), [language, setLanguage]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LanguageContext.Provider value={value}>
|
<LanguageContext.Provider value={value}>
|
||||||
|
|||||||
Reference in New Issue
Block a user