Skip to content

React 高阶组件 ​

一、HOC(高阶组件) ​

高阶组件是一个函数,接收一个组件,返回一个新组件:

jsx
const withComponent = (WrappedComponent) => {
  return class extends React.Component {
    render() {
      return <WrappedComponent {...this.props} />
    }
  }
}

// 使用
const HighComponent = withComponent(MyData)
const withComponent = (WrappedComponent) => {
  return class extends React.Component {
    render() {
      return <WrappedComponent {...this.props} />
    }
  }
}

// 使用
const HighComponent = withComponent(MyData)

请求数据的高阶组件 ​

jsx
const withFetch = (url) => (View) => {
  return class extends React.Component {
    state = { data: null }

    async componentDidMount() {
      const res = await fetch(url)
      const data = await res.json()
      this.setState({ data })
    }

    render() {
      if (!this.state.data) return <div>loading...</div>
      return <View data={this.state.data} {...this.props} />
    }
  }
}

// 使用
export default withFetch('https://api.example.com/banner')(Banner)
const withFetch = (url) => (View) => {
  return class extends React.Component {
    state = { data: null }

    async componentDidMount() {
      const res = await fetch(url)
      const data = await res.json()
      this.setState({ data })
    }

    render() {
      if (!this.state.data) return <div>loading...</div>
      return <View data={this.state.data} {...this.props} />
    }
  }
}

// 使用
export default withFetch('https://api.example.com/banner')(Banner)

二、React.memo ​

阻止函数组件不必要的重新渲染(浅比较 props):

jsx
const App = React.memo((props) => {
  return <div>{props.name}</div>
})

// 自定义比较函数
function areEqual(prevProps, nextProps) {
  return prevProps.seconds === nextProps.seconds
}

export default React.memo(App, areEqual)
const App = React.memo((props) => {
  return <div>{props.name}</div>
})

// 自定义比较函数
function areEqual(prevProps, nextProps) {
  return prevProps.seconds === nextProps.seconds
}

export default React.memo(App, areEqual)

与 Redux 配合 ​

jsx
export default connect(mapStateToProps)(React.memo(App))
export default connect(mapStateToProps)(React.memo(App))

三、React.PureComponent ​

类组件的浅比较版本:

jsx
class App extends React.PureComponent {
  render() {
    return <div>{this.props.name}</div>
  }
}
class App extends React.PureComponent {
  render() {
    return <div>{this.props.name}</div>
  }
}

注意:

  • 只做浅比较,props 层级不能太深
  • 传入对象时,展开后传递每个属性:
jsx
// 不推荐 — 传入整个对象
<Comment data={c} />

// 推荐 — 展开为单独属性
<Comment {...c} />
// 不推荐 — 传入整个对象
<Comment data={c} />

// 推荐 — 展开为单独属性
<Comment {...c} />

函数组件实现 PureComponent ​

jsx
const shouldComponentUpdate = (arePropsEqual) => (BaseComponent) => {
  return class extends React.Component {
    shouldComponentUpdate(nextProps) {
      return !arePropsEqual(this.props, nextProps)
    }
    render() {
      return <BaseComponent {...this.props} />
    }
  }
}

const Pure = (BaseComponent) => {
  const hoc = shouldComponentUpdate((prev, next) => !shallowEqual(prev, next))
  return hoc(BaseComponent)
}
const shouldComponentUpdate = (arePropsEqual) => (BaseComponent) => {
  return class extends React.Component {
    shouldComponentUpdate(nextProps) {
      return !arePropsEqual(this.props, nextProps)
    }
    render() {
      return <BaseComponent {...this.props} />
    }
  }
}

const Pure = (BaseComponent) => {
  const hoc = shouldComponentUpdate((prev, next) => !shallowEqual(prev, next))
  return hoc(BaseComponent)
}

四、lazy 和 Suspense(懒加载) ​

jsx
import React, { lazy, Suspense } from 'react'

const About = lazy(() => import('./pages/About'))

function App() {
  return (
    <Suspense fallback={<div>加载中</div>}>
      <About />
    </Suspense>
  )
}
import React, { lazy, Suspense } from 'react'

const About = lazy(() => import('./pages/About'))

function App() {
  return (
    <Suspense fallback={<div>加载中</div>}>
      <About />
    </Suspense>
  )
}

要点:

  • lazy 和 Suspense 必须搭配使用
  • fallback 必须是组件实例,fallback={<Loading/>} 而不是 fallback={Loading}
  • Suspense 不能捕获加载错误,需配合 componentDidCatch

Webpack 魔法注释 ​

jsx
const About = lazy(() => import(/* webpackChunkName: "about" */ './pages/About'))
const About = lazy(() => import(/* webpackChunkName: "about" */ './pages/About'))

带类型的 lazyImport ​

tsx
function lazyImport<T extends React.ComponentType<any>, I extends { [K2 in K]: T }, K extends keyof I>(
  factory: () => Promise<I>, name: K
): I {
  return Object.create({
    [name]: React.lazy(() => factory().then(module => ({ default: module[name] })))
  })
}

const { Home } = lazyImport(() => import('./Home.component'), 'Home')
function lazyImport<T extends React.ComponentType<any>, I extends { [K2 in K]: T }, K extends keyof I>(
  factory: () => Promise<I>, name: K
): I {
  return Object.create({
    [name]: React.lazy(() => factory().then(module => ({ default: module[name] })))
  })
}

const { Home } = lazyImport(() => import('./Home.component'), 'Home')

延迟加载(开发调试) ​

jsx
function slowImport(value, ms = 1000) {
  return new Promise(resolve => setTimeout(() => resolve(value), ms))
}

const About = React.lazy(() => slowImport(import('./pages/About'), 1000))
function slowImport(value, ms = 1000) {
  return new Promise(resolve => setTimeout(() => resolve(value), ms))
}

const About = React.lazy(() => slowImport(import('./pages/About'), 1000))

五、React.createPortal ​

将子节点渲染到父组件 DOM 层次之外:

jsx
import { createPortal } from 'react-dom'

class Modal extends React.Component {
  constructor() {
    super()
    this.el = document.createElement('div')
    this.el.setAttribute('class', 'modal-mask')
  }

  componentDidMount() { document.body.appendChild(this.el) }
  componentWillUnmount() { document.body.removeChild(this.el) }

  render() {
    return createPortal(this.props.children, this.el)
  }
}
import { createPortal } from 'react-dom'

class Modal extends React.Component {
  constructor() {
    super()
    this.el = document.createElement('div')
    this.el.setAttribute('class', 'modal-mask')
  }

  componentDidMount() { document.body.appendChild(this.el) }
  componentWillUnmount() { document.body.removeChild(this.el) }

  render() {
    return createPortal(this.props.children, this.el)
  }
}

六、React.Children ​

操作 this.props.children 的工具函数:

jsx
React.Children.map(children, fn)        // 遍历
React.Children.forEach(children, fn)     // 遍历(无返回值)
React.Children.count(children)           // 计数
React.Children.only(children)            // 验证唯一子节点
React.Children.toArray(children)          // 转数组(可排序)

// 循环修改 children 属性(需先克隆)
{
  React.Children.map(props.children, (node) => {
    return React.cloneElement(node, {
      value: '',
      onChange: this.handleChange
    })
  })
}
React.Children.map(children, fn)        // 遍历
React.Children.forEach(children, fn)     // 遍历(无返回值)
React.Children.count(children)           // 计数
React.Children.only(children)            // 验证唯一子节点
React.Children.toArray(children)          // 转数组(可排序)

// 循环修改 children 属性(需先克隆)
{
  React.Children.map(props.children, (node) => {
    return React.cloneElement(node, {
      value: '',
      onChange: this.handleChange
    })
  })
}

注意:children 是只读的,需使用 React.cloneElement 克隆后再修改。