本文作者:IMWeb howenhuo 原文出处:IMWeb社区 未经同意,禁止转载
原文链接:How To Master Advanced React Design Patterns: Context API
使用高级设计模式创建灵活可重用的React组件 - 第1部分:复合组件
在本系列的上一部分中,我们探讨了如何使用复合组件和静态类方法来创建灵活可重用的组件。使用我们创造的API,我们能够以声明的方式来动态重建各种变化的组件
我们可以轻松添加任意数量的 step
,我们可以决定 progress
是在左侧还是右侧。
class App extends Component {
render() {
return (
<Stepper stage={1}>
<Stepper.Progress>
<Stepper.Stage num={1} />
</Stepper.Progress>
<Stepper.Steps>
<Stepper.Step num={1} text={"Stage 1"}/>
</Stepper.Steps>
</Stepper>
);
}
}
export default App;
我们能够做到这一点是因为我们使用了一些 React API 辅助函数将所需的属性传递给组件树中的每个子项; stage
和 handleClick
属性可被需要它们的组件访问。
但是这种技术存在一个主要缺陷。 props
只能传递给他们的直接子项。 这使得 API 非常僵硬,它要求 Stepper.Steps
组件必须是 Stepper
组件的直接子组件,否则 props
传递会中断。 这在灵活性上存有巨大影响。
如果我们想要使用 flexbox 添加标题怎么办?
class App extends Component {
render() {
return (
<Stepper stage={1}>
<Stepper.Progress>
<Stepper.Stage num={1} />
</Stepper.Progress>
<div style={{flex: 1, display: 'flex', flexDirection: 'column'}}>
<Stepper.Header title="Stepper Heading"/>
<Stepper.Steps>
<Stepper.Step num={1} text={"Stage 1"}/>
</Stepper.Steps>
</div>
</Stepper>
);
}
}
通过添加一个简单的 div ,我们完全破坏了组件。 Stepper.Steps
组件不再是 Stepper
组件的直接子组件,因此无法接收其 props
。
那有没有一种灵活的,仅需要小调整就能达到我们预期的方法呢?
答案就是:Context
!!
React Context
已经存在了一段时间,但 React 工程师非常清楚它是实验性的,并且很可能在不久的将来会废弃。 好消息的是从 React 16.3 开始,它已经稳定了,我们可以在整个 React 应用程序中使用它。
那么我们一直听到的这个 Context
是什么?
我无法给出比 React 官方文档更清晰的定义:
Context
提供了一种在组件之间共享数据的方式,而不必通过组件树的每个层级显式地传递props
。
这正好解决我们的问题! 使用 Context
,我们不再需要遍历并克隆每个子项来传递所需的 props
。 Context
的设计让我们可以共享“全局”状态,并在 React 树中任何位置获取。
接着,让我展示给你如何使用和运行 Context API
的步骤。
React 现在带有一个名为 createContext
的方法。 我们需要做的只是调用此方法并将其赋给一个变量。
export const StepperContext = React.createContext();
我们创建的新 context
提供我们访问一对 Provider
和 Consumer
。 Provider
为我们提供在整个 React 树中共享状态变化的能力。 Consumer
允许我们在树中的任何位置订阅这些状态更改。
我们刚刚创建的 Context
有一个名为 Provider
的静态类方法,它是一个 React 组件。 该组件接受 value 属性。 这非常重要,因为这个属性代表我们需要传递给树中更下层组件的全局状态。 在我们的例子中,我们想要全局共享的是 stage
属性和 handleClick
方法。
通过使用我们在本系列的第一部分中使用的 props.children
技术,我们可以动态地将任何子组件暴露给 Provider
,无论它在组件树中有多深。
class StepperProvider extends Component {
state = {
stage: 1
}
render() {
return (
<StepperContext.Provider value={{
stage: this.state.stage,
handleClick: () => this.setState({
stage: this.state.stage + 1
})
}}>
{this.props.children}
</StepperContext.Provider>
)
}
}
通过简单地用 StepperProvider
组件包裹原来的 Stepper
组件,树下的所有子组件现在都暴露在我们的 Context
中。
class App extends Component {
render() {
return (
<StepperProvider>
<Stepper stage={1}>
<Stepper.Progress>
<Stepper.Stage num={1} />
</Stepper.Progress>
<Stepper.Steps>
<Stepper.Step num={1} text={"Stage 1"}/>
</Stepper.Steps>
</Stepper>
</StepperProvider>
);
}
}
我们的 Stepper
代码几乎没有变,只是将它包裹在 StepperProvider
组件中,现在我们所有的子组件都可以访问 stage
和 handleClick
,而无需手动将它们向下传递到每个组件。
最初,我们的状态由 Stepper
组件管理,我们克隆了每个子组件来接收所需的 props
。
class Stepper extends Component {
state = {
stage: this.props.stage
}
static defaultProps = {
stage: 1
}
static Progress = Progress
static Steps = Steps
static Stage = Stage
static Step = Step
handleClick = () => {
this.setState({ stage: this.state.stage + 1 });
}
render() {
const { stage } = this.state;
const children = React.Children.map(this.props.children, child => {
return React.cloneElement(child, {stage, handleClick: this.handleClick})
})
return (
<div style={styles.container}>
{children}
</div>
);
}
}
上面的这些代码几乎都不再需要了。 我们不再需要创建状态,我们不再需要传递任何 props
。 我们完全可以废弃这些代码,只保留我们声明的静态方法,来对外提供一个干净可读的API。
class Stepper extends Component {
static Progress = Progress
static Steps = Steps
static Stage = Stage
static Step = Step
render() {
return (
<div style={styles.container}>
{children}
</div>
);
}
}
export default Stepper;
我将使用 Stepper.Step
组件来演示如何连接 Consumer
组件。 以前,Stepper.Step
组件需要其父级直接传递 stage
属性以使其正常运行:
export const Step = ({num, text,stage}) => (
return stage === num ? <div key={num} style={styles.stageContent}>{text}</div> : null
)
随着我们的应用程序连接 Context
,我们可以使用 Consumer
来订阅它:
<Consumer>
{value => /* render something based on the context value */}
</Consumer>
Consumer
需要一个函数作为子项,此函数提供我们全局的 Context
值。函数完成后,返回一个 react 节点。
究竟是什么意思?
起初它有点令人头疼,但让我们来看看“消费”的 Step
组件。
export const Step = ({num, text}) => (
<StepperContext.Consumer>
{value => {
const {stage} = value
return stage === num ? <div key={num} style={styles.stageContent}>{text}</div> : null
}}
</StepperContext.Consumer>
)
我们不是直接将 Step
标记作为子项添加到 Consumer
中,而是添加一个函数。 这个函数提供了我们之前在 Provider
创建的值,然后我们可以使用 ES6 解构来提取 stage
属性。Step
组件现在可以像以前一样访问 stage
属性,只是这一次是从 Context
中获取的。 在这里我们可以随意的使用它; 我们使用它来确定返回什么 React 节点。
这里使用的技术可能看起来有点奇怪。 它被称为 Render Props
,官方 react 文档的解释。 这是一个非常强大的技术,我将在本系列的第3部分中探讨。
到这里我不再逐步详细介绍了,只需要对 Stepper.Steps
,Stepper.Progress
和 Stepper.Stage
组件重复第4和第5步骤,您最终应该看到组件的外观和功能与以前完全相同。
现在,我们任何组件都不依赖于其他组件的直接后代。 我们现在有更灵活的代码,应该能够添加我们之前无法做到的标题了!
class App extends Component {
render() {
return (
<Stepper stage={1}>
<Stepper.Progress>
<Stepper.Stage num={1} />
</Stepper.Progress>
<div style={{flex: 1, display: 'flex', flexDirection: 'column'}}>
<Stepper.Header title="Stepper Heading"/>
<Stepper.Steps>
<Stepper.Step num={1} text={"Stage 1"}/>
</Stepper.Steps>
</div>
</Stepper>
);
}
}
Stepper.Steps
和 Stepper.Step
不再直接从父级那里取出 stage
属性。 他们从 Context
订阅它,所以额外的 div 不会阻止 props 在组件树下进一步传递。 该应用仍然如期运行!
这么做给了我们很大的灵活性。 我们可以重用我们的组件来动态创建 Stepper
组件的复杂变体,而不必担心我们的应用结构是否被破坏
虽然我们可以在应用程序中的任何地方使用此组件,但它仍然不是真正可重用的。我们仍然需要 Context
的引用才能使其工作。
在本系列的下一部分中,我将探讨如何使用 render props
来实现相同的目标,而不必依赖于连接 Context
来共享应用程序中组件之间的状态。