From c3bdb0dcfaa5a4a9275411494c82b0afc2bd14eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20B=C3=B6rjesson=20Skrivanos?= Date: Sun, 14 Mar 2021 12:41:04 +0100 Subject: [PATCH] fix: support shallow route changes * Memoize the i18n client and do not re-create it unless the route or locale has changed (fix #1059). * Let Next's router locale take precedence over initialLocale (fix #1023). * Bump the minimum version of react-i18next. --- examples/simple/yarn.lock | 19 ++++------- package.json | 2 +- src/appWithTranslation.client.test.tsx | 47 ++++++++++++++++++++++++-- src/appWithTranslation.tsx | 35 +++++++++++-------- yarn.lock | 17 +++++++--- 5 files changed, 83 insertions(+), 37 deletions(-) diff --git a/examples/simple/yarn.lock b/examples/simple/yarn.lock index f7a3a85b7..ca316ddd9 100644 --- a/examples/simple/yarn.lock +++ b/examples/simple/yarn.lock @@ -2485,15 +2485,8 @@ neo-async@^2.5.0, neo-async@^2.6.1, neo-async@^2.6.2: integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== "next-i18next@link:../..": - version "8.1.0" - dependencies: - "@types/hoist-non-react-statics" "^3.3.1" - "@types/i18next-fs-backend" "^1.0.0" - core-js "^3" - hoist-non-react-statics "^3.2.0" - i18next "^19.8.4" - i18next-fs-backend "^1.0.7" - react-i18next "^11.8.8" + version "0.0.0" + uid "" next-tick@~1.0.0: version "1.0.0" @@ -3073,10 +3066,10 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-i18next@^11.8.8: - version "11.8.8" - resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.8.8.tgz#23d34518c784f2ada7cec41cfe439ac4ae51875c" - integrity sha512-Z8Daifh+FRpcQsCp48mWQViYSlojv0WiL2bf6e9DOzpfVMDaTT6qsYRbHCjLEeDeEioxoaWHMiWu2JPTW3Ni4w== +react-i18next@^11.8.10: + version "11.8.10" + resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.8.10.tgz#aa64bc20410ee8f660a5b918d53f4e41271edf00" + integrity sha512-ckjNzMjYkmx4fQ8zzuaYTosYN3Co6ebrgCQJzuZCcGFYSR/kGHZzSu0xw9VhtnbjJVKx0gEMV3DLRvzi4xDZUw== dependencies: "@babel/runtime" "^7.13.6" html-parse-stringify2 "^2.0.1" diff --git a/package.json b/package.json index 8c0c06781..e6fba88e0 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "hoist-non-react-statics": "^3.2.0", "i18next": "^19.8.4", "i18next-fs-backend": "^1.0.7", - "react-i18next": "^11.8.8" + "react-i18next": "^11.8.10" }, "peerDependencies": { "next": ">= 10.0.0", diff --git a/src/appWithTranslation.client.test.tsx b/src/appWithTranslation.client.test.tsx index 33e6ad709..4486bfac1 100644 --- a/src/appWithTranslation.client.test.tsx +++ b/src/appWithTranslation.client.test.tsx @@ -2,6 +2,8 @@ import React from 'react' import fs from 'fs' import { screen, render } from '@testing-library/react' import { I18nextProvider } from 'react-i18next' +import { useRouter } from 'next/router' +import createClient from './createClient' import { appWithTranslation } from './appWithTranslation' @@ -19,15 +21,17 @@ jest.mock('react-i18next', () => ({ __esmodule: true, })) +jest.mock('next/router') +jest.mock('./createClient', () => jest.fn()) const DummyApp = appWithTranslation(() => (
Hello world
)) -const props = { +const createProps = (locale = 'en') => ({ pageProps: { _nextI18Next: { - initialLocale: 'en', + initialLocale: locale, userConfig: { i18n: { defaultLocale: 'en', @@ -36,7 +40,9 @@ const props = { }, }, } as any, -} as any +} as any) + +const props = createProps() const renderComponent = () => render( @@ -50,6 +56,8 @@ describe('appWithTranslation', () => { (fs.existsSync as jest.Mock).mockReturnValue(true); (fs.readdirSync as jest.Mock).mockReturnValue([]); (I18nextProvider as jest.Mock).mockImplementation(DummyI18nextProvider) + const actualCreateClient = jest.requireActual('./createClient'); + (createClient as jest.Mock).mockImplementation(actualCreateClient) }) afterEach(jest.resetAllMocks) @@ -124,4 +132,37 @@ describe('appWithTranslation', () => { expect(fs.readdirSync).toHaveBeenCalledTimes(0) }) + it('should let next router locale take precedence', () => { + (useRouter as jest.Mock).mockReturnValue({ locale: 'de' }) + renderComponent() + const [args] = (I18nextProvider as jest.Mock).mock.calls + expect(args[0].i18n.language).toEqual('de') + }) + + it('does not re-call createClient on re-renders unless locale or props has changed', () => { + (useRouter as jest.Mock).mockReturnValue({ route: '/route' }) + const { rerender } = renderComponent() + expect(createClient).toHaveBeenCalledTimes(1) + rerender( + + ) + expect(createClient).toHaveBeenCalledTimes(1) + const newProps = createProps() + rerender( + + ) + expect(createClient).toHaveBeenCalledTimes(2); + (useRouter as jest.Mock).mockReturnValue({ locale: 'de' }) + rerender( + + ) + expect(createClient).toHaveBeenCalledTimes(3) + }) + }) diff --git a/src/appWithTranslation.tsx b/src/appWithTranslation.tsx index 0e70fb895..f19abec3e 100644 --- a/src/appWithTranslation.tsx +++ b/src/appWithTranslation.tsx @@ -9,6 +9,7 @@ import createClient from './createClient' import { SSRConfig, UserConfig } from './types' import { i18n as I18NextClient } from 'i18next' +import { useRouter } from 'next/router' export { Trans, useTranslation, withTranslation } from 'react-i18next' type AppProps = NextJsAppProps & { @@ -22,12 +23,20 @@ export const appWithTranslation = ( configOverride: UserConfig | null = null, ) => { const AppWithTranslation = (props: AppProps) => { - let i18n: I18NextClient | null = null - let locale = null + const router = useRouter() + const { _nextI18Next } = props.pageProps + const initialLocale = _nextI18Next?.initialLocale || null + const locale = router?.locale || initialLocale - if (props?.pageProps?._nextI18Next) { - let { userConfig } = props.pageProps._nextI18Next - const { initialI18nStore, initialLocale } = props.pageProps._nextI18Next + // Memoize the instance and only re-initialize when either: + // 1. The route changes TODO: probably don't do this after #1049 is solved + // and http backend is used by default. + // 2. Router locale changes + const i18n: I18NextClient | null = useMemo(() => { + if (!locale || !_nextI18Next) return null + + let { userConfig } = _nextI18Next + const { initialI18nStore } = _nextI18Next if (userConfig === null && configOverride === null) { throw new Error('appWithTranslation was called without a next-i18next config') @@ -41,21 +50,17 @@ export const appWithTranslation = ( throw new Error('appWithTranslation was called without config.i18n') } - locale = initialLocale; - - ({ i18n } = createClient({ + return createClient({ ...createConfig({ ...userConfig, - lng: initialLocale, + lng: locale, }), - lng: initialLocale, + lng: locale, resources: initialI18nStore, - })) + }).i18n + }, [_nextI18Next, locale]) - useMemo(() => { - globalI18n = i18n - }, [i18n]) - } + globalI18n = i18n return i18n !== null ? (