React学习笔记[8]✨~如何在React中实现可扩展组件(类似于Vue中的插槽)👻

1,600 阅读4分钟

我正在参加「掘金·启航计划」

如果要实现可扩展的组件,类似于:

在Vue中会通过插槽来实现,那么在React中是否也存在插槽呢?

实际上在React中是没有插槽这个概念的,因为React非常灵活,如果想封装一个可扩展的组件,可通过以下两种方案来实现:

方案一:通过组件的children子元素来获取插槽内容

方案二:通过props属性传递React元素

一、通过children实现插槽

父组件可以直接向子组件中插入内容,此时在子组件中可以在this.props.children获取到插入到其中传递的元素

  • 如果插入了多个元素,那么this.props.children是一个数组,此时可通过索引访问到对应位置的元素
  • 如果插入了单个元素,那么this.props.children即该元素(就不是一个数组了

案例

navBar/index.jsx:

import React, { Component } from 'react'
import PropTypes from "prop-types"
import "./style.css"

export class NavBar extends Component {
  render() {
    const { children } = this.props

    return (
      <div className='nav-bar'>
        <div className="left">{children[0]}</div>
        <div className="center">{children[1]}</div>
        <div className="right">{children[2]}</div>
      </div>
    )
  }
}

NavBar.propTypes = {
  children: PropTypes.array
}

export default NavBar

可以通过为子组件设置propTypes的方式来限制传入子组件的元素数量

  • 比如,想限制传入该组件的元素必须是多个可通过 children: PropTypes.array来限制

App.jsx:

import React, { Component } from 'react'
import NavBar from './nav-bar'

export class App extends Component {
  render() {
    return (
      <div>
        <NavBar>
          <button>按钮</button>
          <h2>哈哈哈</h2>
          <i>斜体文本</i>
        </NavBar>
      </div>
    )
  }
}

export default App

如果此时在NavBar中插入了一个元素,那么将会报错:

如果想要限制只能传入一个元素,那么在子组件中可通过PropTypes这样限制:

NavBar.propTypes = {
  children: PropTypes.element
}

此时需要注意的是,不要通过children[0]去获取插入的元素了,如果传入单个元素的话,children即为当前元素:

import React, { Component } from 'react'
import NavBar from './nav-bar'

export class App extends Component {
  render() {
    return (
      <div>
        <NavBar>
          {/* <button>按钮</button> */}
          <h2>哈哈哈</h2>
          {/* <i>斜体文本</i> */}
        </NavBar>
      </div>
    )
  }
}

export default App
import React, { Component } from 'react'
import PropTypes from "prop-types"
import "./style.css"

export class NavBar extends Component {
  render() {
    const { children } = this.props

    return (
      <div className='nav-bar'>
        {/* <div className="left">{children[0]}</div> */}
        <div className="center">{children}</div>
        {/* <div className="right">{children[2]}</div> */}
      </div>
    )
  }
}

NavBar.propTypes = {
  children: PropTypes.element
}

export default NavBar

二、通过props实现插槽

通过children来实现插槽虽然比较方便,但在子组件中需要通过索引来获取传入的元素很容易出错;如果想要精准的获取传入的元素,可以通过props来实现

在父组件中不仅可以使用props来向子组件传递数据、回调函数外,还可以传递元素

案例

App.jsx:

import React, { Component } from 'react'
import NavBar from './nav-bar'

export class App extends Component {
  render() {
    const btn = <button>按钮</button>

    return (
      <div>
        <NavBar
          leftSlot={btn}
          centerSlot={<h2>哈哈哈</h2>}
          rightSlot={<i>123</i>}
        />
      </div>
    )
  }
}

export default App

nav-bar/index.jsx:

import React, { Component } from 'react'

export class NavBarTwo extends Component {
  render() {
    const { leftSlot, centerSlot, rightSlot } = this.props

    return (
      <div className='nav-bar'>
        <div className="left">{leftSlot}</div>
        <div className="center">{centerSlot}</div>
        <div className="right">{rightSlot}</div>
      </div>
    )
  }
}

export default NavBarTwo

三、实现类似Vue中作用域插槽功能

在封装组件时,我们会遇到这样一个场景:在不同父组件中使用该子组件时,子组件展示的数据格式或样式需要由父组件去决定

比如在组件A中使用C,C中的数据展示为文本形态;但是在组件B中使用C时,C中的数据要展示为按钮或文本形态

由于React中并没有作用域插槽这个概念,我们可以换一种思路:

  • 父组件需要拿到子组件中渲染的数据
  • 该数据的样式与展示形态需要由父组件去决定

那么由React中父子组件通信的方式可知,如果子组件想向父组件传递数据的话可以通过调取父组件传递的回调函数来实现

所以,基于以上的场景,我们可以通过props向子组件中传递回调函数来实现:子组件中调取父组件传递过来的回调函数,并以参数的形式向该回调函数中传递子组件中渲染的数据信息;父组件获取到数据后,通过该数据来返回怎样的JSX即可

案例

比如在组件App中使用组件TabControl去展示不同类型的页签,并希望展示的页签的内容以按钮的形式呈现

App.jsx:

import React, { Component } from 'react'
import TabControl from './TabControl'

export class App extends Component {
  constructor() {
    super()

    this.state = {
      titles: ["最新", "热门", "精选"],
    }
  }

  render() {
    const { titles } = this.state

    return (
      <div className='app'>
        <TabControl 
          titles={titles} 
          itemType={item => <button>{item}</button>}
        />
      </div>
    )
  }
}

export default App

TabControl/index.jsx:

import React, { Component } from 'react'

export class TabControl extends Component {
  render() {
    const { titles, itemType } = this.props

    return (
      <div className='tab-control'>
        {
          titles.map((item, index) => {
            return (
              <div
                key={item}
              >
                {itemType(item)}
              </div>
            )
          })
        }
      </div>
    )
  }
}

export default TabControl

再比如,在App中期望根据页签种类来展示不同的形态:

import React, { Component } from 'react'
import TabControl from './TabControl'

export class App extends Component {
  constructor() {
    super()

    this.state = {
      titles: ["最新", "热门", "精选"],
    }
  }
  getTabItem(item) {
    if (item === "最新") {
      return <span>{item}</span>
    } else if (item === "热门") {
      return <button>{item}</button>
    } else {
      return <i>{item}</i>
    }
  }

  render() {
    const { titles } = this.state

    return (
      <div className='app'>
        <TabControl 
          titles={titles} 
          itemType={item => this.getTabItem(item)}
        />
      </div>
    )
  }
}

export default App