上下文
¥Context
Context 是一种通过组件树传递数据的方式,而不必通过 props 将其传递到中间的每个组件。简而言之,它允许层次结构中任何位置的组件订阅值并在值更改时收到通知,从而为 Preact 带来发布-订阅式更新。
¥Context is a way to pass data through the component tree without having to pass it through every component in-between via props. In a nutshell, it allows components anywhere in the hierarchy to subscribe to a value and get notified when it changes, bringing pub-sub-style updates to Preact.
经常会遇到需要将祖父组件(或更高)的值传递给子组件的情况,而中间组件通常不需要它。传递 props 的过程通常称为 "prop drill",可能很麻烦、容易出错并且只是简单的重复,特别是随着应用的增长并且更多值必须通过更多层传递。这是 Context 旨在解决的关键问题之一,它为子组件提供了一种订阅组件树中更高层值的方法,无需将其作为 prop 传递即可访问该值。
¥It's not uncommon to run into situations in which a value from a grandparent component (or higher) needs to be passed down to a child, often without the intermediate component needing it. This process of passing down props is often referred to as "prop drilling" and can be cumbersome, error-prone, and just plain repetitive, especially as the application grows and more values have to be passed through more layers. This is one of the key issues Context aims to address by providing a way for a child to subscribe to a value higher up in the component tree, accessing the value without it being passed down as a prop.
有两种在 Preact 中使用上下文的方法:通过较新的 createContext
API 和旧版上下文 API。如今,很少有理由使用旧版 API,但为了完整性,这里记录了它。
¥There are two ways to use context in Preact: via the newer createContext
API and the legacy context API. These days there's very few reasons to ever reach for the legacy API but it's documented here for completeness.
现代上下文 API
¥Modern Context API
创建上下文
¥Creating a Context
要创建新上下文,我们使用 createContext
函数。此函数将初始状态作为参数并返回具有两个组件属性的对象:Provider
,使上下文可供后代使用,Consumer
,访问上下文值(主要在类组件中)。
¥To create a new context, we use the createContext
function. This function takes an initial state as an argument and returns an object with two component properties: Provider
, to make the context available to descendants, and Consumer
, to access the context value (primarily in class components).
import { createContext } from "preact";
export const Theme = createContext("light");
export const User = createContext({ name: "Guest" });
export const Locale = createContext(null);
设置提供程序
¥Setting up a Provider
一旦我们创建了上下文,我们必须使用 Provider
组件将其提供给后代。必须为 Provider
赋予 value
prop,表示上下文的初始值。
¥Once we've created a context, we must make it available to descendants using the Provider
component. The Provider
must be given a value
prop, representing the initial value of the context.
仅在树中消费者上方没有
Provider
时才使用从createContext
设置的初始值。这可能有助于单独测试组件,因为它避免了在组件周围创建封装Provider
的需要。¥The initial value set from
createContext
is only used in the absence of aProvider
above the consumer in the tree. This may be helpful for testing components in isolation, as it avoids the need for creating a wrappingProvider
around your component.
import { createContext } from "preact";
export const Theme = createContext("light");
function App() {
return (
<Theme.Provider value="dark">
<SomeComponent />
</Theme.Provider>
);
}
提示:你可以在整个应用中拥有相同上下文的多个提供程序,但只会使用最接近消费者的提供程序。
¥Tip: You can have multiple providers of the same context throughout your app but only the closest one to the consumer will be used.
使用上下文
¥Using the Context
有两种使用上下文的方法,很大程度上取决于你喜欢的组件样式:Consumer
(类组件)和 useContext
钩子(函数组件/钩子)。
¥There are two ways to consume a context, largely depending on your preferred component style: Consumer
(class components) and the useContext
hook (function components/hooks).
const ThemePrimary = createContext("#673ab8");
function ThemedButton() {
return (
<ThemePrimary.Consumer>
{theme => <button style={{ background: theme }}>Themed Button</button>}
</ThemePrimary.Consumer>
);
}
function App() {
return (
<ThemePrimary.Provider value="#8f61e1">
<SomeComponent>
<ThemedButton />
</SomeComponent>
</ThemePrimary.Provider>
);
}
Run in REPL
const ThemePrimary = createContext("#673ab8");
function ThemedButton() {
const theme = useContext(ThemePrimary);
return <button style={{ background: theme }}>Themed Button</button>;
}
function App() {
return (
<ThemePrimary.Provider value="#8f61e1">
<SomeComponent>
<ThemedButton />
</SomeComponent>
</ThemePrimary.Provider>
);
}
Run in REPL
更新上下文
¥Updating the Context
静态值可能很有用,但更多时候,我们希望能够动态更新上下文值。为此,我们利用标准组件状态机制:
¥Static values can be useful, but more often than not, we want to be able to update the context value dynamically. To do so, we leverage standard component state mechanisms:
const ThemePrimary = createContext(null);
function ThemedButton() {
const { theme } = useContext(ThemePrimary);
return <button style={{ background: theme }}>Themed Button</button>;
}
function ThemePicker() {
const { theme, setTheme } = useContext(ThemePrimary);
return (
<input
type="color"
value={theme}
onChange={e => setTheme(e.currentTarget.value)}
/>
);
}
function App() {
const [theme, setTheme] = useState("#673ab8");
return (
<ThemePrimary.Provider value={{ theme, setTheme }}>
<SomeComponent>
<ThemedButton />
{" - "}
<ThemePicker />
</SomeComponent>
</ThemePrimary.Provider>
);
}
Run in REPL
旧版上下文 API
¥Legacy Context API
此 API 被视为旧版,应在新代码中避免使用,它存在已知问题,仅出于向后兼容的原因而存在。
¥This API is considered legacy and should be avoided in new code, it has known issues and only exists for backwards-compatibility reasons.
此 API 与新 API 之间的一个主要区别是,当子级和提供程序之间的组件通过 shouldComponentUpdate
中止渲染时,此 API 无法更新子级。发生这种情况时,子项将不会收到更新的上下文值,通常会导致撕裂(部分 UI 使用新值,部分使用旧值)。
¥One of the key differences between this API and the new one is that this API cannot update a child when a component in-between the child and the provider aborts rendering via shouldComponentUpdate
. When this happens, the child will not received the updated context value, often resulting in tearing (part of the UI using the new value, part using the old).
要通过上下文传递值,组件需要具有 getChildContext
方法,返回预期的上下文值。然后,后代可以通过函数组件中的第二个参数或基于类的组件中的 this.context
访问上下文。
¥To pass down a value through the context, a component needs to have the getChildContext
method, returning the intended context value. Descendants can then access the context via the second argument in function components or this.context
in class-based components.
function ThemedButton(_props, context) {
return (
<button style={{ background: context.theme }}>
Themed Button
</button>
);
}
class App extends Component {
getChildContext() {
return {
theme: "#673ab8"
}
}
render() {
return (
<div>
<SomeOtherComponent>
<ThemedButton />
</SomeOtherComponent>
</div>
);
}
}
Run in REPL