Skip to content

React Hooks

函数式组件中没有自己的 this。

React Hooks 是 React 16.8 版本引入的新特性,它允许你在不编写 class 的情况下使用 state 和其他 React 特性。Hooks 是一种可以让你在函数组件中“钩入” React 特性的函数。

Hook 只能在函数组件中使用,不能在类组件,或者函数组件之外的地方使用。且只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。

一、useState

useState 会帮助我们定义一个 state 变量,它与 class 里面的 this.state 提供的功能完全相同。

  • 一般来说,在函数退出后变量就会”消失”,而 state 中的变量会被 React 保留。

    useState 接受唯一一个参数,在第一次组件被调用时使用来作为初始化值。(如果没有传递参数,那么初始化值为 undefined)。

  • useState 的返回值是一个数组,可以通过数组的解构。第一个元素是 state ,第二个是更新 state 的函数。

利用函数式组件完成的点我加 1 案例。

jsx
function Demo() {
    const [count, setCount] = React.useState(0)
    console.log(count, setCount);
    function add() {
        setCount(count + 1)
    }
    return (
        <div>
            <h2>当前求和为:{count}</h2>
            <button onClick={add}>点我加1</button>
        </div>
    )
}
export default Demo

useState 让函数式组件能够维护自己的 state,它接收一个参数,作为初始化 state 的值,赋值给 count,因此 useState 的初始值只有第一次有效,它所映射出的两个变量 count 和 setCount,setCount 可以理解为 setState 来使用。

count 是初始化的值,而 setCount 就像是一个 action 对象驱动状态更新。

可以通过 setCount 来更新 count 的值。

jsx
setCount(count + 1)

二、useEffect

1. 第一个参数

在类式组件中,提供了一些声明周期钩子给我们使用,我们可以在组件的特殊时期执行特定的事情。例如 componentDidMount,能够在组件挂载完成后执行一些东西。

在函数式组件中也可以实现,它采用的是 effectHook。它的语法更加的简单,同时融合了 componentDidUpdata 生命周期,极大的方便了我们的开发。

由于函数的特性,我们可以在函数中随意的编写函数,这里我们调用了 useEffect 函数,这个函数有多个功能。

jsx
React.useEffect(() => {
    console.log('被调用了');
})

当我们像上面代码那样使用时,它相当于 componentDidUpdata 和 componentDidMount 一同使用,也就是在组件挂载和组件更新的时候都会调用这个函数。

2. 第二个参数

它还可以接收第二个参数,这个参数表示它要监测的数据,也就是他要监视哪个数据的变化。

当我们不需要监听任何状态变化的时候,就传递一个空数组,这样它就能当作 componentMidMount 来使用。如果返回值是函数,就相当于 componentDidUnmount 和 componentMidMount。

jsx
React.useEffect(() => {
    console.log('被调用了');
}, [])

这样我们只有在组件第一次挂载的时候触发。

当然当页面中有多个数据源时,我们也可以选择个别的数据进行监测以达到我们想要的效果。

jsx
React.useEffect(() => {
    console.log('被调用了');
}, [count])

这样,我们就只监视 count 数据的变化。

3. 第一个参数返回值

当想要在卸载一个组件之前进行一些清除定时器的操作,在类式组件中,调用生命周期钩子 componentDidUnmount 来实现。在函数式组件中,直接在 useEffect 的第一个参数的返回值中实现即可。也就是说,第一个参数的函数体相当于 componentDidMount。返回体相当于 componentDidUnmount,这样就能实现在组件即将被卸载时输出一些东西了。

实现卸载

jsx
function unmount() {
    ReactDOM.unmountComponentAtNode(document.getElementById("root"))
}

卸载前输出

jsx
React.useEffect(() => {
    console.log('被调用了');
    return () => {
        console.log('我要被卸载了');
    }
}, [count])

因此 useEffect 相当于三个生命周期钩子:componentDidMount、componentDidUpdata、componentDidUnmount。

useEffect 在一个函数式组件中可以书写多次,useEffect 会按照声明的顺序依次调用组件中的每一个 effect。

三、useContext

useContext 允许在函数组件中直接访问 Context 的值。

jsx
import React, { useContext } from 'react';

// 创建一个 Context
const ThemeContext = React.createContext('light');

// 父组件
function ParentComponent() {
  return (
    // 使用 Provider 提供 Context
    <ThemeContext.Provider value="dark">
      <ChildComponent />
    </ThemeContext.Provider>
  );
}

// 子组件
function ChildComponent() {
  // 使用 useContext 访问 Context
  const theme = useContext(ThemeContext);

  return <p>当前的主题是:{theme}</p>;
}

首先创建了一个 Context,并设置了默认值为 'light'。然后在 ParentComponent 组件中,使用 ThemeContext.Provider 来提供 Context,并设置了 value 为 'dark'。

ChildComponent 组件中,使用 useContext Hook 来访问 Context 的值。useContext 接受一个 Context 对象(返回的值从 React.createContext)作为参数,并返回该 Context 的当前值。当前的 Context 值是由上层组件中距离当前组件最近的 <ThemeContext.Provider>value prop 决定的。

因此,ChildComponent 组件中的 theme 值为 'dark',而不是 ThemeContext 的默认值 'light'。这样,我们就可以在函数组件中直接访问 Context 的值,无需使用 Context.Consumer 组件,也无需在组件树中逐层传递 props。

四、useReducer

useReducer 接受一个 reducer 函数和一个初始状态作为参数,返回当前的状态和一个 dispatch 函数。这个 Hook 主要用于处理复杂的 state 逻辑。

useReducer 只是 useState 的一种替代品,并不能替代 Redux。

jsx
import React, { useReducer } from 'react';

// 定义 reducer 函数
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() {
  // 使用 useReducer
  const [state, dispatch] = useReducer(reducer, {count: 0});

  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'increment'})}>+1</button>
      <button onClick={() => dispatch({type: 'decrement'})}>-1</button>
    </>
  );
}

五、useCallback

useCallback 可以返回一个记忆化的版本的回调函数。

它接受两个参数:一个是我们需要记忆化的函数,另一个是一个依赖项数组。当依赖项改变时,useCallback 会返回一个新的记忆化的函数。如果依赖项没有改变,它会返回上一次记忆化的函数。

useCallback 主要用于优化性能。当你把一个回调函数作为 prop 传递给一个子组件时,如果这个回调函数在每次渲染时都是一个新的函数,那么子组件可能会进行不必要的重新渲染。通过使用 useCallback,你可以确保只有当回调函数的依赖项改变时,才会生成一个新的函数。

jsx
import React, { useState, useCallback } from 'react';

function ChildComponent({ onClick }) {
  console.log('ChildComponent rendering');
  return <button onClick={onClick}>Click me</button>;
}

const MemoizedChildComponent = React.memo(ChildComponent);

function ParentComponent() {
  const [count, setCount] = useState(0);
  const [otherState, setOtherState] = useState(0);

  const increment = useCallback(() => {
    setCount(count + 1);
  }, [count]);

  console.log('ParentComponent rendering');

  return (
    <div>
      <button onClick={() => setOtherState(otherState + 1)}>Change other state</button>
      <MemoizedChildComponent onClick={increment} />
    </div>
  );
}

ChildComponent 是一个纯组件,只有当它的 props 改变时,它才会重新渲染。ParentComponent 有两个 state:countotherState,并且它传递一个 increment 函数给 ChildComponent

如果我们不使用 useCallback,那么每次 ParentComponent 渲染时,都会创建一个新的 increment 函数,这会导致 ChildComponent 重新渲染,即使 ChildComponent 的 props 没有改变。

但是如果我们使用 useCallback,那么只有当 increment 函数的依赖项 count 改变时,increment 函数才会被重新创建。如果我们点击 "Change other state" 按钮,ParentComponent 会重新渲染,但是 increment 函数和 count 都没有改变,所以 useCallback 会返回上一次记忆的 increment 函数,ChildComponent 不会重新渲染。

jsx
// 进一步的优化: 当count发生改变时, 也使用同一个函数
// 做法一: 取消count依赖, 缺点: 闭包陷阱
// 做法二: useRef, 在组件多次渲染时, 返回的是同一个值
const countRef = useRef()
countRef.current = count
const increment = useCallback(function foo() {
	console.log("increment")
	setCount(countRef.current + 1)
}, [])

通常使用 useCallback 的目的是不希望子组件进行多次渲染,并不是为了函数进行缓存。

六、useMemo

useMemo 用于优化性能,通过记忆化(也就是缓存)函数的返回值来避免不必要的计算。

useMemo 接受两个参数:一个函数和一个依赖项数组。它会返回这个函数的记忆化结果。只有当依赖项改变时,函数的结果才会被重新计算。如果依赖项没有改变,useMemo 会返回上一次计算的结果。

这在处理计算密集型操作时特别有用,因为你可以避免在每次渲染时都重新计算结果。

jsx
import React, { useState, useMemo } from 'react';

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

  const expensiveComputation = useMemo(() => {
    let result = 0;
    for (let i = 0; i < 1000000000; i++) {
      result += i;
    }
    return result;
  }, []); // 依赖项数组为空,所以这个函数的结果只会被计算一次

  return (
    <div>
      Count: {count}
      <button onClick={() => setCount(count + 1)}>+1</button>
      <p>Expensive computation result: {expensiveComputation}</p>
    </div>
  );
}

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

七、useRef

当想要获取组件内的信息时,在类式组件中,会采用 ref 的方式来获取。在函数式组件中,也可以采用 ref。但是,需要采用 useRef 函数来创建一个 ref 容器,这和 createRef 很类似。

jsx
import React, { useRef } from 'react';

function MyComponent() {
  // 1.创建容器
  const myRef = useRef();

  const handleClick = () => {
    myRef.current.focus();
  };

  return (
    <div>
      {/* 2.搜集元素到容器 */}
      <input ref={myRef} type="text" />
      <button onClick={handleClick}>Focus the input</button>
    </div>
  );
}

export default MyComponent;

解决闭包陷阱。

八、useImperativeHand

useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。

在大多数情况下,我们推荐你避免使用 ref,因为它会破坏组件的封装。但是在某些情况下,你可能需要在父组件中调用子组件的方法,或者访问子组件的某些特定值。这时,你可以使用 useImperativeHandle

useImperativeHandle 接受两个参数:一个 ref 和一个函数。这个函数返回一个对象,这个对象的值将会被附加到 ref 上。这样,父组件就可以通过 ref 访问到这些值。

jsx
import React, { useRef, useImperativeHandle, forwardRef } from 'react';

function ChildComponent(props, ref) {
  useImperativeHandle(ref, () => ({
    sayHello() {
      console.log('Hello from ChildComponent');
    }
  }));

  return <div>ChildComponent</div>;
}

const ForwardedChildComponent = forwardRef(ChildComponent);

function ParentComponent() {
  const childRef = useRef();

  const handleClick = () => {
    childRef.current.sayHello();
  };

  return (
    <div>
      <ForwardedChildComponent ref={childRef} />
      <button onClick={handleClick}>Call child method</button>
    </div>
  );
}

在这个例子中,ChildComponent 使用 useImperativeHandle 来暴露一个 sayHello 方法给父组件。父组件通过 ref 可以调用这个方法。注意,因为 useImperativeHandle 需要配合 ref 使用,所以 ChildComponent 必须使用 forwardRef 来转发 ref。

九、useLayoutEffect

useLayoutEffect 的行为和 useEffect 非常相似,都是用来在组件渲染后执行副作用函数。但是,useLayoutEffect 的副作用函数会在浏览器执行绘制之前同步调用,这意味着如果你的副作用函数执行的操作会导致组件重新渲染,那么用户将不会看到渲染前后的变化。

这使得 useLayoutEffect 在某些特定场景下非常有用,比如读取 DOM 布局并同步触发重渲染。在这种情况下,使用 useEffect 可能会导致闪烁问题,因为 useEffect 是在浏览器完成绘制后异步调用的。

但是请注意,因为 useLayoutEffect 是同步调用的,所以如果你的副作用函数执行时间过长,可能会阻塞浏览器的绘制,导致性能问题。所以,除非你需要从 DOM 读取布局并同步触发更新,否则通常应该使用 useEffect

jsx
import React, { useLayoutEffect, useRef } from 'react';

function Example() {
  const divRef = useRef();

  useLayoutEffect(() => {
    console.log(divRef.current.getBoundingClientRect());
  }, []);

  return <div ref={divRef}>Hello, world</div>;
}

在这个例子中,useLayoutEffect 用于在组件渲染后立即读取 div 的布局信息。因为我们需要在浏览器绘制前获取这些信息,所以我们使用 useLayoutEffect 而不是 useEffect

十、自定义 hook

案例一:

jsx
import { useContext } from "react"
import { UserContext, TokenContext } from "../context"

function useUserToken() {
  const user = useContext(UserContext)
  const token = useContext(TokenContext)

  return [user, token]
}

export default useUserToken

案例二:

jsx
import { useState, useEffect } from "react"

function useScrollPosition() {
  const [ scrollX, setScrollX ] = useState(0)
  const [ scrollY, setScrollY ] = useState(0)

  useEffect(() => {
    function handleScroll() {
      // console.log(window.scrollX, window.scrollY)
      setScrollX(window.scrollX)
      setScrollY(window.scrollY)
    }

    window.addEventListener("scroll", handleScroll)
    return () => {
      window.removeEventListener("scroll", handleScroll)
    }
  }, [])

  return [scrollX, scrollY]
}

export default useScrollPosition

案例三:

jsx
import { useEffect } from "react"
import { useState } from "react"

function useLocalStorage(key) {
  // 1.从localStorage中获取数据, 并且数据数据创建组件的state
  const [data, setData] = useState(() => {
    const item = localStorage.getItem(key)
    if (!item) return ""
    return JSON.parse(item)
  })

  // 2.监听data改变, 一旦发生改变就存储data最新值
  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(data))
  }, [data])

  // 3.将data/setData的操作返回给组件, 让组件可以使用和修改值
  return [data, setData]
}


export default useLocalStorage

附加:React 18 新 hooks

一、useId

SSR 同构应用

什么是同构?一套代码既可以在服务端运行又可以在客户端运行,这就是同构应用。

Hydration

SSR 结果是 HTML 页面返回给用户,但仅有 HTML 不具有交互性。

为了使我们的页面具有交互性,除了在 Node.js 中将页面呈现为 HTML 之外,UI 框架(Vue / React / ...)还在浏览器中加载和呈现页面。(它创建页面的内部表示,然后将内部表示映射到我们在 Node.js 中呈现的 HTML 的 DOM 元素。)

这个过程称为 hydration。

useId

useId 是一个用于生成横跨服务端和客户端的稳定的唯一 ID 的同时避免 hydration 不匹配的 hook。

二、useTransition

官方解释:返回一个状态值表示过渡任务的等待状态,以及一个启动该过渡任务的函数。

翻译:允许告诉 React,某些更新可以稍后进行,这样就不会立即阻塞用户界面。这对于那些需要从服务器获取数据的操作特别有用,因为这些操作可能需要一些时间。useTransition 返回两个值:一个状态值和一个函数。状态值表示是否有一个过渡任务正在等待,函数用于启动一个新的过渡任务。

jsx
import { useTransition } from 'react';

function Example() {
  const [isPending, startTransition] = useTransition();

  function handleClick() {
    startTransition(() => {
      // 这里的更新会被视为过渡任务
    });
  }

  return (
    <div>
      <button onClick={handleClick}>Start Transition</button>
      {isPending && 'Loading...'}
    </div>
  );
}

当用户点击按钮时,startTransition 函数会被调用,启动一个新的过渡任务。isPending 状态值会被用于显示一个加载指示器,告诉用户有一个任务正在进行。

三、useDeferredValue

官方解释:useDeferredValue 接受一个值,并返回该值的新副本,该副本将推迟到更紧急地更新之后。

翻译:允许延迟一个值的更新。这意味着,即使你已经改变了一个值,React 也可能会继续使用旧的值,直到有足够的时间来处理新的值。这对于那些需要大量计算或者可能导致界面卡顿的更新特别有用。通过使用 useDeferredValue,你可以让 React 在有足够的时间时再处理这些更新,从而保持界面的流畅。

jsx
import { useDeferredValue } from 'react';

function Example() {
  const [text, setText] = useState('');
  const deferredText = useDeferredValue(text);

  function handleChange(event) {
    setText(event.target.value);
  }

  // 使用 deferredText 而不是 text
  return <input value={deferredText} onChange={handleChange} />;
}

当用户在输入框中输入文本时,setText 函数会被调用,改变 text 的值。然而,由于我们使用了 useDeferredValue,React 可能会继续使用旧的 text 值,直到有足够的时间来处理新的值。这可以防止输入框在用户输入时出现卡顿。

Released under the MIT License.