上下文
随着应用程序的不断发展,其虚拟 DOM 树通常会变得深度嵌套,并由许多不同的组件组成。树中不同位置的组件有时需要访问公共数据 - 通常是应用程序状态的部分,如身份验证、用户个人资料信息、缓存、存储等。虽然可以将所有这些信息作为组件属性向下传递到树中,但这样做意味着每个组件都需要了解所有这些状态 - 即使它所做的只是通过树转发它。
Context 是一项功能,它允许我们自动向下传递值,而无需组件意识到任何内容。这是使用 Provider/Consumer 方法完成的
<Provider>
在 子树 中设置 Context 的值<Consumer>
获取由最近的父级 Provider 设置的 Context 值
首先,我们来看一个仅包含一个组件的简单示例。在这种情况下,我们提供一个“用户名”Context 值和使用该值
import { createContext } from 'preact'
const Username = createContext()
export default function App() {
return (
// provide the username value to our subtree:
<Username.Provider value="Bob">
<div>
<p>
<Username.Consumer>
{username => (
// access the current username from context:
<span>{username}</span>
)}
</Username.Consumer>
</p>
</div>
</Username.Provider>
)
}
在实际使用中,很少在同一组件中提供和使用 Context - 组件状态通常是最佳解决方案。
与钩子一起使用
Context <Consumer>
API 足以满足大多数用例,但由于它依赖于嵌套函数来确定范围,因此编写起来可能有点乏味。函数组件可以选择使用 Preact 的 useContext()
钩子,它返回 Virtual DOM 树中组件位置处的 Context
的值。
下面是前面的示例,这次将其拆分为两个组件并使用 useContext()
获取 Context 的当前值
import { createContext } from 'preact'
import { useContext } from 'preact/hooks'
const Username = createContext()
export default function App() {
return (
<Username.Provider value="Bob">
<div>
<p>
<User />
</p>
</div>
</Username.Provider>
)
}
function User() {
// access the current username from context:
const username = useContext(Username) // "Bob"
return <span>{username}</span>
}
如果你能想象一个 User
需要访问多个 Context 的值的情况,那么更简单的 useContext()
API 仍然更容易遵循。
实际使用
Context 的更实际的用法是存储应用程序的身份验证状态(用户是否已登录)。
为此,我们可以创建一个 Context 来保存信息,我们将其称为 AuthContext
。AuthContext 的值将是一个对象,其中包含一个 user
属性(包含我们已登录的用户),以及一个 setUser
方法来修改该状态。
import { createContext } from 'preact'
import { useState, useMemo, useContext } from 'preact/hooks'
const AuthContext = createContext()
export default function App() {
const [user, setUser] = useState(null)
const auth = useMemo(() => {
return { user, setUser }
}, [user])
return (
<AuthContext.Provider value={auth}>
<div class="app">
{auth.user && <p>Welcome {auth.user.name}!</p>}
<Login />
</div>
</AuthContext.Provider>
)
}
function Login() {
const { user, setUser } = useContext(AuthContext)
if (user) return (
<div class="logged-in">
Logged in as {user.name}.
<button onClick={() => setUser(null)}>
Log Out
</button>
</div>
)
return (
<div class="logged-out">
<button onClick={() => setUser({ name: 'Bob' })}>
Log In
</button>
</div>
)
}
嵌套 Context
Context 具有一个隐藏的超能力,在大型应用程序中非常有用:Context 提供者可以嵌套以在 Virtual DOM 子树中“覆盖”其值。想象一个基于 Web 的电子邮件应用程序,其中用户界面的各个部分根据 URL 路径显示
/inbox
:显示收件箱/inbox/compose
:显示收件箱和新邮件/settings
:显示设置/settings/forwarding
:显示转发设置
我们可以创建一个 <Route path="..">
组件,它仅在当前路径与给定路径段匹配时才呈现 Virtual DOM 树。为了简化嵌套 Route 的定义,每个匹配的 Route 可以在其子树中覆盖“当前路径”Context 值,以排除匹配的部分路径。
import { createContext } from 'preact'
import { useContext } from 'preact/hooks'
const Path = createContext(location.pathname)
function Route(props) {
const path = useContext(Path) // the current path
const isMatch = path.startsWith(props.path)
const innerPath = path.substring(props.path.length)
return isMatch && (
<Path.Provider value={innerPath}>
{props.children}
</Path.Provider>
)
}
现在我们可以使用这个新的 Route
组件来定义电子邮件应用程序的界面。注意 Inbox
组件不需要知道它自己的路径就可以为其子组件定义 <Route path"..">
匹配
export default function App() {
return (
<div class="app">
<Route path="/inbox">
<Inbox />
</Route>
<Route path="/settings">
<Settings />
</Route>
</div>
)
}
function Inbox() {
return (
<div class="inbox">
<div class="messages"> ... </div>
<Route path="/compose">
<Compose />
</Route>
</div>
)
}
function Settings() {
return (
<div class="settings">
<h1>Settings</h1>
<Route path="/forwarding">
<Forwarding />
</Route>
</div>
)
}
默认上下文值
嵌套上下文是一个强大的功能,我们经常在不知不觉中使用它。例如,在本教程的第一个示例中,我们使用 <Provider value="Bob">
在树中定义 Username
上下文值。
然而,这实际上覆盖了 Username
上下文的默认值。所有上下文都有一个默认值,即传递给 createContext()
的第一个参数的任何值。在示例中,我们没有向 createContext
传递任何参数,因此默认值是 undefined
。
以下是第一个示例使用默认上下文值而不是 Provider 的样子
import { createContext } from 'preact'
import { useContext } from 'preact/hooks'
const Username = createContext('Bob')
export default function App() {
const username = useContext(Username) // returns "Bob"
return <span>{username}</span>
}
试试看!
作为一个练习,让我们创建一个我们在上一章中创建的计数器的同步版本。为此,您需要使用本章身份验证示例中的 useMemo()
技术。或者,您还可以定义两个上下文:一个共享 count
值,另一个共享更新该值的 increment
函数。