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