React 的自定义Hooks和异步组件

495 阅读4分钟

1.自定义Hooks

注意:自定义Hooks本质上还是实现一个函数,关键在于实现逻辑。

1.1 setTitle hook

import { useEffect } from 'react'

const useTitle = (title) => {
  useEffect(() => {
    document.title = title
  }, [])

  return
}

export default useTitle

const App = () => {
  useTitle('new title')
  return <div>home</div>
}

1.2 update hook

import { useState } from 'react'

const useUpdate = () => {
  const [, setFlag] = useState()
  const update = () => {
    setFlag(Date.now())
  }
  return update
}

export default useUpdate

// 实际使用
const App = (props) => {
  // ...
  const update = useUpdate()
  return <div>
    {Date.now()}
    <div><button onClick={update}>update</button></div>
  </div>
}

1.3 useScroll hook

import { useState, useEffect } from 'react'

const useScroll = (scrollRef) => {
  const [pos, setPos] = useState([0,0])

  useEffect(() => {
    function handleScroll(e){
      setPos([scrollRef.current.scrollLeft, scrollRef.current.scrollTop])
    }
    scrollRef.current.addEventListener('scroll', handleScroll)
    return () => {
      scrollRef.current.removeEventListener('scroll', handleScroll)
    }
  }, [])
  
  return pos
}

export default useScroll

// 用法
import React, { useRef } from 'react'
import { useScroll } from 'hooks'

const Home = (props) => {
  const scrollRef = useRef(null)
  const [x, y] = useScroll(scrollRef)

  return <div>
      <div ref={scrollRef}>
        <div className="innerBox"></div>
      </div>
      <div>{ x }, { y }</div>
    </div>
}

2. Hooks VS HOC

  1. Hook最典型的就是取代掉生命周期中大多数的功能,可以把更相关的逻辑放在一起,而非零散在各个生命周期方法中;
  2. 高阶组件可以将外部的属性功能到一个基础 Component 中,更多作为扩展能力的插件(如 react-swipeable-views中的 autoPlay 高阶组件,通过注入状态化的 props 的方式对组件进行功能扩展,而不是直接将代码写在主库中);
  3. Hook的写法可以让代码更加紧凑,更适合做 Controller 或者需要内聚的相关逻辑,一般与目标组件内强依赖,HOC更加强调对原先组件能力的扩展;

3. 异步组件

随着项目的增长,代码包也会随之增长,尤其是在引入第三方库的情况下,要避免因体积过大导致加载时间过长。

React16.6中,引入了 React.lazy 和 React.Suspense 两个API,再配合动态import()语法就可以实现组件代码打包分割和异步加载。

传统模式:渲染组件 -> 请求数据 -> 再渲染组件
异步组件:请求数据 -> 渲染组件;

// demo
import React, { lazy, Suspense } from 'react';
// lazy 和 Suspense 配套使用,react原生支持代码分割
const About = lazy(() => import(/* webpackChunkName: "about" */'./About'));
class App extends React.Component {
  render() {
    return (
      <div className="App">
        <h1>App</h1>
        <Suspense fallback={<div>loading</div>}>
          <About />
        </Suspense>
      </div>
    );
  }
}
export default App;

3.1 前置基础

  • 动态import

相对于静态import的 import XX from XX,动态import指在运行时加载。

import('./test.js').then(test => {
    // ...
});
// 可见,是实现了Promsie规范的,回调函数为返回的模块
  • 错误边界

React V16中引入,部分UI的JS错误不会导致整个应用崩溃;错误边界时一种 React 组件,错误边界在 渲染期间、生命周期方法和整个组件树的构造函数 中捕获错误,且会渲染出备用UI而不是崩溃的组件。

// comp ErrorBoundary 
import React from 'react'

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }
  static getDerivedStateFromError(error) {
    // 更新 state 使下一次渲染能够显示降级后的 UI
    return { hasError: true };
  }
  componentDidCatch(error, errorInfo) {
    // 你同样可以将错误日志上报给服务器
    console.log(error, errorInfo)
  }
  render() {
    if (this.state.hasError) {
        // 你可以自定义降级后的 UI 并渲染
        return <h1>Something went wrong.</h1>;
    }
    return this.props.children;
  }
}
export default ErrorBoundary

// comp App
import React, from 'react';
import ErrorBoundary from './ErrorBoundary'
class App extends React.Component {
  state = {
      count: 1
  }
  render() {
    const { count } = this.state
    if (count === 3) {
        throw new Error('I crashed!');
    }
    return (
      <ErrorBoundary>
        <h1>App</h1>
        <p>{count}</p>
        <button onClick={() => this.setState({ count: count + 1 })}>add</button>
      </ErrorBoundary>
    )
  }
}
export default App;

3.2 手写异步组件

Suspense 组件需要等待异步组件加载完成再渲染异步组件的内容。

  1. lazy wrap住异步组件,React第一次加载组件的时候,异步组件会发起请求,并且抛出异常,终止渲染;
  2. Suspense里面有 compenentDidCatch生命周期函数,异步组件抛出异常会触发这个函数,然后改变状态使其渲染fallback参数传入的组件;
  3. 异步组件的请求成功返回之后,Suspense组件再次改变状态使其渲染正常子组件(即异步组件);
// comp About
const About = lazy(() => new Promise(resolve => {
  setTimeout(() => {
    resolve({
      default: <div>component content</div>
    })
  }, 1000)
}))

// comp Suspense
import React from 'react'
class Suspense extends React.PureComponent {
  /**
   * isRender 异步组件是否就绪,可以渲染
   */
  state = {
    isRender: true
  }
  componentDidCatch(e) {
    this.setState({ isRender: false })
    e.promise.then(() => {
      /* 数据请求后,渲染真实组件 */
      this.setState({ isRender: true })
    })
  }
  render() {
    const { fallback, children } = this.props
    const { isRender } = this.state
    return isRender ? children : fallback
  }
}

export default Suspense

// comp lazy
import React, { useEffect } from 'react'
export function lazy(fn) {
  const fetcher = {
    status: 'pending',
    result: null,
    promise: null,
  }
  return function MyComponent() {
    const getDataPromise = fn()
    fetcher.promise = getDataPromise
    getDataPromise.then(res => {
      fetcher.status = 'resolved'
      fetcher.result = res.default
    })
    useEffect(() => {
      if (fetcher.status === 'pending') {
          throw fetcher
      }
    }, [])
    if (fetcher.status === 'resolved') {
      return fetcher.result
    }
    return null
  }
}

// 实现的效果与React支持内容保持一致
import React, {Suspese, lazy} from 'react'

const About= lazy(() => { import('../About') });

class App extends React.Component {
  render() {
    /**
     * 1. 使用 React.Lazy 和 import() 来引入组件
     * 2. 使用<React.Suspense></React.Suspense>来做异步组件的父组件,并使用 fallback 来实现组件未加载完成时展示信息
     * 3. fallback 可以传入html,也可以自行封装一个统一的提示组件
     */
    return (
      <div>
        <Suspense
          fallback={
            <Loading />
          }
        >
          <About />
        </Suspense>
      </div>
    )
  }
}
export default ReactComp;