React 社区在近几年的发展过程中诞生了很多有趣的组件设计 "模式",通过合理的运用这些模式可以让我们写出更好用的 React 组件。
比如,在不使用任何设计模式的情况下,要实现一个类似这样的 Tabs 组件

最初的代码可能是这样的
const Tab = ({ value, active, onChange, children }) => ( <div className={active && 'active'} onClick={evt => onChange(value, evt)}> {children} </div> ) 用起来是这样的:
class Index extends React.Component { state = { value: 'a' } handleChange = value => { this.setState({ value }) } render() { return ( <div> <Tab value="a" active={this.state.value === 'a'} onChange={this.handleChange}> Tab A </Tab> <Tab value="b" active={this.state.value === 'b'} onChange={this.handleChange}> Tab B </Tab> <Tab value="c" active={this.state.value === 'c'} onChange={this.handleChange}> Tab B </Tab> </div> ) } } 从上面的代码可以看出,Tab 组件的 Props 中只有 value 和 children 是由使用者决定的,active 和 onChange 是有固定模式且重复的,我们可以想一些办法将这些有清晰模式且重复的东西自动传给 Tab 组件,避免我们每次都手动维护
复合组件 Compound Components
这种模式利用 React.Children 这个 API, 给每个 Tab 组件自动传入 value 和 active props,这样我们使用 Tab 组件时只需要提供 value 和 children 就可以了
const Tabs = ({ value, onChange, onClick, children }) => React.Children.map(children, el => { const { value: childValue } = el.props return React.cloneElement(el, { onChange, onClick: onChange, active: value === childValue, value: childValue }) }) 用起来是这样的:
class Index extends React.Component { state = { value: 'Tab A' } handleChange = value => { this.setState({ value }) } render() { return ( <Tabs value={this.state.value} onChange={this.handleChange}> {['Tab A', 'Tab B'].map(tab => ( <Tab value={tab} key={tab}> {tab} </Tab> ))} </Tabs> ) } } 这种模式一般都会限制 DOM 结构,比如:
- Tab 组件只能作为 Tabs 的直系子组件,不能被其他组件包裹
- Tabs 组件只能包含 Tab 组件
Context
针对上面的问题,我们可以用 React 16 新增的 context API 来重写上面的例子
const initialValue = { value: '', onChange() {} } const TabsContext = React.createContext(initialValue) const Tab = ({ value, children }) => ( <TabsContext.Consumer> {ctx => ( <div className={ctx.value === value && 'active'} onClick={evt => ctx.onChange(value, evt)}> {children} </div> )} </TabsContext.Consumer> ) const Tabs = ({ value, onChange, children }) => ( <TabsContext.Provider value={{ value, onChange }}>{children}</TabsContext.Provider> ) 现在可以这样使用:
class Index extends React.Component { state = { value: 'Tab A' } handleChange = value => { this.setState({ value }) } render() { return ( <Tabs value={this.state.value} onChange={this.handleChange}> {['Tab A', 'Tab B'].map(tab => ( <span key={tab}> <Tab value={tab}>{tab}</Tab> </span> ))} </Tabs> ) } } 上面的代码里 Tabs 组件内可以随意嵌套组合其他组件,并不会影响 Tab 组件正常工作
这种模式主要的优点在于可以 props 透传,可以让 DOM 结构更灵活。
Render Props
Render Props 模式本质上相当于控制反转。在前面的 Tab 组件里可以看到它返回了一个 div 元素,但在有些情况下用户会想要用其他组件来代替这个 div 元素,这种情况可以使用一个 callback 作为 prop,将 Tab 组件的核心状态作为参数传给它,用户拿到这些核心状态后到底是渲染 div 还是渲染其他组件就完全交给用户来决定了
将开头的 Tab 组件修改为如下:
const Tab = ({ value, active, onChange, children, render }) => { if (typeof render === 'function') { return render({ value, active, onChange }) } return ( <div className={active && 'active'} onClick={evt => onChange(value, evt)}> {children} </div> ) } 现在可以自定义 render 内容了:
class Index extends React.Component { state = { value: 'Tab A' } handleChange = value => { this.setState({ value }) } render() { return ( <Tabs value={this.state.value} onChange={this.handleChange}> {['Tab A', 'Tab B'].map(tab => ( <Tab value={tab} render={({ value, active, onChange }) => ( <li className={active && 'active'} onClick={evt => onChange(value, evt)}> <span>{tab}</span> </li> )} key={tab} /> ))} </Tabs> ) } }这种模式可以很好的提取纯粹的逻辑组件,最大化的复用业务逻辑
高阶组件 HOC(Higher Order Component)
所谓高阶组件就是一个函数,它可以接收一个组件并返回一个组件,它可以用来装饰组件,给组件注入 props
例如,我们整个应用中有几个不同地方都散布着几个类似下面这样的的组件:
const NavBar = ({ user }) => ( <nav> User: <span>{user ? user.name : 'fetching...'}</span> </nav> ) 这些组件都有一个相同的 user prop,这个 user 是需要从其他地方动态获取的,为了避免获取 user 的操作重复,我们可以把它提取为一个 HOC:
const withUser = WrappedComponent => class HocWithUser extends React.Component { state = { user: null } componentDidMount() { fetchUser().then(user => { this.setState({ user }) }) } render() { return <WrappedComponent {...this.porps} user={this.state.user} /> } } 然后可以这样使用:
const NavBar = ({ user }) => ( <nav> User: <span>{user ? user.name : 'fetching...'}</span> </nav> ) const NavBarWithUser = withUser(NavBar) class Index extends React.Component { render() { return <NavBarWithUser /> } } 通过这个 HOC 我们可以将 user 注入给指定的组件,而不必重复 user 获取逻辑,但是目前还有两个问题:
withUser返回的组件在ReactDevTools里显示的名称都是相同的HocWithUser,这会影响我们的 Debug 效率- 调用
withUser返回的组件是无法访问到WrappedComponent的静态属性,这显然是不行的
针对第一个问题,通常做法是在 HocWithUser 增加一个静态属性 displayName,如下:
const withUser = WrappedComponent => class HocWithUser extends React.Component { static displayName = `withUser(${WrappedComponent.displayNanme || WrappedComponent.name || 'Component'})` state = { user: null } componentDidMount() { fetchUser().then(user => { this.setState({ user }) }) } render() { return <WrappedComponent {...this.porps} user={this.state.user} /> } }针对第二个问题,我们可以遍历 WrappedComponent,将它的静态属性添加到 HocWithUser,但是要排除 React 相关的属性,这个操作可以用 hoist-non-react-statics 或者 recompose/hoistStatics 代劳
这里使用后者,将前面的 withUser 作为 hoistStatics 的第一个参数即可:
import hoistStatics from 'recompose/hoistStatics' const withUser = hoistStatics( WrappedComponent => class HocWithUser extends React.Component { static displayName = `withUser(${WrappedComponent.displayNanme || WrappedComponent.name || 'Component'})` state = { user: null } componentDidMount() { fetchUser().then(user => { this.setState({ user }) }) } render() { return <WrappedComponent {...this.porps} user={this.state.user} /> } } ) 这里的 user 在实际应用中通常是整个应用共享的,这种情况实际上使用前面提到的
context模式比较合适,当然context模式也可以和HOC模式很好的配合
在 controlled 和 uncontrolled 之间切换
在 React 应用里,受控组件也叫木偶组件,就是这个组件的 value 都是父组件控制的,父组件传给它什么内容,它就展示什么,当内容发生变化的时候,通过调用父组件传下来的 onChange 回调函数通知父组件内容发生了变化。
这种模式的好处是可以将展示逻辑和控制逻辑分离,并且可以方便组件之间进行组合复用。
一般情况下受控组件的 props 看起来是这样的
type Props = { value: boolean onChange: () => void } 使用的时候通常需要这样:
class Container extends React.Component { state = { value: false } handleChange = () => { this.setState(prevState => ({ value: !prevState.value }原文:https://baiyun.me/react-design-patterns



