Как управлять повторным рендерингом React с данными, поступающими из контекста

#reactjs #typescript #react-hooks #react-context

#reactjs #машинописный текст #реагирующие крючки #реагировать-контекст

Вопрос:

У меня есть провайдер, который получает уведомления о 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 amp;amp; actionType === ActionType.GET} message='Fetching...' />
                {
                    !executing amp;amp; !actionError amp;amp; data amp;amp; (
                        <div>
                            <IonList>
                                {
                                    groupBySender()
                                        .map(__data =>
                                            <div>
                                                <MessageListItem key={__data.sender} sender={__data.sender} messages={__data.messages} />
                                            </div>
                                        )
                                }
                            </IonList>
                        </div>
                    )
                }
            </IonContent>
        </IonPage>
    );
}
 

И что происходит, так это то, что каждый раз data , когда из контекста обновляется, эта страница повторно отображается, MessageListItem что также приводит к повторному рендерингу every .

Дело в том, что я хочу только повторно отображать те MessageListItem, на которые может повлиять изменение. Возможно ли это? Я думал об использовании useMemo и memo, но я создаю эти объекты динамически и не могу понять, как это сделать.

Возможно ли это вообще?

Я стремлюсь достичь чего-то вроде:

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

И когда я нажимаю на пользователя, я хочу, чтобы элемент расширялся и показывал эти сообщения. Но когда я получаю уведомление, все отображается, и элемент возвращается к тому, что он не расширяется.

Ответ №1:

Вы должны использовать React.useMemo, потому что каждый раз, когда ваш провайдер повторно отображает значение константы, получает новую ссылку, поэтому потребитель повторно передает.

измените это

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

Для

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

useMemo не будет пересчитывать новое значение до тех пор, пока не изменится состояние, get или put

Ответ №2:

У меня была точно такая же проблема, и похоже, что вы не можете контролировать повторный рендеринг дочерних элементов Провайдера. Решение, 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 , который ссылочно сравнивает реквизиты от одного рендеринга к следующему. Недостатком является то, что это может стать сложным, если реквизит содержит объекты, потому что они будут ссылочно отличаться, если вы каждый раз заново создаете реквизит

С помощью typescript вы можете определить метод equals для сравнения свойств объектов для определения равенства и сравнить их в параметре обратного вызова AreEqual memo, чтобы определить, следует ли повторно отображать ваш компонент. Возможно, это не то решение, на которое вы надеялись, но, надеюсь, это позволит обойти проблему, если вам действительно нужно повышение производительности.