| 节选自Morten Barklund的《React Quickly,2ndEdition》。 本节选探讨了React Context的使用。 如果你是一个React开发者或者想了解更多关于React的信息,请阅读它。 |
打75折 React Quickly, 2第2版版在manning.com结账时,在折扣代码框中输入fccmardan2,即可获得25%的折扣。
让我们跳进去,建立一个解决真实世界问题的应用程序。我们要做一个用户仪表板,也就是你登录到一个应用程序后看到的屏幕。这个仪表盘有一个欢迎词,欢迎你的名字,它在左上角也有一个按钮,显示你的名字并链接到你的设置页面。这里的诀窍是,这个名字是动态的,将由后端返回给我们。
最终的结果应该是像图1那样。
图1 我们的用户仪表盘的最终结果。在这个截图中,用户的名字显示了两次,这就是问题的核心所在。
让我们把这个问题分解成几个部分。我们希望顶部菜单是标题的一部分,中央欢迎页只是我们的应用程序可以显示的众多页面之一。我们知道我们在未来会添加更多的东西,所以让我们添加一些额外的层来做准备。我们将使用如图2所示的组件方法。
图2 我们的组件结构,包括名称所需的必要占位符。请注意,"name "属性被使用了两次,但仍然没有作为一个属性传递到任何地方。
然而,正如你在图2中看到的,我们没有显示我们是如何从组件树的最顶端得到这个名字的,在那里它被传递到Dashboard组件中,一直到最后需要显示它的两个小组件。
如果只使用常规的React属性,我们就需要将该属性通过每个组件传递给需要它的组件。如果我们这样做,就会像图3那样。
图3 如果我们把名字传递给每个需要传递的组件,我们的组件结构。总共有五个组件需要name属性,但其中只有两个组件真正显示它
但是请注意,在这个组件树中,我们将name属性传递给Header和Main组件。这两个组件本身实际上都不需要这个属性。我们必须把这个属性传递给这两个组件的唯一原因是,它们可以把这个属性转发给另一个组件。
尽管如此,这当然是可行的,我们可以实现这一点,所以让我们在清单1中这样做。
清单1 带有大量名称道具的仪表板。
const BUTTON_STYLE = {
display: 'inline-block',
padding: '4px 10px',
background: 'transparent',
border: '0',
};
const HEADER_STYLE = {
display: 'flex',
justifyContent: 'flex-end',
borderBottom: '1px solid',
};
function Button({ children }) {
return <button style={BUTTON_STYLE}>{children}</button>;
}
function UserButton({ name }) {
return <Button>👤 {name}</Button>; #A
}
function Header({ name }) { #B
return (
<header style={HEADER_STYLE}>
<Button>Home</Button>
<Button>Groups</Button>
<Button>Profile</Button>
<UserButton name={name} /> #C
</header>
);
}
function Welcome({ name }) {
return <section><h1>Welcome, {name}!</h1></section>;
}
function Main({ name }) { #B
return <main><Welcome name={name} /></main>; #C
}
function Dashboard({ name }) {
return <><Header name={name} /><Main name={name} /></>;
}
#A 你知道你可以在React中直接使用emojis吗?你可以!
#B 这里我们把一个名字属性传递给一个组件,而这个组件本身实际上并不需要使用这个属性
#C 该组件被传递该属性只是为了能够将其传递给另一个组件。
源代码
你可以从github.com/rq2e/rq2e/t…上下载上述例子的源代码,或者你可以通过运行下面的代码段来初始化一个新的React项目,并预先填入这个例子。
npx create-react-app rq10-dashboard-props --template rq10-dashboard-props
这是一个合理的方法,而且很有效。如果你在浏览器中打开这个,你看到的正是我们在图1中想要的东西。
React上下文
那些被传递给组件的属性,只是为了让它们被传递给另一个组件,这看起来不像是好的软件设计。一定有一个更好的方法。如果我们能有一个封装了许多组件的存储对象,当它们需要数据时,可以将其反馈给所有的子组件,那会怎么样?而且它们应该能够这样做而不需要任何额外的属性传递。
恭喜你,我们刚刚发明了React Context。上下文正是这样做的。它用一个值包装了一些组件,所有的子代组件都可以访问这个值,而根本不需要通过属性。
支持物钻取
给一个组件添加属性,唯一的目的是让该组件将这些属性传递给其他组件,而这些组件又可能只是为了让这些组件将这些属性传递给另一层的组件,这种做法被称为道具钻取。你在许多层组件中钻取你的属性,因为你需要把它从外面弄到里面。
在大型代码库中,道具钻取会很快成为一个问题,而React Context是解决这个问题的最好工具之一。如果没有适当的设计模式,例如使用上下文提供者,你可能会在一些组件上出现几十个属性,只是因为需要在组件树上进一步添加这些属性。
这显然是糟糕的软件设计,也是React Context如此受欢迎的原因之一。
React的上下文由两部分组成。它需要一个提供者,包含你想传递给任何后代组件的值;它还需要一个消费者,你在每个想访问所提供的值的后代组件中使用。
上下文提供者是一个相当简单的React组件。消费者可以用两种不同的方式创建。要么作为一个带有函数的组件作为一个子节点,要么作为一个useContext 钩子。前一种方式相当不寻常,而且很麻烦,所以我们根本就不打算使用这种方式。在现代的React应用程序中,你可能根本就看不到这种用法。它只在基于类的代码库中有用,在那里你不能使用钩子的变体。
从本质上讲,使用一个上下文看起来像图4。
图4 使用钩子的提供者和使用useContext钩子的消费者。
我们在这里需要两块React API。首先,我们需要createContext来定义上下文,我们将把它存储在一个变量中。这是一个在任何组件之外创建的变量,与其他组件生活在相同的地方,因此可以像其他组件一样被引用。
另一部分是useContext钩子。这个钩子接受一个对上下文的引用,并返回当前的上下文值。
让我们继续往前走,将NameContext添加到我们前面的仪表盘应用程序的组件树中。我们在图5中这样做。
图5 仪表板应用程序的组件树,周围有一个上下文。
这就是字面上的全部内容。我们可以继续在清单2中实现这一点。
清单2 带有上下文的仪表盘。
import { createContext, useContext } from 'react'; #A
const BUTTON_STYLE = {
display: 'inline-block',
padding: '4px 10px',
background: 'transparent',
border: '0',
};
const HEADER_STYLE = {
display: 'flex',
justifyContent: 'flex-end',
borderBottom: '1px solid',
};
const NameContext = createContext(); #B
function Button({ children }) {
return <button style={BUTTON_STYLE}>{children}</button>;
}
function UserButton() { #C
const name = useContext(NameContext); #D
return <Button>👤 {name}</Button>;
}
function Header() { #C
return (
<header style={HEADER_STYLE}>
<Button>Home</Button>
<Button>Groups</Button>
<Button>Profile</Button>
<UserButton />
</header>
);
}
function Welcome() { #C
const name = useContext(NameContext); #D
return <section><h1>Welcome, {name}!</h1></section>;
}
function Main() { #C
return <main><Welcome /></main>;
}
function Dashboard({ name }) {
return (
<NameContext.Provider value={name}> #E
<Header />
<Main />
</NameContext.Provider>
);
}
#A 我们从React包中导入两个函数
#B 上下文是在全局范围内创建的,所以我们可以从任何地方访问它。
#C 我们的很多组件根本就不需要任何属性了。
#D 两个需要访问名字的组件可以通过使用useContext挂接到上下文来实现。
#E 在仪表盘组件中,我们确保将整个树包在一个以名字为上下文值的上下文提供者中。
源代码
你可以从github.com/rq2e/rq2e/t…上下载上述例子的源代码,或者你可以通过运行下面的代码来初始化一个预先填充了这个例子的新React项目。
npx create-react-app rq10-dashboard-context --template rq10-dashboard-context
我们得到的结果和之前的完全一样,但在我们看来,数据流要好得多。
上下文钩子是有状态的
使用上下文来存储整个应用程序中使用的静态值无疑是很好的,但更棒的是,我们也可以在那里存储动态信息。useContext 钩子是有状态的,所以如果上下文值发生变化,useContext 钩子将导致使用它的组件自动重新渲染。
让我们想象一下同样的仪表盘,但这次你是一个管理员,希望能够看到数据库中任何用户的仪表盘是什么样的。作为一个管理员,你有一个可以看到仪表盘的用户的下拉菜单。我们将像图6那样实现这一点,其中仪表盘组件与之前的组件相同(我们只是不显示它的所有子组件以节省空间)。
图6 管理仪表板允许用户选择查看哪个用户的仪表板。管理仪表板包括一个选择框和普通的用户仪表板。
我们将使用一个简单的选择元素,让用户在系统中的三个用户之间进行选择。Alice, Bob, 和 Carol。我们可以使用一个简单的useState来记住所选的用户,并根据需要将其传递给组件。让我们用清单3中的这个新的管理员仪表板来扩展前面的例子。
清单3 管理员仪表盘。
import { useState, createContext, useContext } from 'react'; #A
const BUTTON_STYLE = {
display: 'inline-block',
padding: '4px 10px',
background: 'transparent',
border: '0',
};
const HEADER_STYLE = {
display: 'flex',
justifyContent: 'flex-end',
borderBottom: '1px solid',
};
const NameContext = createContext();
function Button({ children }) {
return <button style={BUTTON_STYLE}>{children}</button>;
}
function UserButton() {
const name = useContext(NameContext);
return <Button>👤 {name}</Button>;
}
function Header() {
return (
<header style={HEADER_STYLE}>
<Button>Home</Button>
<Button>Groups</Button>
<Button>Profile</Button>
<UserButton />
</header>
);
}
function Welcome() {
const name = useContext(NameContext);
return <section><h1>Welcome, {name}!</h1></section>;
}
function Main() {
return <main><Welcome /></main>;
}
function Dashboard({ name }) { #B
return (
<NameContext.Provider value={name}>
<Header />
<Main />
</NameContext.Provider>
);
}
function AdminDashboard() {
const [user, setUser] = useState('Alice'); #C
return (
<>
<select value={user} onChange={(evt) => setUser(evt.target.value)}> #D
<option>Alice</option>
<option>Bob</option>
<option>Carol</option>
</select>
<Dashboard name={user} /> #E
</>
);
}
#A 我们还需要导入useState钩子
#B 仪表盘组件内部的一切都和以前一样
#C 我们创建一个简单的状态,默认为Alice
#D 我们使用一个受控的选择元素来选择一个用户
#E 我们将当前选择的用户传递给仪表盘组件。
源代码
你可以从github.com/rq2e/rq2e/t…上下载上述例子的源代码,或者你可以通过运行下面的代码来初始化一个新的React项目,并预先填入这个例子。
npx create-react-app rq10-dashboard-admin --template rq10-dashboard-admin
如果我们在浏览器中试一下,它看起来像这样的图7。继续前进,从下拉菜单中选择一个不同的名字,在仪表板的菜单和标题中都可以看到名字正确的更新。
图7 管理仪表板显示Carol的用户仪表板,因为我们在左上方的管理下拉菜单中选择了她的名字。
记忆化
不过这里还有一件事要做。你可能会想,当我们改变名字上下文中的名字时,我们如何能确保正确的组件被重新渲染?这是个合理的问题。因为如果你打开React开发工具中的调试选项,它可以突出显示任何重新渲染的组件,你会看到所有的组件都在重新渲染,如图8所示。
图8 当用户的选择改变时,所有的组件都会重新渲染。你可以看到,整个标题都在重新渲染,因为标题中的所有按钮都在单独重新渲染--它们周围都有一个蓝色的框。
原因很简单。任何组件在其父级组件重新渲染时都会重新渲染。所以当Dashboard组件重新渲染时,它的两个子组件Header和Main也会重新渲染。当它们渲染时,它们的所有子组件也会这样做,等等。但是这两个组件,Main和Header,实际上不需要重新渲染,因为它们没有任何变化。我们可以通过备忘这两个组件来告知React这一点。让我们继续在清单4中这样做。
清单4 带有备忘功能的管理员仪表板。
import { memo, useState, createContext, useContext } from 'react'; #A
const BUTTON_STYLE = {
display: 'inline-block',
padding: '4px 10px',
background: 'transparent',
border: '0',
};
const HEADER_STYLE = {
display: 'flex',
justifyContent: 'flex-end',
borderBottom: '1px solid',
};
const NameContext = createContext();
function Button({ children }) {
return <button style={BUTTON_STYLE}>{children}</button>;
}
function UserButton() {
const name = useContext(NameContext);
return <Button>👤 {name}</Button>;
}
const Header = memo(function Header() { #B
return (
<header style={HEADER_STYLE}>
<Button>Home</Button>
<Button>Groups</Button>
<Button>Profile</Button>
<UserButton />
</header>
);
}
function Welcome() {
const name = useContext(NameContext);
return <section><h1>Welcome, {name}!</h1></section>;
}
const Main = memo(function Main() { #B
return <main><Welcome /></main>;
}
function Dashboard({ name }) {
return (
<NameContext.Provider value={name}>
<Header />
<Main />
</NameContext.Provider>
);
}
function AdminDashboard() {
const [user, setUser] = useState('Alice');
return (
<>
<select value={user} onChange={(evt) => setUser(evt.target.value)}>
<option>Alice</option>
<option>Bob</option>
<option>Carol</option>
</select>
<Dashboard name={user} />
</>
);
}
#A 我们只改变了两件事。我们从React包中导入memo函数。
#B 我们用它来记忆两个组件。
源代码
你可以从github.com/rq2e/rq2e/t…上下载上述例子的源代码,或者你可以通过运行下面的代码来初始化一个新的React项目,并预先填入这个例子。
npx create-react-app rq10-dashboard-memo --template rq10-dashboard-memo
当我们现在在浏览器中打开这个,并在启用调试器的情况下改变用户,我们看到只有需要的组件重新渲染,如图9所示。
图9 这一次,只有正确的元素被重新渲染了。看起来在标题周围有一个蓝色的框,但实际上是在整个Dashboard组件周围(应该是重新渲染的)。你可以看到页眉组件没有被重新渲染,因为页眉中的其他按钮显然没有被重新渲染。
useContext 钩子在工作。它们使自己的组件重新渲染,而它们的父组件根本没有重新渲染--就像一个有状态的钩子应该做的那样!这很好。
这真是太妙了。如果我们思考一下其中的含义,整个上下文概念是相当强大的。我们一会儿就会这样做,但首先我们要更详细地看一下上下文API。
请看第二部分的内容。谢谢你的阅读。
The postEverything You Need to Know about React Context in 2022, part1appeared first onManning.
