掘金文章归档 · 第 65 篇 React 生态 4/5 Hooks · 对比 Vue3

react 内置 hook 介绍及基础用法,对比 vue3 的语法实践

10 个内置 Hook:作用、用法、注意点、场景,逐个对标 vue3 语法

原文作者:随意_(掘金) | juejin.cn/post/7401824176228958242 | 发布于 2024-08-12 | 阅读 413 · 约 26 分钟

0背景

最近由 vue 转为 react,记录下 react 16.8 版本 hook 编程,当然 vue3 现在也流行 hook 编程,部分 hook 与 vue3 做对比使用。仅作记录,有帮助的可以取舍。

1何为 hook

React 函数编程中内置的 Hook 是一套允许在函数组件中使用 state 和其他 React 特性的函数。React 提供了多个内置的 Hook,这些 Hook 极大地扩展了函数组件的功能,使得在无需编写类组件的情况下也能使用 React 的全部功能。即 hook 函数组件的,可以在函数内部使用类似于类组件的特性,下面一一介绍下。

2创建测试项目

npm create vite@latest react-test

选择 react 即可,按提示安装依赖并启动。

31. useState

作用

useState 是 React Hooks 中的一个基础 Hook,它允许你在函数组件中添加状态(state)。在类组件中,我们通过 this.state 来管理组件的状态,但在函数组件中,由于没有实例(this),所以 React 引入了 Hooks 来让函数组件也能拥有状态和其他 React 特性。

用法

useState 的基本用法是调用它并传入一个初始状态值。它返回一个状态变量和一个让你更新它的函数,你可以在组件中通过这两个返回值来管理状态。

import { useState } from 'react'
import './App.css'

function App() {
  // 它返回一个状态变量和一个更新该状态的函数
  const [count, setCount] = useState(0)

  // setCount 为更改状态 state 函数,每次调用会重新 render

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

export default App

注意点

使用场景

useState 的使用场景非常广泛,几乎涵盖了所有需要状态管理的场景。以下是一些常见的使用场景:

import { useState } from 'react'
import './App.css'

function App() {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = (event) => {
    event.preventDefault();
    console.log(`Username: ${username}, Password: ${password}`);
    // 这里可以添加登录逻辑
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Username:
        <input type="text" value={username} onChange={(e) => setUsername(e.target.value)} />
      </label>
      <label>
        Password:
        <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
      </label>
      <button type="submit">Login</button>
    </form>
  );
}

export default App
⚠️ react 中并没有 vue 中 v-model 的双向绑定,如果需要双向绑定,则借助 onChange 事件动态改变 state。

42. useEffect

作用

useEffect 是 React Hooks 中的一个重要 Hook,它用于在函数组件中执行副作用操作。副作用是指那些发生在渲染之外,但可能与渲染相关的操作,如数据获取、订阅外部数据源、手动修改 DOM 等。useEffect 的主要作用是在组件渲染到屏幕之后执行这些副作用操作(副作用可以简单理解为,如类似 vue 中组件生命周期、监听 watch 等等)。

用法

useEffect 接受两个参数:一个副作用函数和一个可选的依赖项数组。

import { useState, useEffect } from 'react'
import './App.css'

function App() {
  useEffect(() => {
    // 副作用操作
    console.log('Component mounted or updated');

    // 清理函数(可选)
    return () => {
      console.log('Cleanup function called');
    };
  }, [/* 依赖项数组 */]);

  return <div>My Component</div>;
}

export default App

场景 1:充当生命周期

useEffect 可以通过不同的方式来模拟类组件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 生命周期方法。

模拟 componentDidMount 和 componentWillUnmount

当你想要在某个组件挂载到 DOM 后执行代码,并在组件卸载时执行清理操作(如取消网络请求、移除事件监听器等)时,可以将一个不带依赖项数组(或空数组)的 useEffect 用作 componentDidMount,并通过返回一个清理函数来模拟 componentWillUnmount。

import { useEffect } from 'react'
import './App.css'

function App() {
  useEffect(() => {
    // 相当于 componentDidMount
    console.log('Component mounted');

    // 假设我们在这里设置了一个定时器
    const timerId = setTimeout(() => {
      console.log('Timer executed');
    }, 1000);

    // 清理函数,相当于 componentWillUnmount
    return () => {
      clearTimeout(timerId);
      console.log('Component will unmount');
    };
  }, []); // 空数组表示这些副作用仅在挂载和卸载时运行

  return <div>Hello, MyComponent!</div>;
}

export default App

模拟 componentDidUpdate,类似于 vue 的 watch

如果你想要在某个状态或属性变化时执行副作用,可以将这些状态或属性作为 useEffect 的依赖项数组的一部分。这样,每当依赖项变化时,副作用就会重新执行。

import { useState, useEffect } from 'react'
import './App.css'

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

  useEffect(() => {
    // 每当 count 变化时,这里的代码就会执行
    // 相当于 componentDidUpdate(但请注意,它也会在首次渲染后执行)
    console.log(`Count is: ${count}`);

    // 这里不需要返回清理函数,除非有资源需要清理
  }, [count]); // 依赖项数组包含 count

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

export default App

注意:useEffect 也会在组件首次渲染后执行。如果你想要避免在首次渲染时执行副作用,可以通过添加一个额外的条件来检查(例如,检查某个状态是否已初始化)。

场景 2:数据获取

import { useState, useEffect } from 'react'
import './App.css'

function App() {
  const [user, setUser] = useState(null);

  useEffect(() => {
    const fetchUser = async () => {
      const response = await fetch('https://api.example.com/user');
      const data = await response.json();
      setUser(data);
    };

    // 初始化执行请求
    fetchUser();

    // 清理函数(在这个例子中可能不需要,因为没有需要清理的资源)
    return () => {};
  }, []); // 空数组表示只在组件挂载时执行一次

  if (!user) {
    return <div>Loading...</div>;
  }

  return <div>Name: {user.name}</div>;
}

export default App

53. useRef

作用

useRef 是 React Hooks 中的一个重要 Hook,它提供了一种在组件的整个生命周期内保持数据不变的方式。与 useState 不同,useRef 返回的引用对象在组件的整个生命周期内保持不变,包括其 .current 属性。这使得 useRef 非常适用于那些不需要引起组件重新渲染,但需要跨渲染周期保持数据的场景。可以简单理解为 vue 中的 ref,获取 DOM 的引用。

用法

useRef 接收一个初始值作为参数(这个初始值只在组件的第一次渲染时设置),并返回一个对象,该对象拥有一个 .current 属性,你可以通过这个属性来访问或修改存储的值。

import { useRef } from 'react'
import './App.css'

function App() {
  const myRef = useRef(null); // initialValue 是可选的

  // 访问或修改 ref 的值
  console.log(myRef.current); // 访问
  myRef.current = "aaa"; // 修改
  console.log(myRef.current); // 访问

  return <div>My Component</div>;
}

export default App

注意点

使用场景 1:访问 DOM 元素

import { useRef } from 'react'
import './App.css'

function App() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` 指向已挂载到 DOM 上的文本输入元素
    inputEl.current.focus();
  };

  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

export default App

使用场景 2:存储定时器 ID

import { useRef, useEffect } from 'react'
import './App.css'

function App() {
  const timerRef = useRef(null);

  useEffect(() => {
    timerRef.current = setTimeout(() => {
      console.log('Timer executed');
    }, 1000);

    return () => {
      clearTimeout(timerRef.current);
    };
  }, []);

  return <div>Timer will execute after 1 second</div>;
}

export default App

使用场景 3:跨渲染周期保持数据

import { useRef, useEffect, useState } from 'react'
import './App.css'

function App() {
  const prevCountRef = useRef(0);
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log(`Count changed from ${prevCountRef.current} to ${count}`);
    prevCountRef.current = count;
  }, [count]);

  return (
    <div>
      <p>prevCountRef: {prevCountRef.current}</p>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

export default App

在这个例子中,prevCountRef 用于存储上一次的 count 值,以便在 count 变化时进行比较。prevCountRef.current 自点击后,总比 count 小 1,即存储的是上一次的 count 状态。

64. useMemo

作用

useMemo 是 React 提供的一个 Hook,主要用于性能优化。它通过缓存函数的计算结果来避免在每次组件渲染时都重新执行昂贵的计算或重新创建相同的对象。只有当依赖项发生变化时,useMemo 才会重新计算并返回新的结果,否则它会返回上一次缓存的结果。类似于 vue 的 computed。

用法

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

注意点

使用场景 1:昂贵的计算

import { useState, useMemo } from 'react'
import './App.css'

const ExpensiveComponent = ({ input }) => {
  const expensiveValue = useMemo(() => {
    // 假设这是一个非常昂贵的计算过程
    return computeExpensiveValue(input);
  }, [input]);

  return <div>{expensiveValue}</div>;
};

// 假设 computeExpensiveValue 是一个实际存在的昂贵计算函数
function computeExpensiveValue(input) {
  // ... 执行一些昂贵的计算 ...
  return `Processed ${input}`;
}

function App() {
  const [input, setInput] = useState('initial value');

  return (
    <div>
      <input
        value={input}
        onChange={(e) => setInput(e.target.value)}
        placeholder="Enter something"
      />
      <ExpensiveComponent input={input} />
    </div>
  );
}

export default App

使用场景 2:类似 vue 的 computed 计算属性

// 使用 useMemo 计算属性
const chunklistChecked = useMemo(() => {
  return chunklist
    .filter((item) => item.isChecked)
}, [chunklist]);

当 chunklist 变化时,筛选出 chunklist 中 item.isChecked 的项。类似于计算属性。

75. useCallback

作用

useCallback 是 React 中的一个 Hook,它返回一个记忆化的回调函数。这个回调函数只有在它的依赖项改变时才会更新。useCallback 的主要作用是防止在每次渲染时都创建新的函数实例,这对于优化性能以及避免在子组件中由于父组件频繁重新渲染而导致的不必要的重新渲染是非常有用的。

用法

const memoizedCallback = useCallback(
  () => {
    // 执行一些操作
  },
  [dep1, dep2, /* ... */],
);

注意点

使用场景

当你将回调函数作为 prop 传递给子组件,并且子组件依赖于该回调函数的引用稳定性时,可以使用 useCallback 来避免在父组件每次渲染时都创建新的函数实例。

import { useState, useEffect, useCallback } from 'react'
import './App.css'

const Child = ({ onChildEvent }) => {
  useEffect(() => {
    // 假设这里有一些逻辑,当某些事件发生时调用 onChildEvent
    console.log('Child effect runs');
  }, [onChildEvent]); // 注意这里的依赖项

  return <div>Child Component</div>;
};

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

  // 使用 useCallback 来记忆化回调函数
  const memoizedCallback = useCallback(() => {
    console.log('Callback called from child');
  }, []); // 这里的依赖项数组为空,因为回调函数不依赖于任何状态或 prop

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      {Array.from({ length: count }, (_, i) => (
        <Child key={i} onChildEvent={memoizedCallback} />
      ))}
    </div>
  );
}

export default App

useCallback 和 useMemo 的区别

📌 两者都是性能优化的工具:useCallback 优化回调函数(减少不必要的重新创建,主要用于传递给子组件的回调);useMemo 优化计算值(减少昂贵的计算,主要用于从 props 或 state 派生的数据)。

86. useImperativeHandle 和 forwardRef

作用

useImperativeHandle 是 React 中的一个自定义 Hook,它的主要作用是允许子组件自定义暴露给父组件的实例值。这通常与 forwardRef 一起使用,以便父组件可以通过 ref 访问子组件内部的方法或属性。通过使用 useImperativeHandle,子组件可以选择性地暴露其内部状态或方法给父组件,从而实现更灵活和可控的组件交互。

useImperativeHandle(ref, createHandle, [deps]);

使用场景

向父组件暴露子组件的方法:当父组件需要调用子组件内部的方法时,可以使用 useImperativeHandle 将这些方法暴露给父组件。

import { useRef, useImperativeHandle, forwardRef } from 'react'
import './App.css'

// 子组件
const ChildComponent = forwardRef((props, ref) => {
  const doSomething = () => {
    console.log('Doing something in ChildComponent');
  };

  useImperativeHandle(ref, () => ({
    doSomething,
  }));

  return <div>Child Component</div>;
});

function App() {
  const childRef = useRef(null);

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

  return (
    <div>
      <ChildComponent ref={childRef} />
      <button onClick={handleClick}>Call Child Method</button>
    </div>
  );
}

export default App
📌 子组件将内部方法暴露给父组件,父组件通过 ref 来调用,有些类似于 vue 的 defineExpose。

97. useContext 数据状态共享

作用

useContext 是 React 提供的一个钩子(Hook),用于在函数式组件中访问上下文(Context)的值。它使得在组件树中传递数据变得更加简单和高效,特别是当需要在多个层级之间共享数据时,避免了通过显式的 props 层层传递的繁琐。通过 useContext,组件可以直接访问到由 Context.Provider 提供的值,实现了跨组件层级的数据共享。

用法

注意点

使用场景:共享主题颜色

import { useState, createContext, useContext } from 'react'
import './App.css'

// 创建一个 Context
const ThemeContext = createContext({ theme: 'light' });

// 父组件,设置主题
function ParentComponent() {
  const [theme, setTheme] = useState('light');

  return (
    <ThemeContext.Provider value={{ theme }}>
      <ChildComponent />
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>Toggle Theme</button>
    </ThemeContext.Provider>
  );
}

// 子组件,使用主题
function ChildComponent() {
  const { theme } = useContext(ThemeContext);

  return <div style={{ color: theme === 'light' ? 'black' : 'red' }}>Hello, {theme} theme!</div>;
}

// 渲染应用
function App() {
  return <ParentComponent />;
}

export default App
📌 这个 hook 主要用来跨组件传值的,可以理解为 vue 的 provide 和 inject。当然你也可以使用 zustand 等状态管理来替代。

108. useReducer

作用

useReducer 是 React 提供的一个用于状态管理的 Hook,它接收一个 reducer 函数和初始状态作为参数,并返回当前状态以及一个 dispatch 函数。useReducer 的主要作用是处理组件中复杂的状态逻辑,特别是当状态更新依赖于先前的状态或需要执行一些复杂计算时。

用法

使用场景

import { useReducer } from 'react'
import './App.css'

// 定义初始状态
const initialState = { count: 0 };

// 定义 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 App() {
  // 使用 useReducer 初始化状态和获取 dispatch 函数
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      Count: {state.count}
      <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
    </div>
  );
}

export default App

useReducer 和 useState 区别

对比项useStateuseReducer
语法const [state, setState] = useState(initialState)const [state, dispatch] = useReducer(reducer, initialState)
适用场景简单的状态管理(表单输入、开关等)复杂的状态逻辑(购物车、多操作计数器等)
复杂度适合单个或简单状态集合适合多个相关状态,按 action.type 拆分更新逻辑
更新逻辑独立更新,不受之前状态影响reducer 按动作类型定义不同更新逻辑
📌 可替换 Redux。对于简单的状态管理,useState 通常更好;对于复杂的状态逻辑,useReducer 更灵活可控。

119. useLayoutEffect

作用

useLayoutEffect 是 React 中的一个 Hook,用于在所有的 DOM 变更之后同步执行某些操作。这些操作会在浏览器进行绘制之前完成,因此可以用于在渲染输出到屏幕之前读取 DOM 布局并同步触发重绘或进行其他需要同步执行的 DOM 操作。

用法

useLayoutEffect 的用法与 useEffect 非常相似,它们都接受一个包含副作用操作的函数和一个依赖项数组作为参数。但是,useLayoutEffect 中的副作用操作会在浏览器绘制之前同步执行。

import { useState, useLayoutEffect } from 'react'
import './App.css'

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

  useLayoutEffect(() => {
    // 这里的代码会在 DOM 更新后立即同步执行
    // 如果你在这里读取 DOM 的某些属性或执行影响布局的操作
    // 它会在浏览器绘制之前完成
    const myElement = document.getElementById('my-element');
    if (myElement) {
      myElement.style.color = 'red'; // 这将在浏览器绘制之前设置颜色
    }

    return () => {
      // 清理函数会在组件卸载前或下一次 useLayoutEffect 运行前执行
    };
  }, [count]); // 依赖项数组

  return (
    <div>
      <p id="my-element">Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

export default App

注意点

典型场景:挂载后立即聚焦输入框

import { useRef, useLayoutEffect } from 'react'
import './App.css'

function App() {
  const inputRef = useRef(null);

  useLayoutEffect(() => {
    if (inputRef.current) {
      inputRef.current.focus();
    }
  }, []); // 空数组表示只在组件挂载时执行

  return <input ref={inputRef} type="text" placeholder="Type here..." />;
}

export default App

useLayoutEffect 和 useEffect 区别

对比项useEffectuseLayoutEffect
触发时机组件渲染到屏幕之后异步执行,不阻塞绘制DOM 更新完成后同步执行,但在浏览器绘制之前
对渲染影响不阻塞页面渲染,适合数据获取、订阅、定时器可能阻塞渲染,适合读取布局、调整滚动、设置焦点
使用场景大多数不需要立即执行的副作用需要 DOM 更新后立即同步执行的副作用,避免闪烁

1210. useDebugValue

作用

React 的 useDebugValue 是一个 Hook,它主要用于在开发过程中帮助开发者调试自定义 Hook。它的主要作用是将自定义 Hook 中的某些值暴露给 React 开发工具(如 React DevTools),以便于开发者在调试时能够更直观地查看和理解组件的状态。它类似于在开发过程中为自定义 Hook 打上了一个"标签"。

注意点

使用场景

import { useState, useDebugValue } from 'react'
import './App.css'

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

  // 使用 useDebugValue 暴露 count 值给 React DevTools
  useDebugValue(`Count: ${count}`);

  const increment = () => setCount(count + 1);
  const decrement = () => setCount(count - 1);

  return [count, increment, decrement];
}

// 渲染应用
function App() {
  const [count, increment, decrement] = useMyCounter(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
    </div>
  );
}

export default App
📌 此 hook 仅为了调试,请注意取舍。

13结尾

以上信息部分借助 AI 工具生成,全部例子经过验证,仅作记录,按需取舍。

截图占位:10 个 Hook 与 Vue3 语法的对照速查(useState↔ref、useEffect↔watch/生命周期、useMemo↔computed、useRef↔ref、useContext↔provide/inject、useImperativeHandle↔defineExpose)