【问题标题】:How to control React Re-Renders with data coming from context如何使用来自上下文的数据控制 React Re-Renders
【发布时间】:2021-02-02 21:06:23
【问题描述】:

我有一个提供者从服务器接收关于 WebSocket 的通知。

export const MessageProvider: React.FC<ProviderProps> = ({ children }) => {
    const [state, dispatch] = useReducer(reducer, messageInitialState);

    useEffect(() => {
        let cancelled: boolean = false;
        __get(cancelled, undefined);
        return () => {
            cancelled = true;
        }
    }, []);
    useEffect(__wsEffect, []);

    const get = useCallback<(cancelled: boolean, userId: number | undefined) => Promise<void>>(__get, []);
    const put = useCallback<(message: Message) => Promise<void>>(__put, []);

    const value = { ...state, get, put };
    return (
        <MessageContext.Provider value={value}>
            {children}
        </MessageContext.Provider>
    )

    ...

    function __wsEffect() {
        log('{__wsEffect}', 'Connecting');
        let cancelled: boolean = false;
        const ws = newWebSocket(async (payload) => {
            if (cancelled) {
                return;
            }

            const message: Message = payload as Message;
            await storageSet(message, __key(message.id));
            dispatch({ actionState: ActionState.SUCCEEDED, actionType: ActionType.SAVE, data: message });
        });

        return () => {
            log('{__wsEffect} Disconnecting');
            cancelled = true;
            ws.close();
        }
    }
}

然后在我的组件中,我像这样使用这个上下文:

export const MessageListPage: React.FC<RouteComponentProps> = ({ history }) => {
    const { data, executing, actionType, actionError, get, put } = useContext(MessageContext);

    ...

    return (
        <IonPage id='MessageListPage'>
            <IonContent fullscreen>
                <IonLoading isOpen={executing && actionType === ActionType.GET} message='Fetching...' />
                {
                    !executing && !actionError && data && (
                        <div>
                            <IonList>
                                {
                                    groupBySender()
                                        .map(__data =>
                                            <div>
                                                <MessageListItem key={__data.sender} sender={__data.sender} messages={__data.messages} />
                                            </div>
                                        )
                                }
                            </IonList>
                        </div>
                    )
                }
            </IonContent>
        </IonPage>
    );
}

然后发生的情况是,每次更新上下文中的data 时,此页面都会重新呈现,从而导致每个MessageListItem 也重新呈现。

问题是我只想重新渲染那些更改可能会影响的 MessageListItem。这可能吗?我想过使用 useMemo 和 memo,但我正在动态创建这些对象,我不知道该怎么做。

这可能吗?

我努力实现以下目标:

user a - x unread messages
user b - y unread messages
...

当我点击一个用户时,我希望该项目展开并显示这些消息。但是当我收到通知时,所有内容都会呈现并且项目恢复为未展开。

【问题讨论】:

    标签: reactjs typescript react-hooks react-context


    【解决方案1】:

    您应该使用 React.useMemo,因为每次您的提供者重新渲染常量 value 都会获得一个新的引用,以便消费者重新渲染。

    改变这个

    const value = { ...state, get, put };
    

    const value = React.useMemo(() => { ...state, get, put }, [state,get,put])
    

    useMemo 不会重新计算新值,直到 stategetput 更改

    【讨论】:

      【解决方案2】:

      我遇到了完全相同的问题,看起来您无法控制 Provider 子项的重新渲染。上面提到的React.memo 的解决方案将不起作用,因为上下文值不是道具。

      有一个越来越受欢迎的 npm 包可以解决这个问题:https://www.npmjs.com/package/react-tracked

      您可以阅读这篇文章以获得更多选择:https://blog.axlight.com/posts/4-options-to-prevent-extra-rerenders-with-react-context/

      我个人最终使用带有事件侦听器和本地存储的钩子来获得正确组件的响应性,从而获得全局状态,而无需使用 useContext

      类似的东西:

          import React, { createContext, useEffect, useState } from "react";
          import ReactDOM from "react-dom";
      
          type Settings = {
              foo?: string;
          }
      
          const useSettings = () => {
              let initialSettings: Settings = {
                  foo: undefined
              };
              try {
                  initialSettings = JSON.parse(localStorage.getItem("settings")) as Settings;
              } catch(e) {};
      
              const [settings, setSettings] = useState<Settings>(initialSettings);
      
              useEffect(() => {
                  const listener = () => {
                      if (localStorage.getItem("settings") !== JSON.stringify(settings)) {
                          try {
                              setSettings(JSON.parse(localStorage.getItem("settings")) as Settings);
                          } catch(e) {};
                      }
                  }
      
                  window.addEventListener("storage", listener);
                  return () => window.removeEventListener("storage", listener);
              }, []); 
      
              const setSettingsStorage = (newSettings: Settings) => {
                  localStorage.setItem("settings", JSON.stringify(newSettings));
              }
      
              return [settings, setSettingsStorage];
          }
      
          const Content = () => {
              const [settings, setSettings] = useSettings();
      
              return <>Hello</>;
          }
      
          ReactDOM.render(
              <Content settings={{foo: "bar"}} />,
              document.getElementById("root")
          );
      

      【讨论】:

        【解决方案3】:

        这是使用上下文的一个缺点,如 react 文档中所引用的。 https://reactjs.org/docs/context.html#caveats

        为了防止列表项重新渲染,您可以探索使用 React.memo 包装这些组件,它可以参考比较道具从一个渲染到下一个渲染。缺点是如果 props 包含对象,这可能会变得复杂,因为如果您每次都重新创建 props,它们的引用会有所不同

        使用 typescript,您可以定义一个 equals 方法来比较对象的属性以确定相等性,并在 memo 的 areEqual 回调参数中比较它们以确定您的组件是否应该重新渲染。这可能不是您希望的解决方案,但如果您确实需要性能提升,希望这可以提供一种解决方法。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2021-11-21
          • 2021-03-18
          • 2020-10-09
          • 2020-12-17
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多