I am working on a project that uses Remix with Shopify integration and have faced a hydration error when using useTranslation from react-i18next. The default language works fine, but specifying a language namespace causes issues. Here’s the detailed problem:
Problem
When I use the useTranslation hook with a specific namespace, I encounter the following error:
Hydration failed because the initial UI does not match what was rendered on the server.
This issue only arises when I use useTranslation("namespace")
with a specified namespace. If I use the default language, there is no hydration error.
I have been debugging this issue for a while and would appreciate any insights or solutions to resolve this error. How can I ensure that the initial UI matches what was rendered on the server to avoid this hydration error?
Short answer: use export let handle = { i18n: "NamespaceName" };
in all routes with non-default namespaces
Long answer:
The problem was that the server-side rendering (SSR) used all available namespaces because it could directly read the files and didn't encounter any translation issues. However, the frontend always loaded only the defaultNS
by default. During hydration, the i18n library realized it lacked the necessary languages, sent an asynchronous request, but since hydrateRoot
did not wait for the language packages to load, it received a tree without the necessary components, different from what SSR returned, and hence we saw the error.
The remix-i18next
library has a built-in mechanism to support this workflow. The solution involves ensuring each route that uses a specific namespace declares it using the following code:
// This tells Remix to load the "home" namespace
export let handle = { i18n: "home" };
export default function HomePage() {
let { t } = useTranslation("home");
return <h1>{t("welcome_message")}</h1>;
}
During SSR, Remix collects all the pages for which handle is declared into an array, sends this array along with the rendered page from the server. Then, the function
// This function detects the namespaces your routes rendered while SSR use
ns: getInitialNamespaces(),
receives the namespaces before the first hydration, loads the necessary namespaces, and then everything hydrates correctly.