說明
支持我們

內容

隨著應用程式越來越大,其虛擬 DOM 樹經常變得層層嵌套,並由許多不同的組件組成。樹中不同位置的組件有時需要存取共用資料,通常是應用程式狀態的部分,例如驗證、使用者個人資料資訊、快取、儲存空間等。雖然可以將所有這些資訊透過組件道具傳遞到樹中,但這樣做表示每個組件都需要知道所有這些狀態,即使它所做的只是將其轉發到樹中。

內容是一個讓我們自動傳遞值到樹中的功能,而不需要組件知道任何事情。這是透過提供者/使用者方法來完成的

  • <Provider>子樹 中設定內容的值
  • <Consumer> 取得由最近的父提供者設定的內容值

首先,讓我們來看一個只有一個組件的簡單範例。在這個情況下,我們提供一個「使用者名稱」內容值使用那個值

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>
  )
}

在實際使用中,內容很少在同一個組件中提供和使用,組件狀態通常是最好的解決方案。

與 hooks 一起使用

內容 <Consumer> API 對大多數使用案例來說已經足夠,但寫起來可能有點繁瑣,因為它依賴於巢狀函式來設定範圍。函式組件可以選擇改用 Preact 的 useContext() hook,它會傳回虛擬 DOM 樹中組件位置的 Context 值。

以下是前一個範例,這次將其拆成兩個組件,並使用 useContext() 來取得內容的目前值

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 需要存取多個內容的值的情況,較為簡單的 useContext() API 仍然容易得多。

實際使用

內容一個更實際的用法是儲存應用程式的驗證狀態(使用者是否已登入)。

為此,我們可以建立一個內容來儲存資訊,我們將其稱為 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>
  )
}

巢狀內容

內容有一個隱藏的超能力,在大型應用程式中非常有用:內容提供者可以巢狀,在虛擬 DOM 子樹中「覆寫」其值。想像一個基於網路的電子郵件應用程式,其中使用者介面的各個部分會根據 URL 路徑顯示

  • /inbox:顯示收件匣
  • /inbox/compose:顯示收件匣和一則新訊息
  • /settings:顯示設定
  • /settings/forwarding:顯示轉寄設定

我們可以建立一個 <Route path=".."> 元件,僅在目前路徑與給定的路徑區段相符時才呈現虛擬 DOM 樹。為了簡化巢狀路由的定義,每個相符的路由可以在其子樹中覆寫「目前路徑」內容值,以排除已相符的路徑部分。

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 context 的預設值。所有 context 都有預設值,也就是傳遞給 createContext() 的第一個參數的任何值。在這個範例中,我們沒有傳遞任何參數給 createContext,所以預設值是 undefined

以下是第一個範例使用預設 context 值,而不是 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() 技巧。或者,你也可以定義兩個 context:一個用於共用 count 值,另一個用於共用更新值的 increment 函式。

載入中...