组件跨层级通信 - Context

context参考

范例:模拟redux存放全局状态,在组件间共享

  1. // 组件跨层级通信
  2. import React, { Component } from 'react'
  3. // 1、创建上下文对象
  4. const Context = React.createContext()
  5. // 2、获取 Provider 和 Consumer
  6. const { Provider, Consumer } = Context
  7. // Child显示计数器,并能修改它,多个Child之间需要共享数据
  8. function Child(props) {
  9. return (
  10. <div onClick={ () => props.add() }>{props.counter}</div>
  11. )
  12. }
  13. export default class ContextTest extends Component {
  14. // state 是要传递的数据
  15. state = {
  16. counter: 0
  17. }
  18. // add 方法可以修改状态
  19. add = () => {
  20. this.setState({ counter: this.state.counter + 1 })
  21. }
  22. render() {
  23. return (
  24. <Provider value={{ counter: this.state.counter, add: this.add }}>
  25. <Consumer>{ value => <Child {...value}></Child> }</Consumer>
  26. <Consumer>{ value => <Child {...value}></Child> }</Consumer>
  27. <Consumer>{ value => <Child {...value}></Child> }</Consumer>
  28. </Provider>
  29. )
  30. }
  31. }

高阶组件-HOC

HOC参考
高阶组件:高阶组件是一个工厂函数,它接收一个组件并返回另一个组件
范例:为展示组件添加获取数据的能力

  1. import React from 'react'
  2. // Lesson 保证功能单一,它不关心数据来源,只负责显示
  3. function Lesson(props) {
  4. return (
  5. <div>
  6. {props.stage} - {props.title}
  7. </div>
  8. )
  9. }
  10. // 模拟数据
  11. const lessons = [
  12. {
  13. stage: '高中',
  14. title: '数学'
  15. },
  16. {
  17. stage: '初中',
  18. title: '英语'
  19. },
  20. {
  21. stage: '小学',
  22. title: '语文'
  23. }
  24. ]
  25. // 定义高阶组件 withContent
  26. // 包装后的组件可以根据传入的参数显示组件
  27. // ES5 写法
  28. // function withContent (Comp) {
  29. // return function (props) {
  30. // const content = lessons[props.idx]
  31. // return <Comp {...content}/>
  32. // }
  33. // }
  34. // ES6 写法
  35. const withContent = Comp => props => {
  36. const content = lessons[props.idx]
  37. return <Comp {...content}/>
  38. }
  39. const LessonWithContent = withContent(Lesson)
  40. export default function HOCTest() {
  41. return (
  42. <div>
  43. { [0, 0, 0].map((item, idx) => <LessonWithContent key={idx} idx={idx} />)}
  44. </div>
  45. )
  46. }

范例:改造前面案例使上下文使用更优雅

这里和前面的范例有所不同,withConsumer 高阶组件工厂,根据配置返回一个高阶组件,,大家想一想 react-redux 里面的 connect 函数是不是和这个很类似。

  1. import React, { Component } from 'react'
  2. // 1、创建上下文对象
  3. const Context = React.createContext()
  4. // 2、获取 Provider 和 Consumer
  5. const { Provider, Consumer } = Context
  6. function Child(props) {
  7. return (
  8. <div onClick={ () => props.add() }>{props.counter}</div>
  9. )
  10. }
  11. // withConsumer 高阶组件工厂,根据配置返回一个高阶组件
  12. const withConsumer = Consumer => {
  13. return Comp => props => {
  14. return <Consumer>{ value => <Comp {...value}/>}</Consumer>
  15. }
  16. }
  17. let ChildWithConsumer = withConsumer(Consumer)(Child)
  18. export default class ContextTest extends Component {
  19. state = {
  20. counter: 0
  21. }
  22. add = () => {
  23. this.setState({ counter: this.state.counter + 1 })
  24. }
  25. render() {
  26. return (
  27. <Provider value={{ counter: this.state.counter, add: this.add }}>
  28. <ChildWithConsumer />
  29. <ChildWithConsumer />
  30. <ChildWithConsumer />
  31. </Provider>
  32. )
  33. }
  34. }

链式调用

  1. // withLog高阶组件,在组件挂载时输出日志
  2. const withLog = Comp => {
  3. return class extends React.Component {
  4. componentDidMount () {
  5. console.log('componentDidMount', this.props)
  6. }
  7. render () {
  8. return <Comp { ...this.props }/>
  9. }
  10. }
  11. }
  12. // 高阶组件的链式调用(层级过多的话,代码可读性会很差,可以使用装饰器语法)
  13. const LessonWithContent = withLog(withContent(Lesson))

装饰器写法

CRA项目中默认不支持js代码使用装饰器语法,需要安装并配置 @babel/plugin-proposal-decorators 之后才能使用:

  1. // package.json
  2. "babel": {
  3. "plugins": [
  4. [
  5. "@babel/plugin-proposal-decorators",
  6. {
  7. "legacy": true
  8. }
  9. ]
  10. ],
  11. "presets": [
  12. "react-app"
  13. ]
  14. },

范例:通过装饰器来使用高阶组件

  1. // 装饰器
  2. import React, { Component } from 'react'
  3. // 模拟数据
  4. const lessons = [
  5. {
  6. stage: '高中',
  7. title: '数学'
  8. },
  9. {
  10. stage: '初中',
  11. title: '英语'
  12. },
  13. {
  14. stage: '小学',
  15. title: '语文'
  16. }
  17. ]
  18. // 定义高阶组件 withContent
  19. // 包装后的组件可以根据传入的参数显示组件
  20. const withContent = Comp => props => {
  21. const content = lessons[props.idx]
  22. return <Comp {...content}/>
  23. }
  24. // withLog高阶组件,在组件挂载时输出日志
  25. const withLog = Comp => {
  26. return class extends React.Component {
  27. componentDidMount () {
  28. console.log('componentDidMount', this.props)
  29. }
  30. render () {
  31. return <Comp { ...this.props }/>
  32. }
  33. }
  34. }
  35. // 装饰器写法 @withLog @withContent 装饰器语法只能用在 class 组件上
  36. @withLog
  37. @withContent
  38. class Lesson extends Component {
  39. render() {
  40. return (
  41. <div>
  42. {this.props.stage} - {this.props.title}
  43. </div>
  44. )
  45. }
  46. }
  47. export default function DecoraterTest() {
  48. return (
  49. <div>
  50. { [0, 0, 0].map((item, idx) => <Lesson key={idx} idx={idx} />)}
  51. </div>
  52. )
  53. }

组件复合 - Composition

组件复合:类似于 Vue 的插槽,具体内容由外部传入

普通插槽

范例:Dialog组件负责展示,内容从外部传入即可,components/Composition.js**

  1. // 组件复合:类似于 vue 的插槽,具体内容由外部传入
  2. import React, { Component } from 'react'
  3. function Dialog(props) {
  4. // props.children:children 是一个合法的 js 表达式
  5. return (
  6. <div style={{border: '1px solid #000'}}>
  7. { props.children }
  8. </div>
  9. )
  10. }
  11. export default class Composition extends Component {
  12. render() {
  13. return (
  14. <div>
  15. <Dialog>
  16. <h1>组件复合</h1>
  17. <p>通过组件符合去自定义组件内容</p>
  18. </Dialog>
  19. </div>
  20. )
  21. }
  22. }

具名插槽

范例:传个对象进去,key表示具名插槽

  1. // 组件复合:类似于 vue 的插槽,具体内容由外部传入
  2. import React, { Component } from 'react'
  3. function Dialog(props) {
  4. // props.children:children 是一个合法的 js 表达式
  5. return (
  6. <div style={{border: '1px solid #000'}}>
  7. { props.children.def }
  8. <div>
  9. { props.children.footer }
  10. </div>
  11. </div>
  12. )
  13. }
  14. export default class Composition extends Component {
  15. render() {
  16. return (
  17. <div>
  18. <Dialog>
  19. {
  20. {
  21. def: (
  22. // React.Fragment的速记语法<></>,仅在最新版本(和Babel 7+)中受支持
  23. <>
  24. <h1>组件复合</h1>
  25. <p>通过组件符合去自定义组件内容</p>
  26. </>
  27. ),
  28. footer: (
  29. <button onClick={ () => alert('组件复合') }>确定</button>
  30. )
  31. }
  32. }
  33. </Dialog>
  34. </div>
  35. )
  36. }
  37. }

作用域插槽

范例:传个函数进去,实现作用于插槽的功能

  1. // 组件复合进阶用法:实现作用域插槽
  2. import React, { Component } from 'react'
  3. function Dialog(props) {
  4. const message = {
  5. foo: { title: 'foo', content: 'foo~' },
  6. bar: { title: 'bar', content: 'bar~' }
  7. }
  8. const { def, footer } = props.children(message[props.msg])
  9. return (
  10. <div style={{border: '1px solid #000'}}>
  11. { def }
  12. <div>
  13. { footer }
  14. </div>
  15. </div>
  16. )
  17. }
  18. export default class HComposition extends Component {
  19. render() {
  20. return (
  21. <div>
  22. <Dialog msg="foo">
  23. {
  24. ({title, content}) => (
  25. {
  26. def: (
  27. <>
  28. <h1>{title}</h1>
  29. <p>{content}</p>
  30. </>
  31. ),
  32. footer: (
  33. <button onClick={ () => alert('组件复合') }>确定</button>
  34. )
  35. }
  36. )
  37. }
  38. </Dialog>
  39. </div>
  40. )
  41. }
  42. }

实现修改 children

如果 props.children 是 jsx,此时它是不能修改的。
范例:实现 RadioGroup 和 Radio 组件,可通过 RadioGroup 设置 Radio 的 name

  1. // 组件复合进阶用法:实现修改 children
  2. import React, { Component } from 'react'
  3. function RadioGroup (props) {
  4. // 如果 props.children是jsx,不能直接修改它
  5. return (
  6. <div>
  7. {
  8. React.Children.map(props.children, radio => {
  9. // 要修改虚拟DOM(jsx编译之后的结果),只能克隆后重新设置属性,不能直接修改
  10. // 参数1:克隆对象
  11. // 参数2:设置的属性
  12. return React.cloneElement(radio, { name: props.name })
  13. })
  14. // props.children.map(radio => {
  15. // return React.cloneElement(radio, { name: props.name })
  16. // })
  17. }
  18. </div>
  19. )
  20. }
  21. function Radio ({children, ...rest}) {
  22. return (
  23. <label>
  24. <input type="radio" {...rest} />
  25. { children }
  26. </label>
  27. )
  28. }
  29. export default class HComposition extends Component {
  30. render() {
  31. return (
  32. <div>
  33. <RadioGroup name="mvvm">
  34. <Radio value="vue">vue</Radio>
  35. <Radio value="react">react</Radio>
  36. <Radio value="ng">angular</Radio>
  37. </RadioGroup>
  38. </div>
  39. )
  40. }
  41. }