React 中常用的 Hook 介绍(一)

2023年06月11日星期日


React Hooks 的诞生是为了简化组件开发、提高逻辑复用性,并解决类组件在状态管理和生命周期方面的局限性。它使得开发者可以更直观地管理状态和副作用,同时提升了代码的可读性和复用性,极大地改善了 React 的开发体验。

什么是Hook?

Hook有"钩子"的意思。React Hooks的意思是,组件尽量写成纯函数,如果需要外部功能和副作用,就用钩子把外部代码"钩"进来。React Hooks就是那些钩子 1

1.useState

useState 是 React 中最常用的 Hook,用于在函数组件中声明和管理状态。它可以让函数组件具备类似类组件中的 this.statethis.setState 功能。

基本语法

const [state, setState] = useState(initialValue)
  • state:当前的状态值。
  • setState:更新状态的函数。
  • initialValue:状态的初始值,可以是任意类型(数字、字符串、对象、数组等)。

管理简单的状态

import React, { useState } from 'react';
 
function Counter() {
  const [count, setCount] = useState(0);
 
  return (
    <div>
      <p>当前计数:{count}</p>
      <button onClick={() => setCount(count + 1)}>增加</button>
    </div>
  );
}
 
export default Counter;
  • 初始状态 count 为 0。
  • 点击按钮时调用 setCount,将 count 的值增加 1。
  • 组件会重新渲染,并显示更新后的 count

使用函数更新状态

当更新状态依赖于前一个状态值时,使用函数形式的 setState 更加安全。

function Counter() {
  const [count, setCount] = useState(0);
 
  return (
    <div>
      <p>当前计数:{count}</p>
      <button onClick={() => setCount(prevCount => prevCount + 1)}>增加</button>
    </div>
  );
}
  • setCount(prevCount => prevCount + 1) 中的 prevCount 表示之前的状态值。
  • 这种方式可以确保在异步操作或多次快速点击时,状态更新是准确的。

管理对象状态

useState 也可以用来管理对象状态。在更新对象时,需要使用解构赋值或 spread 操作符来保持其他字段不变。

function UserInfo() {
  const [user, setUser] = useState({ name: 'Alice', age: 25 });
 
  const updateName = () => {
    setUser(prevUser => ({ ...prevUser, name: 'Bob' }));
  };
 
  return (
    <div>
      <p>姓名:{user.name}</p>
      <p>年龄:{user.age}</p>
      <button onClick={updateName}>修改姓名</button>
    </div>
  );
}
  • 初始状态 user 是一个对象。
  • updateName 函数中使用 setUser 更新 name 字段,同时通过 { ...prevUser } 保留了 age 字段。
  • React 会重新渲染组件并显示新的用户信息。

管理数组状态

function TodoList() {
  const [todos, setTodos] = useState(['学习 React', '写代码']);
 
  const addTodo = () => {
    setTodos(prevTodos => [...prevTodos, '复习 useState']);
  };
 
  return (
    <div>
      <ul>
        {todos.map((todo, index) => (
          <li key={index}>{todo}</li>
        ))}
      </ul>
      <button onClick={addTodo}>添加任务</button>
    </div>
  );
}
  • 初始状态 todos 是一个数组。
  • addTodo 函数中,通过 [...prevTodos, '复习 useState'] 添加新任务,保持了之前的任务列表。
  • 使用 map 方法渲染每个任务。

惰性初始化

如果初始状态计算开销较大,可以使用惰性初始化,即将一个函数传给 useState,仅在组件首次渲染时调用该函数来计算初始值。

function ExpensiveComponent() {
  const [value, setValue] = useState(() => {
    console.log('计算初始值...');
    return expensiveCalculation();
  });
 
  return <div>值:{value}</div>;
}
 
function expensiveCalculation() {
  // 模拟耗时操作
  return 42;
}
  • useState(() => expensiveCalculation()) 只会在首次渲染时调用 expensiveCalculation 计算初始值。
  • 避免了每次渲染都重新计算初始状态,提高了性能。

useState 的常见陷阱

状态更新是异步的 useState 的状态更新并不会立即生效,而是会在下一次渲染时生效。因此,如果需要依赖当前状态的值来进行更新,建议使用函数形式的 setState

function Counter() {
  const [count, setCount] = useState(0);
 
  const handleClick = () => {
    setCount(count + 1);
    setCount(count + 1);
    setCount(count + 1);
  };
 
  return (
    <div>
      <p>当前计数:{count}</p>
      <button onClick={handleClick}>增加</button>
    </div>
  );
}
  • 问题: 点击按钮时,count 只会增加 1,而不是预期的 3。

  • 原因: setCount 是异步的,每次调用 setCount 时,count 的值都还是之前的 0。因此,最终只会增加一次。

2.useEffect

useEffect 是 React 中用于管理副作用的 Hook。它允许在函数组件中执行副作用操作,如数据获取、订阅事件、计时器、手动修改 DOM 等。

基本语法

useEffect(() => {
  // 副作用逻辑
  return () => {
    // 清理副作用(可选)
  };
}, [依赖数组]);
  • 第一个参数:一个函数,包含要执行的副作用逻辑。
  • 返回值(可选):返回一个函数,用于清理副作用(如取消订阅、清除计时器等),在组件卸载之前或者在下一次效果触发之前执行。
  • 依赖数组:控制 useEffect 的执行时机。如果数组为空 [],则只在组件挂载时(首次渲染后)运行。

在组件挂载时运行副作用

import React, { useEffect } from 'react';
 
function Fun() {
  useEffect(() => {
    console.log('组件已挂载');
  }, []); // 空依赖数组,表示只在组件挂载后运行
 
  return <div>Hello, useEffect!</div>;
}
 
export default Example;
  • useEffect 中的回调函数只会在组件首次挂载时执行一次。

依赖数组控制副作用

import React, { useState, useEffect } from 'react';
 
function Counter() {
  const [count, setCount] = useState(0);
 
  useEffect(() => {
    console.log(`计数器更新为:${count}`);
  }, [count]); // 依赖于 count,当 count 发生变化时执行
 
  return (
    <div>
      <p>当前计数:{count}</p>
      <button onClick={() => setCount(count + 1)}>增加</button>
    </div>
  );
}
  • 依赖数组 [count] 表示只有 count 发生变化时,useEffect 才会执行。
  • 每次点击按钮时,useEffect 会输出当前的计数值。

清理副作用

import React, { useState, useEffect } from 'react';
 
function Timer() {
  const [count, setCount] = useState(0);
 
  useEffect(() => {
    const interval = setInterval(() => {
      setCount(prevCount => prevCount + 1);
    }, 1000);
 
    // 返回清理函数,组件卸载时和下一次执行回调时清除定时器
    return () => clearInterval(interval);
  }, []); // 空依赖数组,表示只在挂载时设置定时器
 
  return <p>计时器:{count}</p>;
}
 
export default Timer;
  • useEffect 的返回值中,clearInterval 用于清理定时器,防止内存泄漏。
  • 这种模式常用于订阅事件设置计时器等需要清理的副作用。

模拟数据获取

import React, { useState, useEffect } from 'react';
 
function DataFetcher() {
  const [data, setData] = useState(null);
 
  useEffect(() => {
    async function fetchData() {
      const response = await fetch('https://xxxxx.com/data');
      const result = await response.json();
      setData(result);
    }
 
    fetchData();
  }, []); // 空依赖数组,表示只在组件挂载时运行
 
  return <div>{data ? JSON.stringify(data) : '加载中...'}</div>;
}
 
export default DataFetcher;
  • useEffect 中的异步函数 fetchData 负责获取数据并更新状态。
  • 由于依赖数组为空,fetchData 只会在组件挂载时调用一次。

依赖数组为空和省略依赖数组的区别

useEffect(() => {
  console.log('每次渲染都会执行');
});
  • 空依赖数组 []useEffect 只会在组件初次渲染时运行。
  • 省略依赖数组:useEffect 会在每次组件渲染时运行,类似于类组件中的 componentDidMountcomponentDidUpdate

3.useRef

useRef 是 React 提供的一个 Hook

使用场景
  • 访问 DOM 元素
  • 在组件生命周期内存储可变值(不触发重新渲染)

基本语法

const refContainer = useRef(initialValue);
  • initialValueref 的初始值,可以是 null 或任意值。
  • 返回值是一个包含 .current 属性的对象,refContainer.current 可以存储值或 DOM 节点。

访问 DOM 元素

useRef 常用于获取和操作 DOM 元素,类似于类组件中的 React.createRef

示例:聚焦输入框
import React, { useRef } from 'react';
 
function InputFocus() {
  const inputRef = useRef(null);
 
  const handleFocus = () => {
    inputRef.current.focus(); // 通过 ref 获取 DOM 元素并调用 focus 方法
  };
 
  return (
    <div>
      <input ref={inputRef} type="text" placeholder="点击按钮聚焦" />
      <button onClick={handleFocus}>聚焦输入框</button>
    </div>
  );
}
 
export default InputFocus;
  • inputRef 使用 useRef 创建,并通过 ref 属性绑定到 <input> 元素。
  • 点击按钮时,通过 inputRef.current 获取 <input> 元素并调用其 focus 方法。

存储可变值

useRef 可以存储在组件生命周期内需要保持的值,但更新这个值不会触发组件重新渲染。

示例:保存前一次渲染的值
import React, { useState, useEffect, useRef } from 'react';
 
function PreviousValue() {
  const [count, setCount] = useState(0);
  const prevCountRef = useRef();
 
  useEffect(() => {
    prevCountRef.current = count; // 保存当前 count 值到 ref
  });
 
  return (
    <div>
      <p>当前值:{count}</p>
      <p>前一次值:{prevCountRef.current}</p>
      <button onClick={() => setCount(count + 1)}>增加</button>
    </div>
  );
}
 
export default PreviousValue;
  • 每次 count 更新时,通过 useEffect 将当前 count 存入 prevCountRef.current
  • 在渲染时显示前一次的 count 值。

避免闭包陷阱

import React, { useState, useRef, useEffect } from 'react';
 
function Timer() {
  const [count, setCount] = useState(0);
  const timerRef = useRef(null);
 
  const startTimer = () => {
    timerRef.current = setInterval(() => {
      setCount(prevCount => prevCount + 1);
    }, 1000);
  };
 
  const stopTimer = () => {
    clearInterval(timerRef.current);
  };
 
  useEffect(() => {
    return () => clearInterval(timerRef.current); // 组件卸载时清理定时器
  }, []);
 
  return (
    <div>
      <p>计时器:{count}</p>
      <button onClick={startTimer}>开始</button>
      <button onClick={stopTimer}>停止</button>
    </div>
  );
}
 
export default Timer;
  • timerRef 存储了定时器的 ID,方便在停止计时或组件卸载时清除定时器。
  • 通过 useRef 避免了闭包陷阱,因为 timerRef.current 始终指向最新的定时器 ID。

4.useContext

useContext 是 React 的一个 Hook,用于在函数组件中订阅 React 上下文(Context)。它可以帮助组件直接访问共享的状态或数据,而不需要通过多层级的 props 传递。

使用场景
  • 在组件树中共享状态,例如:主题、用户信息、语言设置等。
  • 通过上下文避免繁琐的 props drilling(逐层传递 props)。

使用示例(主题切换)

创建Context
import React, { createContext, useState, useContext } from 'react';
 
// 创建一个 ThemeContext,初始值为 'light'
const ThemeContext = createContext('light');
 
function App() {
  const [theme, setTheme] = useState('light');
 
  const toggleTheme = () => {
    setTheme(theme === 'light' ? 'dark' : 'light');
  };
 
  return (
    <ThemeContext.Provider value={theme}>
      <Page />
      <button onClick={toggleTheme}>切换主题</button>
    </ThemeContext.Provider>
  );
}
使用 useContext 访问 Context
function Page() {
  const theme = useContext(ThemeContext); // 订阅 ThemeContext
 
  return (
    <div style={{ backgroundColor: theme === 'light' ? '#fff' : '#333', color: theme === 'light' ? '#000' : '#fff' }}>
      当前主题:{theme}
    </div>
  );
}

参考资料

Footnotes

  1. 阮一峰. React Hooks入门教程