React Hook使用☾超详细☽详解

深入解析React 16.8引入的Hooks特性,涵盖useState、useEffect、useMemo、useCallback等,通过实例演示如何在不使用class的情况下管理状态和副作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。同时,它是100% 向后兼容的,不包含任何破坏性改动。

在我们讲解React Hook之前,先讲一下为什么要使用它,就好比谈对象,你得先搞清楚你喜欢他什么。我们知道,在没有 hook 之前,我们写一个 react 项目总是避免不了下面的代码:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

class Example extends React.Component {

  constructor(props) {

    super(props);

    this.state = {

      count: 0

    };

  }

 

  render() {

    return (

      <div>

        <p>You clicked {this.state.count} times</p>

        <button onClick={() => this.setState({ count: this.state.count + 1 })}>

          Click me

        </button>

      </div>

    );

  }

}

我想,对于很多做前端的朋友而言,如果学过后端语言还好,因为他们对类有一个明确的概念,对构造器和继承的理解也很容易,但对于很多直接就开始做前端的朋友(比如:非科班出生的一些自学者),突然让他们接受一个类的概念是很难适应的,虽然 ES6 已经有了 class。但是,如果你用 hook,就可以摆脱 class 了,上面的代码就变成了下面这样:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

import React, { useState } from 'react';

 

function Example() {

  // 声明一个叫 "count" 的 state 变量

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

 

  return (

    <div>

      <p>You clicked {count} times</p>

      <button onClick={() => setCount(count + 1)}>

        Click me

      </button>

    </div>

  );

}

少了extends,class,constuctor,super等关键字,是不是感觉一下子轻松了很多,同时,onClick 的代码也精简了很多。并且,对于前端来说,一个组件实现的是一个功能,用 function 来定义一个组件,比起用 class 来说,更符合语境。

当然,这只是其中的一个点,它的好处还有很多:

Hook 使你在无需修改组件结构的情况下复用状态逻辑,这使得在组件间或社区内共享 Hook 变得更便捷;

Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),而并非强制按照生命周期划分。你还可以使用 reducer 来管理组件的内部状态,使其更加可预测。

Hook 使你在非 class 的情况下可以使用更多的 React 特性,让你的代码远离class。

废话到此结束,接下来,就分别讲讲各个 hook 的用法和作用。不过要记住一点,使用每个 hook 都得先从 react (16.8+版本) 中引入

1

import { useState } from 'react';

 

1、useState

上面的代码已经展示过它的功能了,useState 通过在函数组件里调用它来给组件添加一些内部 state,React 会在重复渲染时保留这个 state。useState 会返回一对值:当前状态和一个让你更新它的函数,你可以在事件处理函数中或其他一些地方调用这个函数。它类似 class 组件的 this.setState,但是它不会把新的 state 和旧的 state 进行合并。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

import React, { useState } from 'react';

 

function Example() {

  // 声明一个叫 "count" 的 state 变量

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

 

  return (

    <div>

      <p>You clicked {count} times</p>

      <button onClick={() => setCount(count + 1)}>

        Click me

      </button>

    </div>

  );

}

useState 支持我们在调用的时候直接传入一个值,来指定 state 的默认值,他支持可以作为js参数一切数据,包括函数,但如果是函数的话,函数必须要有返回值。

1

2

3

4

5

const [ obj,setObj ] = useState({ a: 1 }),

const [ arr,setArr ] = useState([ 1, 2 ]),

const [ count, setCount ] = useState(() => {

  return props.count || 0

})

我们在使用 useState 时,如果想要获取上一轮该 state 的值的话,只需要在 useState 返回的第二个参数,传入一个参数,该函数的参数就是上一轮的 state 的值。

1

2

3

setCount(count){

  return count + 1

}

当我们使用多个 useState 的时候,那 react 是如何识别哪个个是哪个呢?其实很简单,它是靠第一次执行的顺序来记录的,就相当于每个组件存放useState 的地方是一个数组,每使用一个新的 useState,就向数组中 push 一个 useState,所以,当我们在运行时改变 useState 的顺序,数据会混乱,增加 useState,程序会报错。

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

function App (props) {

  let count, setCount

  let sum, setSum

  if (count > 2) {

    [ count, setCount ] = useState(0)

    [ sum, setSum ] = useState(10)

  } else {

    [ sum, setSum ] = useState(10)

    [ count, setCount ] = useState(0)

  }

  return (

    <div>

      点击次数: { count }

      总计:{ sum }

      <button onClick={() => { setCount(count + 1); setSum(sum - 1)}}>点我</button>

    </div>

    )

}

 

2、useEffect

如果你熟悉 React Class 的生命周期函数,你可以把 useEffect 看做是 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合,并且它会根据你传递的第二个参数达到灵活多变的效果。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

// 不使用useEffect

class App extends PureComponent {

  componentDidMount() {

    document.title = count

  }

  componentDidUpdate() {

    document.title = count

  }

}

// 使用useEffect

function App () {

  useEffect(() => {

    document.title = count

  })

}

 

useEffect 支持第二个参数,分三种情况:

1、什么都不传,组件每次 render 之后 useEffect 都会调用,相当于 componentDidMount 和 componentDidUpdate;

2、传入一个空数组 [], 只会调用一次,相当于 componentDidMount 和 componentWillUnmount;

3、’传入一个数组,其中包括变量,只有这些变量变动时,useEffect里的方法才会执行。

如果 useEffect 最后 return 了一个方法,return 的方法会在 componentWillUnmount 阶段执行,比如定时器的清除,就可以通过 return 一个 clearTimer 方法来搞定。

在实际开发中,适当的把逻辑拆分成多个 effect,不仅业务清晰,再配合好第二个参数,生命周期的问题就迎刃而解了。且看下面例子:

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

function App () {

    const [ count, setCount ] = useState(0)

    const [ width, setWidth ] = useState(document.body.clientWidth)

 

    const onChange = () => {

      setWidth(document.body.clientWidth)

    }

    

    useEffect(() => {

      // 相当于 componentDidMount

      window.addEventListener('resize', onChange, false)

 

      return () => {

        // 相当于 componentWillUnmount

        window.removeEventListener('resize', onChange, false)

      }

    }, [])

 

    useEffect(() => {

      // 相当于 componentDidUpdate

      document.title = count

    })

 

    useEffect(() => {

      console.log(`count change: count is ${count}`)

    }, [ count ])

 

    return (

      <div>

        页面名称: { count }

        页面宽度: { width }

        <button onClick={() => { setCount(count + 1)}}>点我</button>

      </div>

      )

  }

3、useMemo

useMemo 主要用于一个变量依赖于另一个变量,有点类似于vue的计算属性,主要用于性能优化。同时它也支持传入第二个参数,用法和 useEffect 类似。不过需要注意的是,它的首次执行是在渲染的时候,而不是渲染完成之后。

 

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

function App(props) {

  const { pullUp, pullDown, onScroll } = props;

 

  // 对传入的上拉和下拉函数进行防抖处理

  let pullUpDebounce = useMemo(() => {

    return debounce(pullUp, 300)

  }, [pullUp]);

 

  let pullDownDebounce = useMemo(() => {

    return debounce(pullDown, 300)

  }, [pullDown]);

 

  // 绑定scrollEnd方法

  useEffect(() => {

    bScroll.on('scrollEnd', () => {

      //判断是否滑动到了底部

      if(bScroll.y <= bScroll.maxScrollY + 100){

        pullUpDebounce();

      }

    });

    return () => {

      bScroll.off('scrollEnd');

    }

  }, []);

 

  // 绑定touchEnd方法

  useEffect(() => {

    bScroll.on('touchEnd', (pos) => {

      //判断用户的下拉动作

      if(pos.y > 50) {

        pullDownDebounce();

      }

    });

    return () => {

      bScroll.off('touchEnd');

    }

  }, []);

}

 

4、useCallback

useCallback 可以说是 useMemo 的语法糖,能用 useCallback 实现的,都可以使用 useMemo,在 react 中我们经常面临一个子组件渲染优化的问题,尤其是在向子组件传递函数props时,每次 render 都会创建新函数,导致子组件不必要的渲染,浪费性能,这个时候,就是 useCallback 的用武之地了,useCallback 可以保证,无论 render 多少次,我们的函数都是同一个函数,减小不断创建的开销。

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

// 例1

const onClick = useMemo(() => {

  return () => {

    console.log('button click')

  }

}, [])

 

const onClick = useCallback(() => {

console.log('button click')

}, [])

 

// 例2

const [count1, changeCount1] = useState(0);

const [count2, changeCount2] = useState(10);

 

const calculateCount = useCallback(() => {

  if (count1 && count2) {

    return count1 * count2;

  }

  return count1 + count2;

}, [count1, count2])

 

useEffect(() => {

  const result = calculateCount(count, count2);

  message.info(`执行副作用,最新值为${result}`);

}, [calculateCount])

 

useCallback 和直接使用 useEffect 不同的地方在于使用 useCallback 生成计算的回调后,在使用该回调的副作用中,第二个参数应该是生成的回调。其实这个问题是很好理解的,我们使用 useCallback 生成了一个与 count1 / count2 相关联的回调方法,那么当关联的状态发生变化时会重新生成新的回调,副作用监听到了回调的变化就会去重新执行副作用,此时 useCallback 和 useEffect 是按顺序执行的, 这样就实现了副作用逻辑的抽离。同样,useCallback 的第二个参数和 useMemo一样,没有区别。

5、useRef

useRef 总共有两种用法:

1、获取子组件的实例

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

const Children = forwardRef((props, ref) => {

  <div>

    <p>{props.title}</p>

  </div>

})

 

function App () {

  const [ count, setCount ] = useState(0)

  // 如果children组件不是一个forwardRef,这里会报错

  const childrenRef = useRef(null)

  // const

  const onClick = useMemo(() => {

    return () => {

      console.log('button click')

      console.log(childrenRef.current) // 这里可以得到Children实例

      setCount((count) => count + 1)

    }

  }, [])

  return (

    <div>

      点击次数: { count }

      <!-- ref得添加在一个forwardRef上才行 -->

      <Children ref={childrenRef}  count={count}></Children>

      <button onClick={onClick}>点我</button>

    </div>

    )

}


2、在函数组件中的一个全局变量,不会因为重复 render 而重复申明, 类似于类组件的 this.xxx。有些情况下,我们需要保证函数组件每次 render 之后,某些变量不会被重复申明,比如说 Dom 节点,定时器的 id 等等,在类组件中,我们完全可以通过给类添加一个自定义属性来保留,比如说 this.xxx, 但是函数组件没有 this,自然无法通过这种方法使用,有的朋友说,我可以使用 useState 来保留变量的值,但是 useState 会触发组件 render,在这里完全是不需要的,我们就需要使用 useRef 来实现了。

 

 

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

function App () {

  const [ count, setCount ] = useState(0)

  // 注意,这里的ref并没有指定给任何元素

  const timer = useRef(null)

  console.log(timer)

 

  let timer2

 

  useEffect(() => {

    let id = setInterval(() => {

      setCount(count => count + 1)

    }, 500)

 

    timer.current = id

    timer2 = id

    return () => {

      clearInterval(timer.current)

    }

  }, [])

 

  const onClickRef = useCallback(() => {

    clearInterval(timer.current)

  }, [])

 

  const onClick = useCallback(() => {

    clearInterval(timer2)

  }, [])

 

  return (

    <div>

      点击次数: { count }

      <button onClick={onClick}>普通</button>

      <button onClick={onClickRef}>useRef</button>

    </div>

  )

}

 

6、useImperativeHandle

useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值,说简单点就是,子组件可以选择性的暴露给父组件一些方法,这样可以隐藏一些私有方法和属性,官方建议,useImperativeHandle 应当与 forwardRef 一起使用。

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

function Kun (props, ref) {

  const kun = useRef()

 

  const introduce = useCallback (() => {

    console.log('i can sing, jump, rap, play basketball')

  }, [])

  // 这里用useImperativeHandle暴露了一个方法出去

  useImperativeHandle(ref, () => ({

    introduce: () => {

      introduce()

    }

  }));

 

  return (

    <div ref={kun}> { props.count }</div>

  )

}

 

const KunKun = forwardRef(Kun)

 

function App () {

  const [ count, setCount ] = useState(0)

  const kunRef = useRef(null)

 

  const onClick = useCallback (() => {

    setCount(count => count + 1)

    // 这里使用暴露出来的方法,执行子组件的内部逻辑

    kunRef.current.introduce()

  }, [])

  return (

    <div>

      点击次数: { count }

      <KunKun ref={kunRef}  count={count}></KunKun>

      <button onClick={onClick}>点我</button>

    </div>

    )

}

 

7、useReducer

useReducer 有点类似 redux 中的功能,相较于 useState,它更适合一些逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等特定场景。

useReducer 总共有三个参数:

第一个参数是 一个 reducer,就是一个函数类似 (state, action) => newState 的函数,传入 上一个 state 和本次的 action;

第二个参数是初始 state,也就是默认值,是比较简单的方法;

第三个参数是惰性初始化,这么做可以将用于计算 state 的逻辑提取到 reducer 外部,这也为将来对重置 state 的 action 做处理提供了便利。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

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 App() {

  const [state, dispatch] = useReducer(reducer, {

    count: 0

  });

  return (

    <>

      点击次数: {state.count}

      <button onClick={() => dispatch({type: 'increment'})}>+</button>

      <button onClick={() => dispatch({type: 'decrement'})}>-</button>

    </>

  );

}

 

好了,讲了那么多,我们似乎知道了 Hook 究竟是怎么一回事,说白了,其实 Hook 就是返回包含了更多逻辑的 State 以及改变 State 的方法。

接下来我们来自定义一个自己的钩子,以计数器来为例:

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

import React, { useState } from 'react';

 

// 编写我们自己的hook,名字以use开头

function useCounter(initialValue) {

  // 接受初始化的值生成state

  const [count, changeCount] = useState(initialValue);

  // 声明减少的方法

  const decrease = () => {

    changeCount(count - 1);

  }

  // 声明增加的方法

  const increase = () => {

    changeCount(count + 1);

  }

  // 声明重置计数器方法

  const resetCounter = () => {

    changeCount(0);

  }

  // 将count数字与方法返回回去

  return [count, { decrease, increase, resetCounter }]

}

 

export default function myHooksView() {

  // 在函数组件中使用我们自己编写的hook生成一个计数器,并拿到所有操作方法的对象

  const [count, controlCount] = useCounter(10);

  return (

    <div>

      当前数量:{count}

      <button onClick={controlCount.decrease}>减少</button>

      <button onClick={controlCount.increase}>增加</button>

      <button onClick={controlCount.resetCounter}>重置</button>

    </div>

  )

}

 

本文主要参考文章:

https://www.cnblogs.com/ascoders/p/10591832.html

https://blog.youkuaiyun.com/landl_ww/article/details/102158814

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值