插槽
在开发中,我们抽取了一个组件,但是为了让这个组件具备更强的通用性,我们不能将组件中的内容限制为固定的div、span等等这些元素
我们应该让使用者可以决定某一块区域到底存放什么内容
因为React中所有元素都是ReactElement对象,所以在React中不需要存在对应的插槽
我们可以使用如下两种方式来模拟插槽:
- 组件的children子元素
- props属性传递React元素
children模拟插槽
每个组件都可以获取到 props.children:它包含组件的开始标签和结束标签之间的内容
当只有一个子元素的时候,props.children就是那个子元素
如果存在多个子元素的时候,props.children是由所有的子元素组成的数组
父组件
export class App extends PureComponent {
render() {
return (
<Cpn>
<button>left</button>
<h2>center</h2>
<div>right</div>
</Cpn>
)
}
}
子组件
export class Cpn extends PureComponent {
render() {
let { children } = this.props
children = Array.isArray(children) ? children : [ children ]
return (
<div className='wrapper'>
<div className="left">
{ children[0] }
</div>
<div className="center">
{ children[1] }
</div>
<div className="right">
{ children[2] }
</div>
</div>
)
}
}
props实现插槽
通过children实现的方案虽然可行,但是有一个弊端:通过索引值获取传入的元素很容易出错,不能精准的获取传入的元素
另外一个种方案就是使用 props 实现
通过具体的属性名,可以让我们在传入和获取时更加的精准
一般为了见名知意,对应的属性名会以Slot结尾
父组件
export class App extends PureComponent {
render() {
return (
<Cpn
leftSlot={<button>left</button>}
centerSlot={<h2>center</h2>}
rightSlot={<div>right</div>}
/>
)
}
}
子组件
export class Cpn extends PureComponent {
render() {
let { leftSlot, centerSlot, rightSlot } = this.props
return (
<div className='wrapper'>
<div className="left">
{ leftSlot }
</div>
<div className="center">
{ centerSlot }
</div>
<div className="right">
{ rightSlot }
</div>
</div>
)
}
}
模拟作用域插槽
父组件
export class App extends PureComponent {
render() {
return (
<Cpn
leftSlot={ctx => <button>{ ctx }</button>}
centerSlot={ctx => <h2>{ ctx }</h2>}
rightSlot={ctx => <div>{ ctx }</div>}
/>
)
}
}
子组件
export class Cpn extends PureComponent {
constructor(props) {
super(props)
this.state = {
leftCtx: 'leftCtx',
centerCtx: 'centerCtx',
rightCtx: 'rightCtx'
}
}
render() {
let { leftSlot, centerSlot, rightSlot } = this.props
const { leftCtx, centerCtx, rightCtx } = this.state
return (
<div className='wrapper'>
<div className="left">
{ leftSlot(leftCtx) }
</div>
<div className="center">
{ centerSlot(centerCtx) }
</div>
<div className="right">
{ rightSlot(rightCtx) }
</div>
</div>
)
}
}
context
在开发中,比较常⻅的数据传递方式是通过props属性自上而下(由父到子)进行传递
但是对于有一些场景:比如一些数据需要在多个组件中进行共享(地区偏好、UI主题、用户登录状态、用户信息等)
如果我们在顶层的App中定义这些信息,之后一层层传递下去,那么对于一些中间层不需要数据的组件来说,是一种冗余的操作
为此React提供了一个API:Context,专门用于非父子组件间的数据共享
Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props
Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言
基本使用
/src/context/themeContext.js
import React from 'react'
// 因为在提供数据组件和使用数据组件都需要使用对应的ctx,所以将ctx在单独的js文件中进行导出
// ctx命名约定:
// 1. 首字母大写
// 2. 以Context结尾
export const ThemeContext = React.createContext()
context提供者
import React, { PureComponent } from 'react'
import ItemList from './components/ItemList'
import { ThemeContext } from '../context/themeContext'
export class App extends PureComponent {
render() {
return (
// 使用ctx的生产者为后代传递对应的数据
// 需要传递的数据定义在value属性中
<ThemeContext.Provider value={{color: 'skyblue', warnColor: 'red'}}>
{/*
将需要使用数据的组件使用ctx进行包裹
所以和vue的provide和inject类似
ctx只能由祖先组件向后代组件传递对应数据
一个组件可以被多个ctx所包裹
*/}
{/*
ItemList内部使用了Item组件
ItemList组件是本例中的中间测试组件
*/}
<ItemList />
</ThemeContext.Provider>
)
}
}
export default App
数据使用者
类组件
import React, { PureComponent } from 'react'
// 在需要使用ctx的组件中引入对应组件
import { ThemeContext } from '../../../context/themeContext'
export class Item extends PureComponent {
// 因为可能提供了多个ctx,所以需要指定使用哪一个ctx
static contextType = ThemeContext
render() {
return (
<div>
{/*
我们可以使用this.context来获取到指定的context
如果没有设置contextType对应的值,那么this.context的值为{}
*/}
{ this.context.color } - { this.context.warnColor }
</div>
)
}
}
export default Item
函数组件
import React from 'react'
import { ThemeContext } from '../../../context/themeContext'
function Item() {
return (
// 函数组件中没有this指向
// 所以需要使用context中的消费者组件
<ThemeContext.Consumer>
{
// 消费者组件中需要传递一个返回需要渲染vDOM的函数作为其children
// 在实际被调用的时候会将共享的数据传递过来
theme => {
/* Consumer中对应的函数需要返回实际所需要渲染的vDOM */
return <div>
<div>{ theme.color }</div>
<div>{ theme.warnColor }</div>
</div>
}
}
</ThemeContext.Consumer>
)
}
export default Item
相关概念
React.createContext
- 创建一个需要共享的Context对象
- 如果一个组件订阅了Context,那么这个组件会从离自身最近的那个匹配的 Provider 中读取到当前的context值
- defaultValue是组件在顶层查找过程中没有找到对应的Provider,那么就使用默认值
Context.Provider
- 每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化
- Provider 接收一个 value 属性,传递给消费组件
- 一个 Provider 可以和多个消费组件有对应关系
- 多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据
- 当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染
Class.contextType
- 挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象
- 这能让你使用 this.context 来消费最近 Context 上的那个值
- 可以在任何生命周期中访问到它,包括 render 函数中
Context.Consumer
- React 组件也可以订阅到 context 变更。这能让你在 函数式组件 中完成订阅 context
- 这里需要 函数作为子元素(function as child)这种做法
- 这个函数接收当前的 context 值,返回一个 React 节点
也就是说,以下场景需要使用Context.Consumer
- 当使用value的组件是一个函数式组件时
- 当组件中需要使用多个Context时
数据生产者
import React, { PureComponent } from 'react'
import ItemList from './components/ItemList'
import { ThemeContext } from '../context/themeContext'
import { UserContext } from '../context/userContext'
export class App extends PureComponent {
render() {
return (
<ThemeContext.Provider value={{color: 'skyblue', warnColor: 'red'}}>
<UserContext.Provider value={{name:'Klaus', age: 23}}>
<ItemList />
</UserContext.Provider>
</ThemeContext.Provider>
)
}
}
export default App
数据消费者
import React, { PureComponent } from 'react'
import { ThemeContext } from '../../../context/themeContext'
import { UserContext } from '../../../context/userContext'
export class Item extends PureComponent {
static contextType = ThemeContext
render() {
return (
<div>
{ this.context.color } - { this.context.warnColor }
<UserContext.Consumer>
{
userInfo => <div>{userInfo.name} - {userInfo.age}</div>
}
</UserContext.Consumer>
</div>
)
}
}
export default Item
默认值
import React from 'react'
// 在createContext方法中可以传入一个对象作为context的默认值
// 如果使用context的组件并不是ThemeContext.Provider的后代组件的时候,就会启用默认值
export const ThemeContext = React.createContext({color: 'gray', warnColor: 'yellow'})
事件总线
/src/utils/index.js
import Mitt from 'mitt'
export default Mitt()
父组件
import React, { PureComponent } from 'react'
import ItemList from './components/ItemList'
import mitt from '../utils'
export class App extends PureComponent {
constructor(props) {
super(props)
this.state = {
name: 'Klaus'
}
}
// 因为事件总线中的this指向的值是undefined
// 所以为了在函数内部可以获取正确的this指向,推荐将函数编写为箭头函数
changeName = name => {
this.setState({
name
})
}
componentDidMount() {
mitt.on('changeName', this.changeName)
}
componentWillUnmount() {
mitt.off('changeName', this.changeName)
}
render() {
return (
<div>
<div>{ this.state.name }</div>
<ItemList />
</div>
)
}
}
export default App
子组件
import React, { PureComponent } from 'react'
import mitt from '../../../utils'
export class Item extends PureComponent {
changeName() {
// mitt在触发事件的时候,只能传递两个参数
// 参数一: 事件名
// 参数二: 需要传递的参数,如果需要传递多个参数可以使用对象进行传递
mitt.emit('changeName', 'Steven')
}
render() {
return (
<div>
<button onClick={this.changeName}>change</button>
</div>
)
}
}
export default Item