原文链接:How To Master Advanced React Design Patterns: Compound Components (https://itnext.io/using-advanced-design-patterns-to-create-flexible-and-reusable-react-components-part-1-dd495fa1823)
为了庆祝 React 16.3 的正式发布,我决定分享我最近使用的一些技术,这些技术彻底改变了我创建 React 组件的方法。因此,我能够设计出完全可重用的组件,并且可以在许多不同的环境中灵活地使用这些组件。
https://codesandbox.io/embed/5x22900pnl?referrer=https://itnext.io/media/90a97a5f3dbfdc1ddac8890fa032a576?postId=dd495fa1823
上面的 sandbox
是一个简洁的 Stepper
组件的初始代码,我将使用它来展示其中的一些技术。 就目前而言,这个组件完全正常工作,并且完全按照设计目的进行,但它缺乏灵活性。
import React, { Component } from 'react';import Stepper from "./Stepper"class App extends Component {
render() { return (
<Stepper stage={1}/>
);
}
}
export default App;
如上可视,Stepper
组件的灵活性在 stage
属性处终止;我们只能修改 stage
的值来确定 Stepper
组件进行到哪一步。
stage
的 Stepper
怎么办?stage
的内容怎么办?stage
的顺序怎么办?就目前而言,我要实现这些变化的唯一方法是完全重写组件,以相同的方式重写一个类似的组件。 但是,如果将来又要进行其他更改,那该组件又一次需要重写。 因此,让我们尝试不同的方法来重写组件,使其具有灵活性和可重用性,以应变将来任何的配置。
在本系列的第一部分中,我们将探讨一种名为“复合组件”的设计模式
首先,让我们来看看 Stepper
组件。
class Stepper extends Component { state = {
stage: this.props.stage
}
static defaultProps = {
stage: 1
}
handleClick = () => {
this.setState({ stage: this.state.stage + 1 })
}
render() {
const { stage } = this.state;
return ( <div style={styles.container}>
<Progress stage={stage}/>
<Steps handleClick={this.handleClick} stage={stage}/>
</div>
);
}
}
export default Stepper;
Stepper
组件有一个存储当前 stage
的状态对象,一个增加 stage
属性值的方法,以及一个 render
方法,它返回包含2个子组件的div。
目前,我们明确地将 Progress
和 Steps
组件直接放在 Stepper
组件中。 为了减少这种静态写法,我们可以使用 props
对象动态插入子组件。
在 Stepper.js
文件中使用 props.children
对象替换 Progress
和 Steps
组件,并将它们放在 App.js
中的 Stepper
组件内。
只需这简单的改变就给我们带来很大的收益。现在我们可以选择组件树中的哪个组件先渲染; 我们可以选择进度块是在左侧还是右侧。
但这种方法有一个问题: Progress
和 Steps
组件不能再访问 stage
和 handleClick
属性了。 为了让每个子组件获取它们需要的属性,我们需要手动遍历每个子组件并向其注入这些属性。 我们可以使用 react API 提供的一些辅助方法来实现。 两个方法是: Children.map()
和 cloneElement()
。
const children = React.Children.map(this.props.children, child => {
return React.cloneElement(child, {stage, handleClick: this.handleClick})})
Children.map()
类似于 Array.map()
方法。但请务必使用Children.map()
,因为 children.props
具有不透明的数据结构,使得 Array.map()
方法不适合此用例。
cloneElement
如名称一样,它克隆这些子组件并可以注入额外的属性,最后返回新的组件。
// Render method of Stepper.jsconst { 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>
);
现在我们可以将 Progress
和 Stage
作为子项添加到 Stepper
组件中,运行效果不变。但此时我们可以决定每个组件的位置,甚至可以在左右两侧同时设置进度块。
import React, { Component } from 'react';import Stepper from "./Stepper"import Progress from './components/Progress';import Steps from './components/Steps';class App extends Component {
render() { return ( <div>
<Stepper stage={1}>
<Progress />
<Steps />
</Stepper>
</div>
);
}
}export default App;
另外一种能够提高可读性和易用性的技术就是使用类的静态属性。它允许我们直接在类上调用方法。
这是什么意思? 让我们来看看…。
首先,我们在 Stepper
组件中创建两个静态方法,并将 Progress
和 Steps
组件赋值给它们:
static Progress = Progressstatic Steps = Steps
那么现在,我们不需要再到处引入 Progress
和 Steps
组件,而是直接从 Stepper
组件中引用它们:
import React, { Component } from 'react';import Stepper from "./Stepper"class App extends Component {
render() { return (
<Stepper stage={1}>
<Stepper.Progress />
<Stepper.Steps />
</Stepper>
);
}
}
export default App;
到目前为止,我们已经创建了一个简单可读且灵活的API。那么是不是可以对 Progress
组件使用相同的技术呢? 让我们来看看......
export default class Progress extends Component {
render(){
const {stage} = this.props return(
<div style={styles.progressContainer}>
<Stage stage={stage} num={1} />
<Stage stage={stage} num={2} />
<Stage stage={stage} num={3} />
<Stage stage={stage} num={4} />
</div>
)
}
}
您也许已经注意到 Progress
组件与之前的 Stepper
组件存在同样的问题。所以我们用 props.children
对象来替换这 4 个 Stage
组件并遍历子项添加所需的属性,然后在 Stepper
类中添加一个 Stage
静态方法,供外部直接引用 Stage
。
export default class Progress extends Component {
render(){
const {stage} = this.props
const children = React.Children.map(this.props.children, child => { return React.cloneElement(child, {stage})
}) return(
<div style={styles.progressContainer}>
{children}
</div>
)
}
}
完成上述步骤后,我们可以在任意位置动态添加任意数量的 Stage
组件:
import React, { Component } from 'react';import Stepper from "./Stepper"class App extends Component {
render() { return (
<Stepper stage={1}>
<Stepper.Progress>
<Stepper.Stage num={1} />
<Stepper.Stage num={2} />
<Stepper.Stage num={3} />
<Stepper.Stage num={4} />
</Stepper.Progress>
<Stepper.Steps />
</Stepper>
);
}
}
export default App;
接下来我们可以对 Steps
组件做同样的改进,但这个有点不同,因为每个子项都要被 React's Transition Group 的 Transition
组件包裹。同样是使用 Children.map()
遍历,但只有 Steps
组件的 stage
属性与子组件的 num
属性匹配时才展示该子组件。 即它们匹配时,子组件会被包裹在 Transition
组件中(ReactTransitionGroup文档解释了此目的)并在屏幕上渲染。
class Steps extends Component {
render() {
const { stage, handleClick } = this.props
const children = React.Children.map(this.props.children, child => {
console.log(child.props)
return (
stage === child.props.num && <Transition appear={true} timeout={300} onEntering={entering} onExiting={exiting}>
{child} </Transition>
)
})
return ( <div style={styles.stagesContainer}>
<div style={styles.stages}>
<TransitionGroup>
{children} </TransitionGroup>
</div>
<div style={styles.stageButton}>
<Button disabled={stage === 4} click={handleClick}>Continue</Button>
</div>
</div>
);
}
}
export default Steps;
通过在 Stepper
组件上添加相应的静态方法,我们可以按照我们想要的顺序添加任意数量的Step组件。
import React, { Component } from 'react';
import Stepper from "./Stepper"
class App extends Component {
render() {
return ( <Stepper stage={1}>
<Stepper.Progress>
<Stepper.Stage num={1} />
<Stepper.Stage num={2} />
<Stepper.Stage num={3} />
</Stepper.Progress>
<Stepper.Steps>
<Stepper.Step num={1} text={"Stage 1"}/>
<Stepper.Step num={2} text={"Stage 2"}/>
<Stepper.Step num={3} text={"Stage 3"}/>
<Stepper.Step num={4} text={"Stage 4"}/>
</Stepper.Steps>
</Stepper>
);
}
}
export default App;
我们用一种方式就创建了非常灵活可重用的组件。现在我们可以选择多少个 stage,每个 stage 的文本和顺序,以及我们可以决定进度条在左侧还是右侧。
https://codesandbox.io/embed/5x22900pnl?referrer=https://itnext.io/media/90a97a5f3dbfdc1ddac8890fa032a576?postId=dd495fa1823
虽然改进了很多,但在灵活性上我们仍然受到限制! 如果我们想在 Steps
上方添加标题怎么办?
class App extends Component {
render() {
return ( <Stepper stage={1}>
<Stepper.Progress>
<Stepper.Stage num={1} />
<Stepper.Stage num={2} />
<Stepper.Stage num={3} />
</Stepper.Progress>
<div>
<div>Title</div>
<Stepper.Steps>
<Stepper.Step num={1} text={"Stage 1"}/>
<Stepper.Step num={2} text={"Stage 2"}/>
<Stepper.Step num={3} text={"Stage 3"}/>
<Stepper.Step num={4} text={"Complete!"}/>
</Stepper.Steps>
</div>
</Stepper>
);
}
}
export default App;
上面这样做会破坏我们应用程序的结构,因为 Stepper.Steps
组件不再是 Stepper
组件的直接子组件,所以它无法访问 stage
属性了。
这就是为什么 React 16.3 的发布非常重要! 到目前为止,React’s context
API 还处于试验阶段,但现在它已经正式发布了!
在本系列的第2部分中,我将探讨如何实现 context
API 以便能够在组件树中的任何位置传递属性,这样无论 Stepper.Steps
组件位于何处,它始终都能够访问 stage
属性。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有