issue-blog icon indicating copy to clipboard operation
issue-blog copied to clipboard

React Hooks 尝鲜

Open SunShinewyf opened this issue 5 years ago • 0 comments

React Hooks 尝鲜

前言

React 在 16.70-alpha 中首次提出 Hooks 这个新特性,并且在 16.8.0 正式发布 Hooks 稳定版本。React Hooks 指的是在 Function Component 中插入一些 Hooks,通过使用这些 Hooks 可以让 Function Component 拥有 state 和生命周期等 React 特性。

为什么会有 React Hooks

React Hooks 要解决的问题是状态逻辑复用,是继 render-props 和 higher-order components 之后的第三种状态共享方案。它主要解决了如下一些痛点:

组件中的状态逻辑难以复用

在一个 React Componet 中,会在 state 中存储状态,并且在组件的各个 lifecycle 函数中执行特有的逻辑,比如在 componentDidMount 去请求数据、在 componentWillUnMount 中卸载实例、取消事件监听器等。这些 state 和 生命周期和组件强耦合,使得 state 和生命函数的逻辑无法抽离得到复用。而 Hooks 可以从组件中提取状态逻辑,从而达到复用。

复杂组件导致的 wrapper hell 和逻辑难以维护

React 的组件带来的好处是模块化,但是当逻辑比较复杂时,就会出现组件嵌套地狱,看看 Devtool 里面的嵌套,是不是有点吓人。
                                      images

除此之外,复杂组件会在不同的生命周期中执行很复杂的逻辑,比如在 componentDidMount 请求数据,或者在 componentWillReceiveProps 中根据 nextProps 改变组件 state 等等,后期维护和理解的成本会非常高。
再加上 class Component 的 this 指向问题,为了保证指向正确,需要用 bind 绑定或者使用箭头函数,如果没有绑定,就会出现各种 bug。

怎么用 React Hooks

首先使用 class Component 来实现一个最简单的组件:

import React, { useState } from 'react';
import { Button } from 'antd';

function Demo() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <Button type="primary" onClick={() => {
        setCount(count => count + 1)
      }}>Add</Button>
      <div className="number">{count}</div>
    </div>
  )
}

代码链接

在上面这个例子中,useState 就是一个 Hook,它接受一个参数作为 state 的初始值,返回一个数组,结构为 [value,setValue],其中 value 对应的 state,相当于 Class Component 中的 this.state,setValue 是修改 state 的函数,相当于  class Component 中 this.setState。 其他的就按照 Function Component 写就好了,是不是看起来很清爽?

API 介绍

现在介绍一下 React 内置的几种 Hooks 以及它们的用法。

useState

上面的例子就是用的 useState,当然是最简单的用法,总结一下 useState 的特性,如下:

  • 首次渲染时,将传入的参数作为 initialState 使用,当再次 render 时,使用最新的值渲染。如果 initialState 的逻辑比较复杂,可以传入一个函数,并返回计算后的初始值,该函数只在初始渲染时才会被调用。
const [complexState, setComplexState] = useState(()=> {
  const initialValue = complexFunction(props);
  return initialValue;
})
  • 返回一个 state 和更新 state 的函数,且该函数接受一个新的 state 值
  • 多个 state 使用多个 useState,并且 useState 之间相互独立。
function Demo() {
  const [count, setCount] = useState(0);
  const [type, setType] = useState('default')
  const [list, setList] = useState([1])
  ....
}

useEffect

useEffect 是用来执行副作用操作的,通过这个 Hook,可以执行一些组件渲染之后的逻辑,比如事件监听、设置标题等。它相当于是 Class Component 中的 componentDidMount、componentDidUpdate、componentWillUnMount 这三个生命周期的集合。举个最简单的例子,如下:

import React, { useEffect, useState } from 'react';
import axios from 'axios'

function UseEffectDemo() {
  const [list, setList] = useState([]);

  useEffect(() => {

    async function fetchData() {
      const result = await axios('https://hn.algolia.com/api/v1/search?query=react');
      setList(result.data.hits);
    }
    fetchData()

  }, [])

  return <div className="movie-container">
    {list.map(item => {
      return <div className="item">{item.title}</div>
    })}
  </div>
}

代码地址

这个 Hook 的特性如下:

  • 组件首次渲染和之后更新的每次渲染都会调用 useEffect。
  • 允许传入第二个参数来决定是否执行 effect 里的逻辑,这可以减少一些不必要的性能损耗,如果传入一个 [],则只会在 componentDidMount 和 componentWillUnMount 时期才执行,传入 state,则表示只有当 state 发生改变的时候才触发。如下:

      

useEffect(() => {
  // do something
}, [type]); // 只有 type 发生改变才执行 useEffect 里面的逻辑
  • 在 useEffect 中返回一个函数可以执行一些清理操作,比如取消订阅等,这些逻辑会在前一次 effect 执行之后下一次 effect 执行之前以及 componentWillUnMount 的时候执行。如下:
useEffect(() => {
  //do something
  return function cleanup(){
    //do something clean up
  }
})

useContext

useContext 主要是为了使用 context,而且不用像以前一样用 Provider、Consumer 包裹组件,可以大大提高代码的简洁性。使用 createContext 实现一个简单的例子,如下:

const themeContext = React.createContext('light')

// App 组件
class App extends React.Component {
  state = {
    theme: 'red'
  }

  changeThme = (type) => {
    this.setState({ theme: type })
  }

  render() {
    return (<themeContext.Provider value={this.state.theme}>
      <Button onClick={this.changeThme.bind(this, 'black')}>黑色</Button>
      <Button onClick={this.changeThme.bind(this, 'red')}>红色</Button>
      <Consumer />
    </themeContext.Provider>);
  }
}

// Consumer 组件
class Consumer extends React.Component {
  render() {
    return <themeContext.Consumer>
      {theme => {
        return <div>{theme}</div>
      }}
    </themeContext.Consumer>
  }
}

代码地址

使用 context,就可以避免 props 的多层传递。对上面的例子使用 useContext 进行改造,代码如下:

export const ThemeContext = createContext('light')

// App组件
function UseContextDemo() {
  const [theme, setTheme] = useState('red');

  return (<ThemeContext.Provider value={theme}>
    <Button onClick={() => setTheme('black')}>黑色</Button>
    <Button onClick={() => setTheme('red')}>红色</Button>
    <Consumer />
  </ThemeContext.Provider>);
}

// Consumer 组件
function Consumer() {
  // 直接通过 useContext 获取值即可,不需要使用 Context.Consumer 包裹
  const theme  = useContext(ThemeContext)
  return <div>{theme}</div>
}

代码地址

使用 useContext 可以直接获取值,不需要用 ThemeContext.Consumer 包裹组件。代码看起来更加简洁。

useReducer

useReducer 主要是 useState 的语法糖,主要是针对复杂 state 或者下一个 state 依赖之前 state 的场景,主要有如下特点:

  • 和 useState 返回很像,返回 state 和 dispatch 函数
  • 接受三个参数,第一个参数是 reducer,类 redux 的 reducer,第二个参数是 initialState,如果你想重置,state,可以传入第三个参数-- init 函数,此时第二个参数作为 init 函数的参数。reducer 的形式如下:
function reducer(state, action) {
  switch (action.type) {
    case "increment":
      return {
        ...state,
        count: count + 1
      };
    default:
      return state;
  }
}

举个🌰如下:

const initialState = { count: 0 }

const init = (initialState) => {
  return initialState;
}

const reducer = (state, action) => {
  switch (action.type) {
    case 'ADD':
      return { ...state, count: state.count + 1 };
      break;
    case 'DEC':
      return { ...state, count: state.count - 1 };
      break;
    case 'RESET':
      return init(action.payload)
    default:
      return state;
  }
}

function UseReducerDemo() {
  const [state, dispatch] = useReducer(reducer, initialState, init)

  return (
    <div>
      <div>Count: {state.count}</div>
      <button onClick={() => dispatch({ type: 'ADD' })}>Add</button>
      <button onClick={() => dispatch({ type: 'DEC' })}>DEC</button>
			// 传入 initialState,进行复位
      <button onClick={() => dispatch({ type: 'RESET', payload: initialState })}>RESET</button>
    </div>
  );
}

代码地址

如上例子所示,定义一个 reduce 函数,接收一个 state 和 action 参数,返回一个新的 state。通过传递 init 初始化函数,可以对 state 进行复位。

useCallback 和 useMemo

在 Class Component 中,我们可以使用 shouldComponentUpdate 来控制组件重新渲染的条件,从而避免复杂逻辑带来性能性能上的损耗,而在 Function Component 中没有 shouldComponentUpdate 这个生命周期,怎么办?useCallback 和 useMemo 就是用来解决这个问题的。
useCallback 和 useMemo 会在组件首次渲染的时候执行,然后会根据依赖项是否发生改变而再次执行,并且这两个 Hook 都返回缓存的值,useCallback 返回缓存的函数,useMemo 返回缓存的变量。
首先举一个🌰,如下:

function UseMemoDemo() {

  const [count, setCount] = useState(1);
  const [value,setValue] = useState('')

  //第一种,没有使用 useMemo
  const computing = () => {
    let sum = 0;
    for(let i=0;i<count*100;i++){
        sum += i;
    }
    console.log(sum, 'computing')
    return sum;
  }

   // 第二种使用 useMemo
   const computing =useMemo( () => {
    let sum = 0;
    for(let i=0;i<count*100;i++){
        sum += i;
    }
    console.log(sum, 'computing')
    return sum;
  },[count])

  return <div>
    <div>Count: {count}</div>
    <div> SUM: {expensive}</div>
    <button onClick={() => setCount(count + 1)}>Add</button>
    <input className="input" onChange={(e)=>setValue(e.target.value)}/>
  </div>
}

代码地址

如上所示:expensive 是计算量很大的函数,并且当 state 发生改变时,组件就会重新渲染,从而导致 computing 函数重新执行,而 computing 只和 count 相关,但是 value 发生改变,computing 还是会重新计算。这是没必要的,所以我们可以使用 useMemo 来控制没必要的执行,第二个参数表示依赖项,只有依赖项发生改变时才会执行。

useCallback 和 useMemo 不同的是,它返回一个缓存的函数,并且 useCallback(fn,deps) = useMemo(()=>fn(),deps),它有什么作用呢,举一个例子:

function UseCallbackDemo() {
  const [count, setCount] = useState(0);
  const [value, setValue] = useState('')

  // 第一种,没有使用 useCallback
  // const callback = () => {
  //   return count + 1;
  // }
  
  // 第一种,没有使用 useCallback
  const callback = useCallback(() => {
    return count + 1
  }, [count]);
  
  return (<div>
    <div>Parent Count: {count} </div>
    <button onClick={() => { setCount(count + 1) }}>Add</button>
    <input className="input-container" onChange={(e) => { setValue(e.target.value) }} />
    <Child callback={callback} />
  </div>)
}

// Child 组件
function Child(props) {
  const [value, setValue] = useState(0)

  useEffect(() => {
    setValue(props.callback())
  },[props.callback])
  return <div>Child Count: {value}</div>
}

代码地址

如上所示,父组件将 callback 函数传递给子组件,然后子组件在 useEffect 中判断 callback 是否发生改变,从而更新自身的 state,当父组件的 value state 发生改变是,并不会触发 useEffect 的更新操作,所以使用 useCallback 可以避免子组件不必要的重复渲染。

useRef

useRef 相当于是一个存储属性的地方,它在组件的整个生命周期内都保持不变,它的特性如下:

  • 接受一个参数,并且作为属性 current 的初始值。
  • 比 ref 更有用,可以存储任何可变值。
  • 当 ref 的 current 属性变化时,不会触发组件的重新渲染

它的用法如下:

function UseRefDemo() {
  const inputRef = useRef(null)

  const onClick = () => {
    inputRef.current.focus()
  }
  return <div>
    <input ref={inputRef}></input>
    <button onClick={onClick}>Focus</button>
  </div>
}

代码地址

如上,在点击 button 时,设置 inputRef 获取焦点,其中 input 这个实例就保存在 inputRef 中。

useImperativeHandle

useImperativeHandle 用于自定义暴露给父组件的 ref 属性,该 hooks 需要和 forwardRef 一起使用,例子如下:

function UseImperativeHandleDemo() {
  const parentRef = createRef()
  return (<div>
    <Child ref={parentRef} />
    <br />
    <button onClick={() => { parentRef.current.focus() }} >获取焦点</button>
  </div>)
}

//Child 组件
import React, { useRef, useImperativeHandle, forwardRef } from 'react';

const Child = (props, ref) => {
  const inputRef = useRef(null);

  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }))
  return <input ref={inputRef} />
}
export default forwardRef(Child)

代码地址

如上面所示,父组件可以直接调用在 Child 里面定义的 Ref.current.focus 方法。

useLayoutEffect

useLayoutEffect 和 useEffect 类似,两者不同的地方是:

  • useEffect 是被 react-scheduler 调度,异步执行,不会阻塞浏览器的渲染
  • useLayoutEffect 在 DOM 变更之后同步执行,比较适用于 DOM 更新后,立刻去执行变更 DOM 副作用的场景,和以往的 componentDidMount 和 componentDidUpdate 的表现一致。

useDebugValue

用户在 react devtools 中显示 hooks 属性,第二个参数可以进行格式化能力,例子如下:

const [date] = useState(new Date());
useDebugValue(date, date => date.toDateString());

自定义 Hooks

除了上面提到的官方已有的 hooks,我们还可以自定义 hooks,通过自定义,可以将组件逻辑提取到可重用的函数中。并且自定义的 hooks 之间也是相互独立的,举个例子:

function CustomHookDemo() {
  const [hoverRef, isHover] = useHover();
  return <div ref={hoverRef}>{isHover ? 'I am hovered' : 'I am not hovered'}</div>
}

//useHover 的 hook
import React, { useState, useRef, useEffect } from 'react';

const useHover = () => {
  const [isHover, setIsHover] = useState(false)
  const ref = useRef(null);

  const handleMouseOver = () => {
    setIsHover(true)
  }
  const handleMouseOut = () => {
    setIsHover(false)
  }
  useEffect(() => {
    const node = ref.current;
    if (node) {
      node.addEventListener('mouseover', handleMouseOver);
      node.addEventListener('mouseout', handleMouseOut);

      return () => {
        node.removeEventListener('mouseover', handleMouseOver);
        node.removeEventListener('mouseout', handleMouseOut);
      };
    }
  }, [ref.current]);

  return [ref, isHover]
}

export default useHover

代码地址

如上所示,我们把 hover 的逻辑抽离到 useHover 这个 hook 中,并且把 hover 的 DOM 实例和 isHover 的值返回,这样其他组件想要这个逻辑就可以直接复用。
自定义 Hooks,需要遵循如下规则:

  • 自定义 hook 是一个函数,必须以 use 开头
  • 自定义 hook 可以调用官方提供的 Hooks

使用 Hooks 需要注意的点

虽然 Hooks 比较强大,但是在使用过程中,还是有一些点需要注意,比如:

  • 只在顶层使用 Hook,不在循环、条件或者嵌套函数中调用 hook
  • 只在 React 函数和 自定义 Hooks 中使用,不要在普通 js 中使用 Hooks

SunShinewyf avatar Jul 15 '19 04:07 SunShinewyf