【问题标题】:How to update a React component when mutating an object?改变对象时如何更新 React 组件?
【发布时间】:2022-10-04 22:02:04
【问题描述】:

我将react-localization 用于i18n,它使用带有setLanguage 方法的对象。我正在尝试为它构建一个语言切换器,但是当我更改语言时,组件不会使用更新的对象重新渲染。

我试过useEffectuseCallbackuseRef。唯一可行的是useCallback,并从我的自定义钩子中导出回调,然后在组件呈现时调用它,我认为这非常丑陋和不正确。

从钩子中改变对象并重新渲染使用它的组件的正确方法是什么?

这是我创建的用于测试的 CodeSandbox:https://codesandbox.io/s/react-localization-context-wfo2p2

翻译.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 };

【问题讨论】:

    标签: javascript reactjs react-hooks internationalization react-localization


    【解决方案1】:

    你实际上比你意识到的更接近。与其将回调 cb 返回到 UI 以作为无意的副作用进行调用,不如创建/移动一些状态到钩子中。

    TranslationProvider 存储当前全局 language 状态,但 useTranslation 钩子应该/可以存储它自己对 LocalizedStrings 对象的引用,并将翻译后的消息存储在通过 useEffect 钩子更新的本地状态中.

    例子:

    const useTranslation = (_strings, key) => {
      // Access global language state and setter
      const { language, setLanguage } = useContext(TranslationContext);
    
      // Local message state for translations
      const [message, setMessage] = useState();
    
      // LocalizedStrings instance ref
      const stringsRef = useRef(_strings && new LocalizedStrings(_strings));
    
      // Side-effect to translate to current language
      // Update message state to trigger rerender
      useEffect(() => {
        stringsRef.current?.setLanguage(language);
        setMessage(stringsRef.current?.message);
      }, [language]);
    
      // Expose context value and current translated message
      return { language, message, setLanguage };
    };
    

    用法:

    const strings = {
      en: { message: "Test message in English" },
      fr: { message: "Message de test en Français" }
    };
    
    const Test = () => {
      const { message } = useTranslation(strings, "Test");
    
      useEffect(() => {
        console.log("[RENDER <Test/>", strings);
      });
    
      return <div>{message}</div>;
    };
    

    ...

    const _strings = {
      en: { message: "Test message #2 in English" },
      fr: { message: "Message de test #2 en Français" }
    };
    
    const Test2 = () => {
      const { language, message } = useTranslation(_strings, "Test2");
    
      useEffect(() => {
        console.log("[RENDER] <Test2/>", _strings);
      })
    
      return (
        <div>
          {message} {language}
        </div>
      );
    };
    

    【讨论】:

    • 感谢您的建议!这样就避免了实际上到处都需要导入本地化包,你可以从钩子中返回当前对象引用,只要你在useEffect中调用setState,组件就会重新渲染。
    • @EthanO。您排队以触发重新渲染的“状态”需要是动态的,因此 React 不会 bail out of the state update... 在这里使用新的翻译消息才有意义,但是翻转布尔值或增加计数将完成同样的事情。
    • @EthanO。欢迎,乐于助人。干杯和好运!
    【解决方案2】:

    language 状态位于父上下文提供程序组件中,因此它将重新渲染受影响的子组件,因此您不需要像 useEffect 这样的额外逻辑。

    老实说,我尝试直接在自定义钩子中调用该方法并且它有效;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>;
    };
    

    如果您只想在 language 状态更新上运行本地化,我们将需要 useEffect,并确保组件在 useEffect(在组件渲染后运行)运行后更新,因此我们创建一个状态我们在组件/自定义钩子中更新。

    您可以使用布尔状态来翻转每次更新,但您也可以通过解构将本地化对象的属性保存到状态。然后使用该状态进行渲染。

    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>;
    };
    

    【讨论】:

    • 好吧,我承认这行得通!谢谢你的建议。但是,我不敢相信这会被认为是一种非常 React 的方式。你怎么看?
    • 查看我对语言状态的编辑。您正在正确使用上下文和状态,因此以 imo 的方式这样做很好。如果你想让它更干净一点,你可以将 state 与 locale updater 分开。它会有点多余,但也更清晰。自定义钩子本质上就像将代码块复制到组件中一样,所以我认为将上下文获取和本地更新抽象到自定义钩子中是很好的。为什么你认为它不是很 React?
    • 我认为它不是很 React,因为钩子用于在每次呈现对象时改变对象,而不是仅在更新语言时。只是感觉它应该是useEffect 的一部分并使用某种ref。你怎么看?如果我有很多使用相同机制的组件,性能方面会发生什么?
    • 我更新了我的答案以解决这个问题。我认为不需要使用ref,因为我们没有使用用旧状态初始化的回调,而是从本地化对象设置新状态,而本地化对象本身就在组件之外。
    猜你喜欢
    • 2021-04-24
    • 1970-01-01
    • 2021-08-11
    • 2018-10-28
    • 2017-11-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多