Search code examples
javascriptreactjsreact-hooksinternationalizationreact-localization

How to update a React component when mutating an object?


I'm using react-localization for i18n that uses an object with a setLanguage method. I'm trying to build a language switcher for it, but when I change the language, the component doesn't re-render with the updated object.

I've tried with useEffect, useCallback, useRef. The only thing that works is useCallback, and exporting the callback from my custom hook, then calling it when the component renders, which I think is very ugly and incorrect.

What's the proper way to mutate an object from a hook and have the component that uses it be re-rendered?

Here's the CodeSandbox I created to test this: https://codesandbox.io/s/react-localization-context-wfo2p2

translation.js

import {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useRef,
  useState
} from "react";

const TranslationContext = createContext({
  language: "fr",
  setLanguage: () => null
});

const TranslationProvider = ({ children }) => {
  const [language, setLanguage] = useState("fr");

  return (
    <TranslationContext.Provider value={{ language, setLanguage }}>
      {children}
    </TranslationContext.Provider>
  );
};

const useTranslation = (_strings, key) => {
  const { language, setLanguage } = useContext(TranslationContext);

  const ref = useRef(_strings);
  const cb = useCallback(() => _strings.setLanguage(language), [
    language,
    _strings
  ]);

  useEffect(() => {
    ref?.current?.setLanguage(language);
  }, [language]);

  return { language, cb, setLanguage };
};

export { useTranslation, TranslationProvider, TranslationContext };

Solution

  • The language state is in the parent context provider component, so it'll re-render affected child components, so you don't need extra logic like useEffect.

    I'll be honest, I tried to call the method directly in the custom hook and it worked ;P.

    // translation.js
    const useTranslation = (_strings, key) => {
      const { language, setLanguage } = useContext(TranslationContext);
      _strings?.setLanguage(language);
      return { language, setLanguage };
    };
    
    // Test.js
    const Test = () => {
      useTranslation(strings, "Test");
      return <div>{strings.message}</div>;
    };
    

    Edit react-localization context (forked)

    If you want to run the localization only on language state updates, we'll need useEffect, and to make sure the components update after useEffect, which runs after the component's rendered, runs, so we create a state that we update in the component/custom hook.

    You can use a boolean state that you flip each update, but you can also save the properties of the localized object to the state by destructuring it. Then use that state for the rendering.

    const useTranslation = (_strings, key) => {
      const { language } = useContext(TranslationContext);
      const [fields, setFields] = useState({..._strings});
      
      useEffect(() => {
        _strings.setLanguage(language)
        setFields({..._strings});
      }, [language]);
    
      return fields;
    };
    
    const Test = () => {
      const fields = useTranslation(_strings, "Test");
      return <div>{fields.message}</div>;
    };
    

    Edit react-localization context (forked)