# React-hooks

相关文章:

钩子列表:

  • useState
  • useEffect
  • useRef
  • useReducer
  • useContext
  • useMemo
  • useCallback 每当依赖的数组改变时,就会创建一个 memoized 函数
  • useImperativeHandle
  • useDebugValue
  • useLayoutEffect API 同 useEffect 一致,可用于需要同步执行的操作,例如测量布局

以前,React API 只有一套,现在有两套:类(class)API 和基于函数的钩子(hooks) API。

任何一个组件,都可以用类来写,也都可以用钩子来写。只用其中某一套均可实现同样的结果。

但官方推荐使用钩子(函数),而不是类。因为钩子更简洁,代码量少,用起来比较"轻",而类比较"重"。而且,钩子是函数,更符合 React 函数式的本质。

钩子到底是什么?

一句话,钩子(hook)就是 React 函数组件的副效应解决方案,用来为函数组件引入副效应。 函数组件的主体只应该用来返回组件的 HTML 代码,所有的其他操作(副效应)都必须通过钩子引入。

由于副效应非常多,所以钩子有许多种。React 为许多常见的操作(副效应),都提供了专用的钩子。

  • useState():保存状态
  • useContext():保存上下文
  • useRef():保存引用
  • ......

上面这些钩子,都是引入某种特定的副效应。

Hooks 使用注意事项:

  • 只能在函数内部的最外层调用 Hook,不要在循环、条件判断或者子函数中调用
  • 只能在 React 的函数组件中调用 Hook,不要在其他 JavaScript 函数中调用

# useState() 数据管理

useState() 接收一个参数作为初始值,返回一个数组,数组第一个元素是当前值,数组第二个元素是一个函数,这个函数是用来修改值的函数,调用这个函数并传入一个值,则会修改数组第一个元素值。

语法:

const [<state>, <setState>] = useState(<value>)
1

示例:

import { useState } from 'react'
const Index = () => {
  // 定义一个组件内状态,初始值为空字符串
  const [value, setValue] = useState('')

  return (
    <div>
      <input type="text" value={value} onChange={e => setValue(e.target.value)} />
    </div>
  )
}

export default Index
1
2
3
4
5
6
7
8
9
10
11
12
13

<setState> 除了可以接收新的值以外,也可以接收一个回调函数:

  • 回调函数得到的第一个参数就是原来的值
  • 回调函数的返回值将作为新的值使用
const Index = () => {
  const [value, setValue] = useState(0)

  return (
    <div>
      <div>{ value }</div>
      <button onClick={ () => setValue(v => v + 1)}>Click</button>
    </div>
  )
}
1
2
3
4
5
6
7
8
9
10

useState() 除了接收一个值作为初始值,也可以接收一个回调函数,这个回调函数只会在组件初始化时被执行一次,后续更新不会再执行。

const Index = props => {
  const initFn = () => {
    // 在函数内可以执行一些计算,然后再返回初始值
    return 0
  }
  const [value, setValue] = useState(initFn)

  return (
    <div>
      <div>{ value }</div>
      <button onClick={ () => setValue(v => v + 1)}>Click</button>
    </div>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

注意事项:

  • state 更新,整个组件会重新渲染,包括所有子孙组件,不管子孙组件依不依赖该 state 。
  • 修改状态的时候,如果传的新值和旧值一样,则不会重新渲染
  • 和 class 组件的 this.setState() 不一样,hook 是直接用新值替换旧值

# useMemouseCallback

useMemouseCallback 都是用来解决 state 改变引发整个组件(包括子孙组件)重新渲染的,包括那些不依赖该状态的组件,通过 useMemouseCallback 添加依赖的 state。就能只在依赖的 state 发生改变时才执行重新计算。

  • useMemo() 用于缓存值,返回的结果就是缓存的值,依赖值变化才会返回新的值
  • useCallback() 用于缓存函数,返回的结果就是缓存的函数,依赖值变化才会返回新的函数

语法:

const value = useMemo(() => {
  return <结果值>
}, [<依赖值>])

const fn = useCallback(() => {
  // 执行函数操作
}, [<依赖值>])
1
2
3
4
5
6
7

useCallback(fn, deps) 相当于 useMemo(() => fn, deps) ,主要区别是 useMemo 将调用 fn 函数并返回其结果,而 React.useCallback 将返回 fn 函数本身。

# useEffect() 做出一些副效应(副作用)的操作(可监听一个状态值的变化而执行)

函数式编程将那些跟数据计算无关的操作,都称为 "副效应" (side effect) 。如果函数内部直接包含产生副效应的操作,就不再是纯函数了,我们称之为不纯的函数。

React 的函数组件应该是一个纯函数,只应该做一件事情:返回组件的 HTML 代码,而没有其他的功能,纯函数内部只有通过间接的手段(即通过其他函数调用),才能包含副效应。所以要在函数组件内使用 useEffect() 来执行含有副效应的操作。

useEffect() 是通用的副效应钩子 。找不到对应的钩子时,就可以用它。其实,从名字也可以看出来,它跟副效应(side effect)直接相关。

useEffect(fn, []) 函数接收两个参数,第一个参数是一个函数,第二个参数是监听的组件状态组成的数组,当第二个数组参数中的状态值有变化时,第一个函数参数就会被执行。

  • 第一个函数参数会在组件第一次挂载的时候被触发,也可以在它的依赖更新时被触发。
  • 如果 useEffect() 第二个参数是一个空数组,第一个参数函数则只会执行初始化的那一次。
import { useState, useEffect } from 'react'
const Index = (props) => {
  const [value, setValue] = useState(localStorage.getItem('value' || ''))

  // 当 value 变化时,函数就会被执行
  useEffect(() => {
    localStorage.setItem('value', value)
  }, [value])

  return (
    <div>
      <input type="text" value={value} onChange={e => setValue(e.target.value)} />
    </div>
  )
}

export default Index
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

只要是副效应,都可以使用 useEffect() 引入。它的常见用途有下面几种。

  • 获取数据(data fetching)
  • 操作缓存 Storage(data fetching)
  • 事件监听或订阅(setting up a subscription)
  • 改变 DOM(changing the DOM)
  • 输出日志(logging)

最常见的就是向服务器请求数据。以前,放在 componentDidMount() 里面的代码,现在可以放在 useEffect()

副效应是随着组件加载而发生的,那么组件卸载时,可能需要清理这些副效应。

useEffect() 允许返回一个函数,在组件卸载时,会自动执行该函数,清理副效应。如果不需要清理副效应, useEffect() 就不用返回任何值。

useEffect(() => {
  const subscription = props.source.subscribe()
  return () => {
    subscription.unsubscribe()
  }
}, [props.source])
1
2
3
4
5
6

上面例子中,useEffect() 在组件加载时订阅了一个事件,并且返回一个清理函数,在组件卸载时取消订阅。

实际使用中,由于副效应函数默认是每次渲染都会执行,所以清理函数不仅会在组件卸载时执行一次,每次副效应函数重新执行之前,也会执行一次,用来清理上一次渲染的副效应。

使用 useEffect() 时,有一点需要注意。如果有多个副效应,应该调用多个 useEffect() ,而不应该合并写在一起。

function App() {
  const [varA, setVarA] = useState(0)
  const [varB, setVarB] = useState(0)
  useEffect(() => {
    const timeoutA = setTimeout(() => setVarA(varA + 1), 1000)
    const timeoutB = setTimeout(() => setVarB(varB + 2), 2000)

    return () => {
      clearTimeout(timeoutA)
      clearTimeout(timeoutB)
    }
  }, [varA, varB])

  return <span>{varA}, {varB}</span>
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

上面的例子是错误的写法,副效应函数里面有两个定时器,它们之间并没有关系,其实是两个不相关的副效应,不应该写在一起。正确的写法是将它们分开写成两个 useEffect()

function App() {
  const [varA, setVarA] = useState(0)
  const [varB, setVarB] = useState(0)

  useEffect(() => {
    const timeout = setTimeout(() => setVarA(varA + 1), 1000)
    return () => clearTimeout(timeout)
  }, [varA])

  useEffect(() => {
    const timeout = setTimeout(() => setVarB(varB + 2), 2000)

    return () => clearTimeout(timeout)
  }, [varB])

  return <span>{varA}, {varB}</span>
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 自定义 hooks

基于 useStateuseEffect 来写一个自定义的 hook

新建 useStorage.js 并写入:

// useStorage.js

import { useState, useEffect } from 'react'

const useStorage = (key, initialState) => {
  const [value, setValue] = useState(localStorage.getItem(key) || initialState)

  useEffect(() => {
    localStorage.setItem(key, value)
  }, [value, key])

  return [value, setValue]
}

export default useStorage
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// index.js

import useStorage from './useStorage'
const Index = (props) => {
  const [inputValue, setInputValue] = useStorage('value')

  return (
    <div>
      <input type="text" value={inputValue} onChange={e => setInputValue(e.target.value)} />
    </div>
  )
}

export default Index
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# useRef 创建一个 ref 引用,用于访问 DOM 节点

useRef() 创建并返回一个 ref 引用对象,将这个对象赋值给一个元素的 ref 属性即可引用这个 DOM 节点。

import { useState, useRef } from 'react'
const Index = (props) => {
  const [value, setValue] = useState('')

  // 创建一个 ref 引用对象
  const inputRef = useRef()

  // 引用对象上的 current 属性即是对应 DOM 节点元素
  const inputElement = inputRef.current

  const handleClick = () => {
    // 访问 DOM 节点元素 input 的 focus() 方法
    inputElement.focus()
  }

  return (
    <div>
      <input ref={inputRef} type="text" value={value} onChange={e => setValue(e.target.value)} />
      <button onClick={handleClick}>点击让输入框获得焦点</button>
    </div>
  )
}

export default Index
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# useReducer 使用 reducer 来管理状态

useReducer() 接收一个 reducer 函数和状态的初始值 state 作为参数,并且返回一个包含两项内容的数组。数组的第一个成员是状态的当前值,第二个成员是发送 action 的 dispatch 函数。

语法:

const [state, dispatch] = useReducer(reducer, initialState)
1
import { useReducer } from 'react'
import style from './index.module.scss'
import Add from './Add'
import List from './List'

// 先定义一个 reducer 函数,第一个参数为状态 state, 第二个参数是 action 动作
const todoListReducer = (state, action) => {
  switch (action.type) {
    case 'SET_LIST':
      return action.payload
    case 'REMOVE_ITEM':
      return state.filter(item => item.id !== action.payload.id)
    default:
      return state
  }
}

const Index = () => {
  // 使用 reducer, useReducer() 第一个参数传入 reducer 函数,第二个参数为 state 初始默认值
  const [list, dispatchList] = useReducer(todoListReducer, [])

  const handleAdd = text => {
    const newItem = {
      text,
      done: false,
    }
    const newList = [...list, newItem]
    dispatchList({
      type: 'SET_LIST',
      payload: newList,
    })
  }

  return (
    <div className={style['view']}>
      <h1>todo</h1>
      <Add onConfirm={handleAdd} />
      <List list={ list } />
    </div>
  )
}

export default Index
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

# useContext() 共享状态钩子

如果需要在组件之间共享状态,可以使用useContext()。

现在有两个组件 Navbar 和 Messages,我们希望它们之间共享状态。

<div className="App">
  <Navbar/>
  <Messages/>
</div>
1
2
3
4

第一步就是使用 React 的 Context API React.createContext(),在组件外部建立一个 Context。

const AppContext = React.createContext({})
1

组件封装代码如下。

<AppContext.Provider
  value={{
    username: 'superawesome'
  }}
>
  <div className="App">
    <Navbar/>
    <Messages/>
  </div>
</AppContext.Provider>
1
2
3
4
5
6
7
8
9
10

上面代码中,AppContext.Provider 提供了一个 Context 对象,这个对象可以被子组件共享。

Navbar 组件的代码如下。

const Navbar = () => {
  const { username } = useContext(AppContext)
  return (
    <div className="navbar">
      <p>AwesomeSite</p>
      <p>{username}</p>
    </div>
  )
}
1
2
3
4
5
6
7
8
9

上面代码中,useContext() 钩子函数用来引入 Context 对象,从中获取 username 属性。

Message 组件的代码也类似。

const Messages = () => {
  const { username } = useContext(AppContext)

  return (
    <div className="messages">
      <h1>Messages</h1>
      <p>1 message for {username}</p>
      <p className="message">useContext is awesome!</p>
    </div>
  )
}
1
2
3
4
5
6
7
8
9
10
11