React Hook

摩森特沃 2021年07月22日 428次浏览

Hook 的作用

Hook 是为更好地复用 React 状态逻辑代码而生的。注意这里说的不是模板代码,模板代码可以用组件来复用;而单纯的状态逻辑代码没法用组件复用

注意,其实普通的函数也可以复用逻辑代码,但是没法复用带状态的逻辑代码

什么是React的状态

举个例子:


const Comp = () => {
  const [id, setId] = useState(0)
  const [assets, setAssets] = useState()

    useEffect(() => {
      fetch(`https://google.com?id=${id}`).then(async response => {
       const data = await response.json();
        if (response.ok) {
          setAssets(data)
        } else {
          return Promise.reject(data);
        }
        })
      }, [])

  return <div>{assets.map(a => a.name)}</div>
}

这里面的 id,assets就是状态,它的特征是它是由特定的API(useState)定义的,而且它改变的时候组件会做出相应的反应(比如重新render)

const sum = (a, b) => a + b

这个普通的函数就没有状态,sum的返回值无论怎么变,都不会让任何组件重新render

React团队是非常注重React 状态代码复用性的,从React被创造出来,他们就一直在优化代码复用的解决方案,大概经历了:Mixin → HOC → Render Props,一直到现在的 Hook

Mixin

Mixin 是最早的 React 代码复用方案

var SubscriptionMixin = {
  getInitialState: function() {
    return {
      comments: DataSource.getComments()
    };
  },

  componentDidMount: function() {
    DataSource.addChangeListener(this.handleChange);
  },

  componentWillUnmount: function() {
    DataSource.removeChangeListener(this.handleChange);
  },

  handleChange: function() {
    this.setState({
      comments: DataSource.getComments()
    });
  }
};

var CommentList = React.createClass({
  mixins: [SubscriptionMixin],

  render: function() {
    // Reading comments from state managed by mixin.
    var comments = this.state.comments;
    return (
      <div>
        {comments.map(function(comment) {
          return <Comment comment={comment} key={comment.id} />
        })}
      </div>
    )
  }
});

它的好处是简单粗暴,符合直觉,也确实起到了重用代码的作用;但是坏处也很明显,隐式依赖,名字冲突,不支持 class component,难以维护,总之,现在已经被完全淘汰了

HOC (higher-order component)

2015年,React团队判处Mixin死刑以后,推荐大家使用HOC模式,HOC是采用了设计模式里的装饰器模式

function withWindowWidth(BaseComponent) {
  class DerivedClass extends React.Component {
    state = {
      windowWidth: window.innerWidth,
    }

    onResize = () => {
      this.setState({
        windowWidth: window.innerWidth,
      })
    }

    componentDidMount() {
      window.addEventListener('resize', this.onResize)
    }

    componentWillUnmount() {
      window.removeEventListener('resize', this.onResize);
    }

    render() {
      return <BaseComponent {...this.props} {...this.state}/>
    }
  }
  return DerivedClass;
}

const MyComponent = (props) => {
  return <div>Window width is: {props.windowWidth}</div>
};

经典的 容器组件与展示组件分离 (separation of container presidential) 就是从这里开始的

// components/AddTodo.js

import React from 'react'
import { connect } from 'react-redux'
import { addTodo } from '../redux/actions'

class AddTodo extends React.Component {
  // ...

  handleAddTodo = () => {
    // dispatches actions to add todo
    this.props.addTodo(this.state.input)

    // sets state back to empty string
    this.setState({ input: '' })
  }

  render() {
    return (
      <div>
        <input
          onChange={(e) => this.updateInput(e.target.value)}
          value={this.state.input}
        />
        <button className="add-todo" onClick={this.handleAddTodo}>
          Add Todo
        </button>
      </div>
    )
  }
}

export default connect(null, { addTodo })(AddTodo)

一个很经典的HOC使用案例是react redux 中的 connect 方法,AddTodo组件像一只无辜的小白兔,它的addTodo方法是connect方法给它注入进去的,它有以下优点:

  1. 可以在任何组件包括 Class Component 中工作
  2. 它所倡导的 容器组件与展示组件分离 原则做到了:关注点分离

但也有以下明细的缺点

  1. 不直观,难以阅读
  2. 名字冲突
  3. 组件层层层层层层嵌套

Render Props

2017年,render props流行起来

class WindowWidth extends React.Component {
  propTypes = {
    children: PropTypes.func.isRequired
  }

  state = {
    windowWidth: window.innerWidth,
  }

  onResize = () => {
    this.setState({
      windowWidth: window.innerWidth,
    })
  }

  componentDidMount() {
    window.addEventListener('resize', this.onResize)
  }

  componentWillUnmount() {
    window.removeEventListener('resize', this.onResize);
  }

  render() {
    return this.props.children(this.state.windowWidth);
  }
}

const MyComponent = () => {
  return (
    <WindowWidth>
      {width => <div>Window width is: {width}</div>}
    </WindowWidth>
  )
}

2017年,render props流行起来,它的缺点是,难以阅读,难以理解,下面是一个使用案例

Hook

在上面的两种方法中,它们最终的目的是为了向组件注入 windowWidth 这个状态,为了这一个目的它们用了复杂又不直观的方法,有没有办法直观呢?那就是 Hook 了,

还是上面相同的需求,以下为Hook的实现方式

import { useState, useEffect } from "react";

const useWindowsWidth = () => {
  const [isScreenSmall, setIsScreenSmall] = useState(false);

  let checkScreenSize = () => {
    setIsScreenSmall(window.innerWidth < 600);
  };
  useEffect(() => {
    checkScreenSize();
    window.addEventListener("resize", checkScreenSize);

    return () => window.removeEventListener("resize", checkScreenSize);
  }, []);

  return isScreenSmall;
};

export default useWindowsWidth;

import React from 'react'
import useWindowWidth from './useWindowWidth.js'

const MyComponent = () => {
  const onSmallScreen = useWindowWidth();

  return (
    // Return some elements
  )
}

Hook相比其他方案的优点:

  1. 提取逻辑出来非常容易
  2. 非常易于组合
  3. 可读性非常强
  4. 没有名字冲突问题

React 自带 Hook 详解

useState

useState 是最基础的一个Hook,为什么这么说呢,因为它是状态生产器。它产生的状态和普通变量有什么区别呢?

const [count, setCount] = useState(initialCount);
---------
const count = 1
const setCount = (value) => count = value

这两个有什么区别呢?区别就在于第一个useState产生的是状态,状态改变的时候组件会重新渲染,它是响应式的;而第二个,就是一个普通变量,它改变什么都不会发生,听起来是不是有点可怜呢

useEffect

有了useState产生的状态,就可以写一些简单的组件了,以下是一个简单的计数组件

const Count = () => {
  const [count, setCount] = useState(0)
  const add = setCount(count + 1)
  return <button onClick={add}>add</button>
}

在真实的代码中,通常需要和这个组件外面的世界产生联系,组件的状态,也要和外面的世界同步,才能产生工业的价值。通常发生在外面的事情被称为副作用

比如说当想将count和服务器的代码同步,将count和手机的震动同步,这时候就需要用到useEffect了。要摒弃以前的生命周期的概念,useEffect的唯一作用就是同步副作用。

useContext

React 的组件化可以将不同的业务代码分割开,但是也带来了一个问题,那就是组件间共享状态是非常不方便的。比如,有个很多组件都会用到的状态,app 主题状态,如何让一个组件随时可以获取到这个状态呢?首先想到的可能是状态提升,但是它只能缓解却并不能解决这个问题。而context就是为了解决这个问题,context 可以被看作是React自带的Redux,实际上Redux就是用context实现的

useReducer

上文中提到useState是主要的状态生产器,那么useReducer就是另一个没那么常用的状态生产器。它适合状态逻辑很复杂的时候,或者下一个state值依赖于上一个state值,比如

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

这种情况用useState当然也可以,但是用useReducer就显得代码干净漂亮

useCallback/useMemo

现在有这样一个问题:父组件刷新,所有的子组件都会跟着刷新,这句话对吗?

这句话是对的,父组件刷新,所有的子组件都会刷新,这样听起来很耗性能,但是对于绝大多数组件来说,性能都是没有问题的,因为React真的很快

但是对于耗性能的组件来说,这样就有很大的问题了,耗性能的组件不希望被经常刷新,所以可以用 React.memo包裹住它们,这样只有在它们的props变化的时候它们才会刷新

这样又有一个问题,比如:

const TestComp = () => {
  const value = {name: 'Jack'}
  return <MemoExpensiveList value={value}/>
}

已知MemoExpensiveList是被React.memo给处理过的,它的props变化它才会刷新。但是在上面这个案例里,TestComp一刷新MemoExpensiveList就会刷新,这是为什么呢?原因就是,onClick在每次TestComp刷新时都会生成一个新的实例,{name: 'Jack'} ≠= {name: 'Jack'}

这就是 useMemo派上用场的时候了,我们可以用useMemo包裹住:

const value = useMemo(() => {}, [])

这样它只会生成一个实例,也就不会骚扰到MemoExpensiveList了

而useCallback就是一个特殊版本的useMemo,专门来处理函数的

useRef

上面详细讲解了状态的概念,有时候会面临这样的需求,希望创建一种类型的值,它不是状态,但是又可以在不同的render之间以同一个实例的形式存在。它有点类似于在class component里的 this.xxx

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` points to the mounted text input element
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

自定义 Hook

自定义Hook是目前最好的重用React逻辑的方法,它和普通的函数很像很像,自定义Hook的特殊之处在于,它是有状态的,它返回的也是状态。通常在需要抽象出处理状态的逻辑的时候可以使用自定义Hook,例如

const Comp = () => {
  const [arr, setArr] = useState([1, 2])
  return <button onClick={() => setArr([...arr, value])}>add</button>
}

如果项目中有好几处这种数组处理,则可以使用以下方式处理

export const useArray = <T>(initialArray: T[]) => {
  const [value, setValue] = useState(initialArray);
  return {
    value,
    setValue,
    add: (item: T) => setValue([...value, item]),
    clear: () => setValue([]),
    removeIndex: (index: number) => {
      const copy = [...value];
      copy.splice(index, 1);
      setValue(copy);
    },
  };
};

用这样一个自定义的Hook,不仅返回了状态,也返回了处理这个状态的方法

这个例子也展示了,自定义Hook可以以状态为核心,并将它和与它相关的东西封装在一起。这也符合关注点分离的原则

另一个示例:

const Comp = () => {
  const [id, setId] = useState(0)
  const [assets, setAssets] = useState()

    useEffect(() => {
      fetch(`https://google.com?id=${id}`).then(async response => {
       const data = await response.json();
        if (response.ok) {
          setAssets(data)
        } else {
          return Promise.reject(data);
        }
        })
      }, [])

  return <div>{assets.map(a => a.name)}</div>
}

这里的fetch的内容和这个组件关系大吗?不大,因为这个组件其实不怎么在乎fetch的细节,它只在乎拿到result.data,那么按照以下方式使用hook来封装

// util.ts
const useAssets = (id) => {
    const [assets, setAssets] = useState()

    useEffect(() => {
      fetch(`https://google.com?id=${id}`).then(async response => {
       const data = await response.json();
        if (response.ok) {
          setAssets(data)
        } else {
          return Promise.reject(data);
        }
        })
      }, [])
    return assets
}

// comp.tsx
const Comp = () => {
  const [id, setId] = useState(0)
  const assets = useAssets(id)

  return <div>{assets.map(a => a.name)}</div>
}