帮助
支持我们

上下文

随着应用程序的不断发展,其虚拟 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 函数。

加载中...