博客
React实践总结

React 使用总结

组件跨层级通信 - Context

在 Context 模式下有两个角色:

  • Provider:外层提供数据的组件
  • Consumer:内层获取数据的组件

实现 Context:

// 1. 创建上下文
const Context = React.createContext()
 
// 2. 获取Provider和Consumer
const Provider = Context.Provider
const Consumer = Context.Consumer

当 Context 使用时,应当避免直接使用 Provider 组件, 否则每次更改 Provider 的 value 内容都会重新渲染它的所有子组件, 应当抽离 Provider,单独封装一个层复合组件

下面是我总结出来的 Context 一种使用方式:

// 创建ContextProvider组件用于存取Provider数据
class ContextProvider extends Component {
  state = {
    counter: 0
  }
  add = (num = 1) => {
    this.setState({
      counter: this.state.counter + num
    })
  }
  render() {
    return (
      <Provider
        value={{
          ...this.state,
          add: this.add
        }}
      >
        {this.props.children}
      </Provider>
    )
  }
}
 
// 封装一个高阶组件withConsumer,它根据配置返回一个高阶组件
// 用Consumer方式使用时更方便
function withConsumer(Consumer) {
  return Comp => props => {
    return <Consumer>{value => <Comp {...value} />}</Consumer>
  }
}
 
class App extends Component {
  render() {
    return (
      <ContextProvider>
        <MiddleComp />
      </ContextProvider>
    )
  }
}
 
// 当Provider的value放在App的state进行管理的时候,
// 每次改变value,都会重新渲染MiddleComp组件
// 用ContextProvider替换后就不会重新渲染MiddleComp组件
const MiddleComp = () => {
  console.log('MiddleComp')
  return (
    <div>
      <Child />
      <Child />
      <Child />
    </div>
  )
}
 
// 子组件
const ChildComp = props => {
  return (
    <div
      onClick={() => {
        props.add()
      }}
    >
      {props.counter}
    </div>
  )
}
 
// 用withConsumer高阶组件包装子组件
const Child = withConsumer(Consumer)(ChildComp)

获取上下文内容时也可以用下面两种方式:

  1. class 组件时
class Child2 extends Component {
  static contextType = Context
  render() {
    return (
      <div
        onClick={() => {
          this.context.add()
        }}
      >
        {this.context.counter}
      </div>
    )
  }
}
  1. 函数组件时
const Child3 = props => {
  const context = useContext(Context)
  return (
    <div
      onClick={() => {
        context.add()
      }}
    >
      {context.counter}
    </div>
  )
}

重写 MiddleComp,你会发现效果是一样的

const MiddleComp = () => {
  console.log('MiddleComp')
  return (
    <div>
      <Child />
      <Child2 />
      <Child3 />
    </div>
  )
}

参考链接: 避免 React Context 导致的重复渲染 (opens in a new tab)

高阶组件

高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧,高阶组件是一个工厂函数,它接收一个参数并返回另外一个组件。

HOC的使用:

  1. 扩展单一组件,让它变得更强大,下面就是一种高阶组件简单使用:
const models = {
  a: {
    name: '水果店',
    productList: ['香蕉', '🍎', '梨']
  },
  b: {
    name: '超市',
    productList: ['地瓜', '🍉', '洋芋']
  }
}
 
const FruitShop = props => {
  return (
    <div>
      <h3>{props.name}</h3>
      <ul>
        {props.productList.map(a => (
          <li key={a}>{a}</li>
        ))}
      </ul>
    </div>
  )
}
 
// 定义高阶组件
const withCont = Comp => props => {
  const data = models[props.id]
  return <Comp {...props} {...data} />
}
 
// 包装
const ShopWithCont = withCont(FruitShop)
 
function HocTest() {
  return (
    <div>
      {['a', 'b'].map(item => (
        <ShopWithCont id={item} key={item} />
      ))}
    </div>
  )
}
  1. 自定义参数,返回一个新的高阶组件:
// 重写withCont
const withCont = mapModelToProps => {
  const data = mapModelToProps(models)
  return Comp => props => {
    return <Comp {...props} {...data} />
  }
}
 
// 使用, 这样组件就可以在使用的时候自定义需要的数据
const ShopWithCont = withCont(models => {
  return models['a']
})(FruitShop)
 
function HocTest() {
  return (
    <div>
      <ShopWithCont />
    </div>
  )
}
  1. 高阶组件嵌套使用:
// 添加一个withLog高阶组件,能够在组件挂载时输出日志
const withLog = Comp => {
  return class extends Component {
    componentDidMount() {
      console.log('didMount', this.props)
    }
    render() {
      return <Comp {...this.props} />
    }
  }
}
 
// 嵌套使用高阶组件
const ShopWithCont = withLog(
  withCont(models => {
    return models['a']
  })(FruitShop)
)
  1. 为了更好的实现嵌套使用多个高阶组件,实现一个 compose 方法,方法摘抄自redux (opens in a new tab) 组件库,自己的理解写在了注释上
// 实现compose
function compose(...funcs) {
  // 没有参数的话直接返回一个函数组件
  // compose()(a) -> a
  // 先返回一个返回自身参数的函数 -> 函数执行,返回a
  if (funcs.length === 0) {
    return arg => arg
  }
 
  // 只有一个函数参数时返回该函数参数
  // compose(withA)(a) -> withA(a)
  // 先返回一个withA函数 -> 函数执行,并且参数为a
  if (funcs.length === 1) {
    return funcs[0]
  }
 
  // 用reduce遍历funcs,并且形成最终需要执行的函数
  // compose(withA, withB, withC, withD)(a)
  // -> withA(withB(withC(withD(a))))
  return funcs.reduce((a, b) => {
    return (...args) => a(b(...args))
  })
  // 当a,b参数为withA,withB时, return (...args) -> withA(withB(...args))
  // 当a,b参数为上一轮返回函数,withC时,
  // return (...args2) -> (...args) => withA(withB(...args))(withC(...args2))
  // 执行结果为:
  // (...args2) => withA(withB(withC))(withC(...args2))
  // ... 持续执行,最终结果返回一个函数,函数的参数放在funcs最后一个函数:
  // (...argsN) => withA(withB(withC(...withN(...argsN))))
}
  1. 装饰器写法

高阶函数本身就是对装饰器模式的应用,可以利用 ES7 中出现的 装饰器 (opens in a new tab) 写法来更优雅的书写代码。如果项目不支持装饰器写法,需要额外配置,如果时 ts 项目,就可以直接支持装饰器写法,代码如下:

// 执行顺序从下网上, 同compose方法 -> withA(withB(TestComp))
@withA
@withB
class TestComp extends Component {
  render() {
    return (
      ...
    )
  }
}

注意:装饰器只能用在class上

组件组合 Composition

组件组合方式给予你足够的敏捷去定义自定义组件的外观和行为,这种方式更明确和安全。如果组件间有公用的非 UI 逻辑,将它们抽取为 React 组件导入使用,而不是继承它

组件复合通常有以下几种常用的方式:

  1. 直接控制 props.children 来实现

  2. 通过属性实现

    注意:props.children 是合法的 js 表达式,jsx 也会被编译为 js 对象。

主要记录一下 props.children 的实现方式:

  1. 常用方式:直接通过属性来实现具名插槽,children 为默认插槽
function Acticle(props) {
  const defaultFooter = '地址: 深圳 ~~'
  return (
    <div>
      <h1>{props.title}</h1>
      <hr />
      <div>{props.children}</div>
      <hr />
      <footer>
        {props.footer ? props.footer(defaultFooter) : defaultFooter}
      </footer>
    </div>
  )
}
 
function CompositionBase() {
  return (
    <Acticle
      title={<div>文章标题</div>}
      footer={address => (
        <div style={{ color: '#f00' }}>
          文章footer
          <br />
          {address}
        </div>
      )}
    >
      <div>文章内容</div>
      <div>文章内容</div>
      <div>文章内容</div>
    </Acticle>
  )
}
  1. 通过 children 传递对象,插值都放在 children
function Modal(props) {
  return (
    <div>
      {props.children.def}
      <hr />
      <footer>{props.children.footer}</footer>
    </div>
  )
}
 
function CompositionChildrenObj() {
  return (
    <Modal>
      {{
        def: (
          <>
            <h1>默认标题</h1>
            <div>默认内容</div>
          </>
        ),
        footer: (
          <button
            onClick={() => {
              alert('hello React!')
            }}
          >
            确定
          </button>
        )
      }}
    </Modal>
  )
}
  1. 通过 children 传递函数,可以获取组件内部数据,来进行下一步操作
function Dialog(props) {
  const messages = {
    foo: {
      title: 'foo',
      content: 'foo ~~'
    },
    bar: {
      title: 'bar',
      content: 'bar ~~'
    }
  }
  const { body, footer } = props.children(messages[props.type])
  return (
    <div>
      {body}
      <hr />
      <footer>{footer}</footer>
    </div>
  )
}
 
function CompositionChildrenFunc() {
  return (
    <Dialog type='bar'>
      {({ title, content }) => ({
        body: (
          <>
            <h1>{title}</h1>
            <div>{content}</div>
          </>
        ),
        footer: (
          <button
            onClick={() => {
              alert(title)
            }}
          >
            确定
          </button>
        )
      })}
    </Dialog>
  )
}
  1. 实现简单版 RadioGroup 和 Radio 组件

考虑:在 RadioGroup 上添加 name 属性,给内部所有 Radio 都加上 name 属性

注意:如果 Props.children 是 jsx,此时它是不能修改的

function RadioGroup(props) {
  const children = React.Children.map(props.children, child => {
    // 要修改child属性必须先克隆它
    return React.cloneElement(child, { name: props.name })
  })
  return children
}
 
// Radio传入value,name和children,注意区分
function Radio({ children, ...rest }) {
  return (
    <label>
      <input {...rest} type='radio' />
      {children}
    </label>
  )
}
 
function TestRadioGroup() {
  const data = [
    { label: '🍎', value: 1 },
    { label: '香蕉', value: 2 }
  ]
  return (
    <RadioGroup name='hello'>
      {data.map(item => (
        <Radio key={item.value} value={item.value}>
          {item.label}
        </Radio>
      ))}
    </RadioGroup>
  )
}

Hook

HookReact16.8的新增项,它可以让你在不编写class的情况下使用state以及其他到的React特性 Hook的特点:

  • 使你无需修改组件结构的情况下复用状态逻辑
  • 可将组件中相互关联的部分拆分成更小的函数,复杂组件将变得更容易理解
  • 更简洁、更易理解的代码

附上React 官方文档 - Hook (opens in a new tab)

后面再记录下一些 hook 的使用方法

总结

这是第一次写这种文章,希望这篇这篇文章能带给读者一些收获。 写文章太费时,还需要一个个写案例,真感谢平时大佬们花费那么多时间写文章分享给我们。

相关链接

避免 React Context 导致的重复渲染 (opens in a new tab)

React 中文文档 (opens in a new tab)