使用React的高阶组件对原来的<AppLayout />进行改造,将其转变为一个独立通用的组件。对于原来的<AppLayout />,可以使用这个SlotProvider高阶组件,转换成一个具备插槽分发能力的组件。
import { SlotProvider } from './SlotProvider.js' class AppLayout extends React.Component { static displayName = 'AppLayout' render () { return ( <div> <header> <Slot></Slot> </header> <main> <Slot></Slot> </main> <footer> <Slot></Slot> </footer> </div> ) } } export default SlotProvider(AppLayout)
通过以上的经历,可以看到,当设计开发一个组件时,
组件可能需要由一个根组件和多个子组件一起合作来完成组件功能。比如插槽分发组件实际上需要SlotProvider与<Slot />和<AddOn />一起配合使用,SlotProvider作为根组件,而<Slot />和<AddOn />都算是子组件。
子组件相对于根组件的位置或者子组件之间的位置是不确定。对于SlotProvider而言,<Slot />的位置是不确定的,它会处在被SlotProvider这个高阶组件所包裹的组件的模板的任何位置,而对于<Slot />和<AddOn />,他们直接的位置也不确定,一个在SlotProvider包装的组件的内部,另一个是SlotProvider的children。
子组件之间需要依赖一些全局态的API或者数据,比如<Slot />实际渲染的内容来自于SlotProvider收集到的<AddOn />的内容。
这时我们就需要借助一个中间者作为媒介来共享数据,相比额外引入redux这些第三方模块,直接使用Context可以更优雅。
尝试一下新版本的Context API使用新版的Context API对之前的插槽分发组件进行改造。
// SlotProvider.js function getDisplayName (component) { return component.displayName || component.name || 'component' } export const SlotContext = React.createContext({ requestAddOnRenderer: () => {} }) const slotProviderHoC = (WrappedComponent) => { return class extends React.Component { static displayName = `SlotProvider(${getDisplayName(WrappedComponent)})` // 用于缓存每个<AddOn />的内容 addOnRenderers = {} requestAddOnRenderer = (name) => { if (!this.addOnRenderers[name]) { return undefined } return () => ( this.addOnRenderers[name] ) } render () { const { children, ...restProps } = this.props if (children) { // 以k-v的方式缓存<AddOn />的内容 const arr = React.Children.toArray(children) const nameChecked = [] this.addOnRenderers = {} arr.forEach(item => { const itemType = item.type if (item.type.displayName === 'AddOn') { const slotName = item.props.slot || '$$default' // 确保内容唯一性 if (nameChecked.findIndex(item => item === stubName) !== -1) { throw new Error(`Slot(${slotName}) has been occupied`) } this.addOnRenderers[stubName] = item.props.children nameChecked.push(stubName) } }) } return ( <SlotContext.Provider value={ requestAddOnRenderer: this.requestAddOnRenderer }> <WrappedComponent {...restProps} /> </SlotContext.Provider> ) } } } export const SlotProvider = slotProviderHoC
移除了之前的childContextTypes和getChildContext(),除了局部的调整,整体核心的东西没有大变化。
// Slot.js import { SlotContext } from './SlotProvider.js' const Slot = ({ name, children }) => { return ( <SlotContext.Consumer> {(context) => { const addOnRenderer = requestAddOnRenderer(name) return (addOnRenderer && addOnRenderer()) || children || null }} </SlotContext.Consumer> ) } Slot.displayName = 'Slot' Slot.propTypes = { name: PropTypes.string } Slot.defaultProps = { name: '$$default' }
由于之前就按照生产者消费者的模式来使用Context,加上组件自身也比较简单,因此使用新的API进行改造后,差别不大。
总结相比props和state,React的Context可以实现跨层级的组件通信。
Context API的使用基于生产者消费者模式。生产者一方,通过组件静态属性childContextTypes声明,然后通过实例方法getChildContext()创建Context对象。消费者一方,通过组件静态属性contextTypes申请要用到的Context属性,然后通过实例的context访问Context的属性。
使用Context需要多一些思考,不建议在App中使用Context,但如果开发组件过程中可以确保组件的内聚性,可控可维护,不破坏组件树的依赖关系,影响范围小,可以考虑使用Context解决一些问题。
通过Context暴露API或许在一定程度上给解决一些问题带来便利,但个人认为不是一个很好的实践,需要慎重。
旧版本的Context的更新需要依赖setState(),是不可靠的,不过这个问题在新版的API中得以解决。