Vivere
Vivere

Reputation: 2280

How to control React Re-Renders with data coming from context

I have a provider which receives notifications on WebSocket from the server.

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

Then in my Component, I use this context like this:

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

And what happens, is that every time data from context gets updated, this page re-renders, causing every MessageListItem to re-render as well.

The thing is that I want only to re-render those MessageListItem with whom the change could affect. Is this possible? I thought of using useMemo and memo, but I'm creating these objects dynamically and I can't figure it out how to do it.

Is this even possible?

I strive to achieve something like:

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

And when I click on a user, I want the item to expand and show those messages. But when I receive a notification, everything renders and the item reverts to not being expanded.

Upvotes: 0

Views: 641

Answers (3)

Almaju
Almaju

Reputation: 1383

I had the exact same problem and it looks like that you can't control the re-render of children of the Provider. The solution with React.memo mentioned above won't work because context values are not props.

There is a npm package growing in popularity that addresses this issue: https://www.npmjs.com/package/react-tracked

You can read this article to get more options: https://blog.axlight.com/posts/4-options-to-prevent-extra-rerenders-with-react-context/

I personally ended up using hooks with event listeners and local storage to get reactivity in the right components to have a global state without having to use useContext.

Something like that:

    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")
    );

Upvotes: 1

ARZMI Imad
ARZMI Imad

Reputation: 980

You should use React.useMemo because each time your provider re-render the constants value gets a new reference so the consumer re-rendred.

change this

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

to

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

useMemo will not recalculate a new value until state or get or put changed

Upvotes: 2

Doug Hill
Doug Hill

Reputation: 156

This is a drawback to using contexts, as referenced here in the react docs. https://reactjs.org/docs/context.html#caveats

To prevent the list items from re-rendering, you could explore wrapping those components with React.memo, which referentially compares props from one render to the next. The drawback is this can become complex if the props contain objects, because they will be referentially different if you re-create the props each time

With typescript, you could define an equals method to compare the properties of objects to decide equality and compare them in the areEqual callback parameter of memo to determine if your component should re-render. This may not be the solution you were hoping for but hopefully this provides a work-around if you really need the performance boost.

Upvotes: 0

Related Questions