Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React Hooks 尝鲜 #49

Open
SunShinewyf opened this issue Jul 15, 2019 · 0 comments
Open

React Hooks 尝鲜 #49

SunShinewyf opened this issue Jul 15, 2019 · 0 comments
Labels

Comments

@SunShinewyf
Copy link
Owner

SunShinewyf commented Jul 15, 2019

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant