內容
隨著應用程式越來越大,其虛擬 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
函式。