如何使用来自上下文的数据控制 React Re-Renders
How to control React Re-Renders with data coming from context
我有一个从服务器接收 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
...
当我点击某个用户时,我希望该项目展开并显示这些消息。但是当我收到通知时,所有内容都会呈现并且项目恢复为未展开。
这是使用上下文的缺点,如 react 文档中所引用。 https://reactjs.org/docs/context.html#caveats
为了防止重新渲染列表项,您可以探索使用 React.memo 包装这些组件,它引用地比较一个渲染与下一个渲染的道具。缺点是如果道具包含对象,这会变得复杂,因为如果你每次重新创建道具,它们将在引用上不同
使用 typescript,您可以定义一个 equals 方法来比较对象的属性以确定是否相等,并在 memo 的 areEqual 回调参数中比较它们以确定您的组件是否应该重新渲染。这可能不是您希望的解决方案,但如果您确实需要提高性能,希望这能提供一种解决方法。
您应该使用 React.useMemo,因为每次您的提供者重新呈现常量时,value 都会得到一个新的引用,以便消费者重新呈现。
改变这个
const value = { ...state, get, put };
至
const value = React.useMemo(() => { ...state, get, put }, [state,get,put])
useMemo 不会重新计算新值,直到 state 或 get 或 put 改变
我遇到了完全相同的问题,看起来您无法控制提供程序子项的重新呈现。上面提到的 React.memo
的解决方案将不起作用,因为上下文值不是 props.
有一个越来越受欢迎的 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")
);
我有一个从服务器接收 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
...
当我点击某个用户时,我希望该项目展开并显示这些消息。但是当我收到通知时,所有内容都会呈现并且项目恢复为未展开。
这是使用上下文的缺点,如 react 文档中所引用。 https://reactjs.org/docs/context.html#caveats
为了防止重新渲染列表项,您可以探索使用 React.memo 包装这些组件,它引用地比较一个渲染与下一个渲染的道具。缺点是如果道具包含对象,这会变得复杂,因为如果你每次重新创建道具,它们将在引用上不同
使用 typescript,您可以定义一个 equals 方法来比较对象的属性以确定是否相等,并在 memo 的 areEqual 回调参数中比较它们以确定您的组件是否应该重新渲染。这可能不是您希望的解决方案,但如果您确实需要提高性能,希望这能提供一种解决方法。
您应该使用 React.useMemo,因为每次您的提供者重新呈现常量时,value 都会得到一个新的引用,以便消费者重新呈现。
改变这个
const value = { ...state, get, put };
至
const value = React.useMemo(() => { ...state, get, put }, [state,get,put])
useMemo 不会重新计算新值,直到 state 或 get 或 put 改变
我遇到了完全相同的问题,看起来您无法控制提供程序子项的重新呈现。上面提到的 React.memo
的解决方案将不起作用,因为上下文值不是 props.
有一个越来越受欢迎的 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")
);