React 上下文、性能?
今天我们来聊聊React context。它的作用有时会被误解,被错误地称为 mini-redux。首先,我们来了解一下它是什么,然后再讨论它的性能以及一些可用的解决方法。
它是什么?
我无法找到比文档中更好的定义:
Context 提供了一种通过组件树传递数据的方法,而无需在每个级别手动传递 props。
从概念上讲,你将数据放入React 上下文中,并通过Provider将其提供给 React 子树组件。然后,在这个子树中的所有组件中,你都可以通过Consumer获取数据。上下文中数据的每次变化都会通知每个消费者。
所以这里没有状态管理的概念,所以不要混淆,React context 不是一个迷你版的 redux。但是如果你将它与state
或结合使用,你可以模拟它reducer
。然而,你必须知道redux提供了一些功能,例如:
- 时间旅行
- 中间件
- 性能优化
如何使用 React 上下文
创建上下文
createContext
创建过程通过pulls from 方法完成React
。该方法仅接受可选的默认值作为参数:
const MyContext = React.createContext();
提供者
可以Provider
通过创建的上下文进行访问:
const MyProvider = MyContext.Provider;
所Provider
获得的组件具有以下属性:
- 值:您想要向子组件提供的值
- children:您想要提供价值的子项
<MyProvider value={valueToProvide}>
{children}
</MyProvider>
注意:创建一个
Provider
接受组件children
而不是直接将组件放入提供程序中,如下所示:
function App() {
const [data, setData] = useState(null);
return (
<MyContext.Provider value={{ data, setData }}>
<Panel>
<Title />
<Content />
</Panel>
</MyContext.Provider>
);
}
每次调用时都这样做setData
,它将渲染所有组件Title
,Content
即使Panel
它们不使用data
。
所以应该这样做:
function MyProvider({ children }) {
const [data, setData] = useState(null);
return (
<MyContext.Provider value={{ data, setData }}>
{children}
</MyContext.Provider>
);
}
function App() {
return (
<MyProvider>
<Panel>
<Title />
<Content />
</Panel>
</MyProvider>
);
}
消费者
一旦我们提供了一些数据,我们可能希望在子级中获取它。有两种方法可以获取它:
- 带钩
useContext
- 使用
Consumer
我们创建的上下文提供的组件
useContext
这是一个从上下文中获取值的钩子。你只需要将上下文传递给钩子即可:
const myValue = useContext(MyContext);
注意:您可能希望创建一个自定义钩子,
useMyContext
而不是直接导出上下文。这将允许您进行一些检查,以确保Provider
已正确添加:
const useMyContext = () => {
const value = useContext(MyContext);
if (!value) {
throw new Error(
"You have to add the Provider to make it work"
);
}
return value;
};
警告:如果在上下文中输入默认值,则检查将不起作用。
Consumer
成分
如前所述,创建的上下文也会导出一个Consumer
组件(如Provider
),然后您可以通过将函数作为子函数传递来获取值:
<MyContext.Consumer>
{(value) => {
// Render stuff
}
</MyContext.Consumer>
推荐和财产
将上下文放在最接近其使用的地方
建议将Provider
s 放在最靠近使用位置的地方。我的意思是,不要把所有Provider
s 都放在应用的顶部。这样可以帮助你深入代码库,实现关注点分离,并且应该会略微提高 React 的速度,因为这样就不必遍历所有组件树了。
注意:您需要将一些内容放在申请表
Provider
的顶部。例如:I18nProvider
,,,...SettingsProvider
UserProvider
这样做时,如果您将对象作为值传递(大多数情况下都是这种情况),则在父级重新渲染时可能会遇到一些性能问题。
例如,如果您有:
const MyContext = React.createContext();
function MyProvider({ children }) {
const [data, setData] = useState(null);
const onClick = (e) => {
// Whatever process
};
return (
<MyContext.Provider value={{ data, onClick }}>
{children}
</MyContext.Provider>
);
}
function ComponentUsingContext() {
const { onClick } = useContext(MyContext);
return <button onClick={onClick}>Click me</button>;
}
const MemoizedComponent = React.memo(ComponentUsingContext);
function App() {
const [counter, setCount] = useState(0);
return (
<div>
<button
onClick={() => setCounter((prev) => prev + 1)}
>
Increment counter: counter
</button>
<MyProvider>
<MemoizedComponent />
</MyProvider>
</div>
);
}
在这种情况下,当我们增加计数器时,MemoizedComponent
即使它被记忆,也会重新渲染,因为上下文中的值发生了变化。
在这种情况下,解决方案是记住该值:
const value = useMemo(() => {
const onClick = (e) => {
// Whatever process
};
return {
data,
onClick,
};
}, [data]);
好了,MemoizedComponent
增加计数器时不再渲染。
警告:仅当使用上下文的组件被记忆时,记忆值才有用。否则,它将什么也不做,并继续重新渲染,因为父组件会重新渲染(在上一种情况下,父组件是 App)。
嵌套提供程序
可以在相同的上下文中使用嵌套的 Provider。例如,在react-router
实现中使用,请参阅我的文章。
在这种情况下,消费者将获得距离他们最近的提供者的值。
const MyContext = React.createContext();
export default function App() {
return (
<MyContext.Provider value="parent">
<ParentSubscriber />
<MyContext.Provider value="nested">
<NestedSubscriber />
</MyContext.Provider>
</MyContext.Provider>
);
}
function ParentSubscriber() {
const value = useContext(MyContext);
return <p>The value in ParentSubscriber is: {value}</p>;
}
function NestedSubscriber() {
const value = useContext(MyContext);
return <p>The value in NestedSubscriber is: {value}</p>;
}
在前面的例子中,ParentSubscriber
将获得值parent
,而在另一端NestedSubscriber
将获得nested
。
表现
为了讨论性能,我们将制作一个具有一些功能的小型音乐应用程序:
- 能够看到我们的朋友正在听什么
- 表演音乐
- 显示当前音乐
重要提示:请勿评判 React context 功能的使用。我知道,在实际应用中,我们可能不会使用此类功能。
朋友和音乐功能
规格:
- 好友功能包括每 2 秒获取一次虚假 API,该 API 将返回以下类型的对象数组:
type Friend = {
username: string;
currentMusic: string;
}
- 音乐功能将仅获取一次可用的音乐并返回:
type Music = {
uuid: string; // A unique id
artist: string;
songName: string;
year: number;
}
好的。让我们来实现它。
我只是想把所有这些数据放在同一个context中,然后提供给我的应用程序。
让我们实现 Context 和 Provider:
import React, {
useContext,
useEffect,
useState,
} from "react";
const AppContext = React.createContext();
// Simulate a call to a musics API with 300ms "lag"
function fetchMusics() {
return new Promise((resolve) =>
setTimeout(
() =>
resolve([
{
uuid: "13dbdc18-1599-4a4d-b802-5128460a4aab",
artist: "Justin Timberlake",
songName: "Cry me a river",
year: 2002,
},
]),
300
)
);
}
// Simulate a call to a friends API with 300ms "lag"
function fetchFriends() {
return new Promise((resolve) =>
setTimeout(() => {
resolve([
{
username: "Rainbow",
currentMusic:
"Justin Timberlake - Cry me a river",
},
]);
}, 300)
);
}
export const useAppContext = () => useContext(AppContext);
export default function AppProvider({ children }) {
const [friends, setFriends] = useState([]);
const [musics, setMusics] = useState([]);
useEffect(() => {
fetchMusics().then(setMusics);
}, []);
useEffect(() => {
// Let's poll friends every 2sec
const intervalId = setInterval(
() => fetchFriends().then(setFriends),
2000
);
return () => clearInterval(intervalId);
}, []);
return (
<AppContext.Provider value={{ friends, musics }}>
{children}
</AppContext.Provider>
);
}
Friends
现在我们来看看and组件的实现Musics
。没什么复杂的:
function Friends() {
const { friends } = useAppContext();
console.log("Render Friends");
return (
<div>
<h1>Friends</h1>
<ul>
{friends.map(({ username, currentMusic }) => (
<li key={username}>
{username} listening {currentMusic}
</li>
))}
</ul>
</div>
);
}
和:
function Musics() {
const { musics } = useAppContext();
console.log("Render Musics");
return (
<div>
<h1>My musics</h1>
<ul>
{musics.map(({ uuid, artist, songName, year }) => (
<li key={uuid}>
{artist} - {songName} ({year})
</li>
))}
</ul>
</div>
);
}
现在,我问你一个问题。你知道控制台里会渲染/打印什么吗?
是的, 和Friends
都会Musics
大约每 2 秒渲染一次。为什么?
你还记得我告诉过你,如果提供的值发生变化,每个消费者都会被触发,即使他们使用了该值中不变的部分。
这种情况只会Musics
从musics
上下文中提取不变的 。
您可以在以下codesandbox中看到它:
这就是为什么我建议在不同的环境中按业务领域分离数据。
在我们的例子中,我将创建两个单独的上下文FriendsContext
和MusicContext
。
您可以在此处看到实现:
当前正在听的音乐
现在我们希望能够从列表中选择一首音乐并收听。
我将创建一个新的上下文来存储currentMusic
:
import React, { useContext, useState } from "react";
const CurrentMusicContext = React.createContext();
export const useCurrentMusicContext = () =>
useContext(CurrentMusicContext);
export default function CurrentMusicProvider({ children }) {
const [currentMusic, setCurrentMusic] =
useState(undefined);
return (
<CurrentMusicContext.Provider
value={{ currentMusic, setCurrentMusic }}
>
{children}
</CurrentMusicContext.Provider>
);
}
我在组件中添加了一个按钮Musics
来收听相关音乐:
function MyMusics() {
const musics = useMusicContext();
const { setCurrentMusic } = useCurrentMusicContext();
console.log("Render MyMusics");
return (
<div>
<h1>My musics</h1>
<ul>
{musics.map((music) => (
<li key={music.uuid}>
{getFormattedSong(music)}{" "}
<button onClick={() => setCurrentMusic(music)}>
Listen
</button>
</li>
))}
</ul>
</div>
);
}
该CurrentMusic
组件非常简单:
function CurrentMusic() {
const { currentMusic } = useMusicContext();
console.log("Render CurrentMusic");
return (
<div>
<h1>Currently listening</h1>
{currentMusic ? (
<strong>{getFormattedSong(currentMusic)}</strong>
) : (
"You're not listening a music"
)}
</div>
);
}
好的,当您选择听一首新音乐时会发生什么?
目前,和MyMusics
都会CurrentMusic
渲染。因为当currentMusic
发生变化时,会有一个新对象传递给提供程序。
分离dynamic
和static
数据
一种策略是将动态数据和静态数据分离在两个不同的上下文CurrentMusicDynamicContext
中CurrentMusicStaticContext
:
import React, { useContext, useState } from "react";
const CurrentMusicStaticContext = React.createContext();
const CurrentMusicDynamicContext = React.createContext();
export const useCurrentMusicStaticContext = () =>
useContext(CurrentMusicStaticContext);
export const useCurrentMusicDynamicContext = () =>
useContext(CurrentMusicDynamicContext);
export default function CurrentMusicProvider({ children }) {
const [currentMusic, setCurrentMusic] =
useState(undefined);
return (
<CurrentMusicDynamicContext.Provider
value={currentMusic}
>
<CurrentMusicStaticContext.Provider
value={setCurrentMusic}
>
{children}
</CurrentMusicStaticContext.Provider>
</CurrentMusicDynamicContext.Provider>
);
}
现在我们开始吧。只需使用正确的钩子从上下文中获取值。
注意:正如您所看到的,它确实使代码变得复杂,这就是为什么不要尝试修复不存在的性能问题。
use-context-selector
第二种解决方案是使用dai-shi开发的库use-context-selector
。我写了一篇关于其实现的文章。
它将包装 React 的原生上下文 API,让你可以访问多个钩子,只有当 store 中选定的值发生变化时,这些钩子才会重新渲染你的组件。
createContext
原理很简单,你可以通过库提供的函数创建上下文。
然后使用 从中获取数据useContextSelector
。API 如下:
useContextSelector(CreatedContext, valueSelectorFunction)
例如如果我想获得currentMusic
:
const currentMusic = useContextSelector(
CurrentMusicContext,
(v) => v.currentMusic
);
为了不暴露上下文,我做了一个钩子:
export const useCurrentMusicContext = (selector) =>
useContextSelector(CurrentMusicContext, selector);
就这样。代码如下:
结论
我们已经了解了如何使用 React 上下文,以及你可能遇到的性能问题。
但一如既往,不要过早进行优化。等到真正出现问题时再考虑。
正如你所见,优化会使你的代码可读性下降,代码更加冗长。
只需尝试将不同的业务逻辑分离到不同的上下文中,并将提供程序尽可能靠近需要的地方,以使代码更清晰。不要将所有内容都放在应用程序的顶部。
如果由于上下文而遇到真正的性能问题,你可以:
- 在不同的上下文中分离动态和静态数据
useMemo
如果父组件重新渲染导致值发生变化,则该值会被重置。但你必须memo
在使用上下文(或父组件)的组件上放置一些值,否则它不会执行任何操作。- 使用这个
use-context-selector
库来解决 context 的缺陷。也许有一天会原生地实现,正如你在这个已打开的 PRreact
中看到的那样。 - 我们在本文中没有讨论的另一种策略是不要使用 React 上下文,而是使用原子状态管理库,例如:
jotai
,,recoil
...
请随时发表评论,如果您想了解更多信息,您可以在Twitter上关注我或访问我的网站。
鏂囩珷鏉ユ簮锛�https://dev.to/romaintrotard/react-context-performance-5832