组件跨层级通信 - Context
范例:模拟redux存放全局状态,在组件间共享
// 组件跨层级通信
import React, { Component } from 'react'
// 1、创建上下文对象
const Context = React.createContext()
// 2、获取 Provider 和 Consumer
const { Provider, Consumer } = Context
// Child显示计数器,并能修改它,多个Child之间需要共享数据
function Child(props) {
return (
<div onClick={ () => props.add() }>{props.counter}</div>
)
}
export default class ContextTest extends Component {
// state 是要传递的数据
state = {
counter: 0
}
// add 方法可以修改状态
add = () => {
this.setState({ counter: this.state.counter + 1 })
}
render() {
return (
<Provider value={{ counter: this.state.counter, add: this.add }}>
<Consumer>{ value => <Child {...value}></Child> }</Consumer>
<Consumer>{ value => <Child {...value}></Child> }</Consumer>
<Consumer>{ value => <Child {...value}></Child> }</Consumer>
</Provider>
)
}
}
高阶组件-HOC
HOC参考
高阶组件:高阶组件是一个工厂函数,它接收一个组件并返回另一个组件
范例:为展示组件添加获取数据的能力
import React from 'react'
// Lesson 保证功能单一,它不关心数据来源,只负责显示
function Lesson(props) {
return (
<div>
{props.stage} - {props.title}
</div>
)
}
// 模拟数据
const lessons = [
{
stage: '高中',
title: '数学'
},
{
stage: '初中',
title: '英语'
},
{
stage: '小学',
title: '语文'
}
]
// 定义高阶组件 withContent
// 包装后的组件可以根据传入的参数显示组件
// ES5 写法
// function withContent (Comp) {
// return function (props) {
// const content = lessons[props.idx]
// return <Comp {...content}/>
// }
// }
// ES6 写法
const withContent = Comp => props => {
const content = lessons[props.idx]
return <Comp {...content}/>
}
const LessonWithContent = withContent(Lesson)
export default function HOCTest() {
return (
<div>
{ [0, 0, 0].map((item, idx) => <LessonWithContent key={idx} idx={idx} />)}
</div>
)
}
范例:改造前面案例使上下文使用更优雅
这里和前面的范例有所不同,withConsumer 高阶组件工厂,根据配置返回一个高阶组件,,大家想一想 react-redux 里面的 connect 函数是不是和这个很类似。
import React, { Component } from 'react'
// 1、创建上下文对象
const Context = React.createContext()
// 2、获取 Provider 和 Consumer
const { Provider, Consumer } = Context
function Child(props) {
return (
<div onClick={ () => props.add() }>{props.counter}</div>
)
}
// withConsumer 高阶组件工厂,根据配置返回一个高阶组件
const withConsumer = Consumer => {
return Comp => props => {
return <Consumer>{ value => <Comp {...value}/>}</Consumer>
}
}
let ChildWithConsumer = withConsumer(Consumer)(Child)
export default class ContextTest extends Component {
state = {
counter: 0
}
add = () => {
this.setState({ counter: this.state.counter + 1 })
}
render() {
return (
<Provider value={{ counter: this.state.counter, add: this.add }}>
<ChildWithConsumer />
<ChildWithConsumer />
<ChildWithConsumer />
</Provider>
)
}
}
链式调用
// withLog高阶组件,在组件挂载时输出日志
const withLog = Comp => {
return class extends React.Component {
componentDidMount () {
console.log('componentDidMount', this.props)
}
render () {
return <Comp { ...this.props }/>
}
}
}
// 高阶组件的链式调用(层级过多的话,代码可读性会很差,可以使用装饰器语法)
const LessonWithContent = withLog(withContent(Lesson))
装饰器写法
CRA项目中默认不支持js代码使用装饰器语法,需要安装并配置 @babel/plugin-proposal-decorators 之后才能使用:
// package.json
"babel": {
"plugins": [
[
"@babel/plugin-proposal-decorators",
{
"legacy": true
}
]
],
"presets": [
"react-app"
]
},
范例:通过装饰器来使用高阶组件
// 装饰器
import React, { Component } from 'react'
// 模拟数据
const lessons = [
{
stage: '高中',
title: '数学'
},
{
stage: '初中',
title: '英语'
},
{
stage: '小学',
title: '语文'
}
]
// 定义高阶组件 withContent
// 包装后的组件可以根据传入的参数显示组件
const withContent = Comp => props => {
const content = lessons[props.idx]
return <Comp {...content}/>
}
// withLog高阶组件,在组件挂载时输出日志
const withLog = Comp => {
return class extends React.Component {
componentDidMount () {
console.log('componentDidMount', this.props)
}
render () {
return <Comp { ...this.props }/>
}
}
}
// 装饰器写法 @withLog @withContent 装饰器语法只能用在 class 组件上
@withLog
@withContent
class Lesson extends Component {
render() {
return (
<div>
{this.props.stage} - {this.props.title}
</div>
)
}
}
export default function DecoraterTest() {
return (
<div>
{ [0, 0, 0].map((item, idx) => <Lesson key={idx} idx={idx} />)}
</div>
)
}
组件复合 - Composition
组件复合:类似于 Vue 的插槽,具体内容由外部传入
普通插槽
范例:Dialog组件负责展示,内容从外部传入即可,components/Composition.js**
// 组件复合:类似于 vue 的插槽,具体内容由外部传入
import React, { Component } from 'react'
function Dialog(props) {
// props.children:children 是一个合法的 js 表达式
return (
<div style={{border: '1px solid #000'}}>
{ props.children }
</div>
)
}
export default class Composition extends Component {
render() {
return (
<div>
<Dialog>
<h1>组件复合</h1>
<p>通过组件符合去自定义组件内容</p>
</Dialog>
</div>
)
}
}
具名插槽
范例:传个对象进去,key表示具名插槽
// 组件复合:类似于 vue 的插槽,具体内容由外部传入
import React, { Component } from 'react'
function Dialog(props) {
// props.children:children 是一个合法的 js 表达式
return (
<div style={{border: '1px solid #000'}}>
{ props.children.def }
<div>
{ props.children.footer }
</div>
</div>
)
}
export default class Composition extends Component {
render() {
return (
<div>
<Dialog>
{
{
def: (
// React.Fragment的速记语法<></>,仅在最新版本(和Babel 7+)中受支持
<>
<h1>组件复合</h1>
<p>通过组件符合去自定义组件内容</p>
</>
),
footer: (
<button onClick={ () => alert('组件复合') }>确定</button>
)
}
}
</Dialog>
</div>
)
}
}
作用域插槽
范例:传个函数进去,实现作用于插槽的功能
// 组件复合进阶用法:实现作用域插槽
import React, { Component } from 'react'
function Dialog(props) {
const message = {
foo: { title: 'foo', content: 'foo~' },
bar: { title: 'bar', content: 'bar~' }
}
const { def, footer } = props.children(message[props.msg])
return (
<div style={{border: '1px solid #000'}}>
{ def }
<div>
{ footer }
</div>
</div>
)
}
export default class HComposition extends Component {
render() {
return (
<div>
<Dialog msg="foo">
{
({title, content}) => (
{
def: (
<>
<h1>{title}</h1>
<p>{content}</p>
</>
),
footer: (
<button onClick={ () => alert('组件复合') }>确定</button>
)
}
)
}
</Dialog>
</div>
)
}
}
实现修改 children
如果 props.children 是 jsx,此时它是不能修改的。
范例:实现 RadioGroup 和 Radio 组件,可通过 RadioGroup 设置 Radio 的 name
// 组件复合进阶用法:实现修改 children
import React, { Component } from 'react'
function RadioGroup (props) {
// 如果 props.children是jsx,不能直接修改它
return (
<div>
{
React.Children.map(props.children, radio => {
// 要修改虚拟DOM(jsx编译之后的结果),只能克隆后重新设置属性,不能直接修改
// 参数1:克隆对象
// 参数2:设置的属性
return React.cloneElement(radio, { name: props.name })
})
// props.children.map(radio => {
// return React.cloneElement(radio, { name: props.name })
// })
}
</div>
)
}
function Radio ({children, ...rest}) {
return (
<label>
<input type="radio" {...rest} />
{ children }
</label>
)
}
export default class HComposition extends Component {
render() {
return (
<div>
<RadioGroup name="mvvm">
<Radio value="vue">vue</Radio>
<Radio value="react">react</Radio>
<Radio value="ng">angular</Radio>
</RadioGroup>
</div>
)
}
}