React Hook 核心指南:从实战到源码,彻底掌握状态与副作用

Hook 的出现让函数组件拥有了管理状态和副作用的能力,极大地提升了代码的可读性和复用性。今天,我想和大家分享 React 中最核心、最常用的几个 Hook:useStateuseEffectuseContextuseReduceruseCallbackuseMemouseRef。我会从基本用法讲起,结合实际例子,深入探讨它们的原理和最佳实践,并手写简化版实现,最后附上高频面试题。希望这篇博客能让你对 React Hook 有更深刻的理解。

1. useState:管理组件状态

1.1 基本概念

useState 是最基础的 Hook,用于在函数组件中添加状态。它接收一个初始状态值,返回一个包含当前状态和更新函数的数组。

javascript 复制代码
const [state, setState] = useState(initialState);
  • state:当前状态值。
  • setState:更新状态的函数。
  • initialState:初始状态,可以是任意值(原始值、对象、函数等)。

1.2 使用场景

计数器是最经典的例子:

javascript 复制代码
import React, { useState } from 'react';

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

    return (
        <div>
            <p>当前计数: {count}</p>
            <button onClick={() => setCount(count + 1)}>
                增加
            </button>
            <button onClick={() => setCount(prevCount => prevCount - 1)}>
                减少
            </button>
        </div>
    );
}

关键点:

  • setState 是异步的,多次调用会进行批处理。
  • 更新函数可以接收一个函数,该函数的参数是前一个状态值(prevState),这在需要基于前一个状态更新时非常有用。

1.3 手写实现(简化版)

javascript 复制代码
// 模拟 React 内部状态存储
let hooks = [];
let currentHookIndex = 0;

function useState(initialValue) {
    const hookIndex = currentHookIndex;

    // 如果是第一次调用,初始化状态
    if (!hooks[hookIndex]) {
        hooks[hookIndex] = initialValue;
    }

    // 返回当前状态和更新函数
    const setState = (newValue) => {
        // 如果传入的是函数,执行它
        const value = typeof newValue === 'function' 
            ? newValue(hooks[hookIndex]) 
            : newValue;
        
        hooks[hookIndex] = value;
        // 模拟触发重新渲染
        render();
    };

    return [hooks[hookIndex], setState];
}

// 模拟组件渲染
function render() {
    currentHookIndex = 0; // 重置索引
    // 重新执行组件函数
    App();
}

function App() {
    const [count, setCount] = useState(0);
    const [name, setName] = useState('张三');

    console.log('Count:', count, 'Name:', name);
}

// 初始渲染
App(); // Count: 0 Name: 张三
setCount(1); // Count: 1 Name: 张三
setName('李四'); // Count: 1 Name: 李四

注意: 这只是一个极度简化的模型,真实的 React 使用 Fiber 节点和 Hook 链表来管理状态。

1.4 注意事项

  • 不要在条件或循环中使用 Hook :React 依赖 Hook 的调用顺序来正确匹配状态。违反规则会导致 Invalid hook call 错误。
  • 状态更新是异步的setState 后立即读取状态可能得不到最新值。
  • 函数式更新 :当新状态依赖于前一个状态时,使用函数形式 setState(prev => prev + 1)

2. useEffect:处理副作用

2.1 基本概念

useEffect 用于在函数组件中执行副作用操作,如数据获取、订阅、手动 DOM 操作等。它替代了类组件中的 componentDidMountcomponentDidUpdatecomponentWillUnmount

javascript 复制代码
useEffect(() => {
    // 执行副作用
    return () => {
        // 清理副作用(可选)
    };
}, [dependencies]); // 依赖数组

2.2 使用场景

场景一:数据获取

javascript 复制代码
import React, { useState, useEffect } from 'react';

function UserProfile({ userId }) {
    const [user, setUser] = useState(null);
    const [loading, setLoading] = useState(true);

    useEffect(() => {
        // 模拟 API 请求
        const fetchUser = async () => {
            setLoading(true);
            try {
                // 假设这里调用 API
                const response = await fetch(`/api/users/${userId}`);
                const userData = await response.json();
                setUser(userData);
            } catch (error) {
                console.error('获取用户信息失败:', error);
            } finally {
                setLoading(false);
            }
        };

        fetchUser();
    }, [userId]); // 仅当 userId 变化时重新执行

    if (loading) return <div>加载中...</div>;
    if (!user) return <div>用户不存在</div>;

    return (
        <div>
            <h1>{user.name}</h1>
            <p>{user.email}</p>
        </div>
    );
}

场景二:订阅和清理

javascript 复制代码
import React, { useState, useEffect } from 'react';

function MouseTracker() {
    const [position, setPosition] = useState({ x: 0, y: 0 });

    useEffect(() => {
        const handleMouseMove = (event) => {
            setPosition({ x: event.clientX, y: event.clientY });
        };

        // 添加事件监听器
        window.addEventListener('mousemove', handleMouseMove);

        // 返回清理函数
        return () => {
            window.removeEventListener('mousemove', handleMouseMove);
        };
    }, []); // 空依赖数组,只在挂载时执行一次

    return (
        <div>
            鼠标位置: {position.x}, {position.y}
        </div>
    );
}

2.3 依赖数组详解

  • []:只在组件挂载时执行一次(类似 componentDidMount)。
  • [dep1, dep2]:当 dep1dep2 变化时执行。
  • 不传:每次组件重新渲染时都执行(不推荐,容易造成性能问题或无限循环)。

2.4 手写实现(简化版)

javascript 复制代码
let dependencies = [];
let cleanup = null;

function useEffect(callback, deps) {
    const hasChanged = !deps || 
        deps.some((dep, index) => !dependencies[index] || dep !== dependencies[index]);

    if (hasChanged) {
        // 执行清理函数(如果存在)
        if (cleanup) {
            cleanup();
        }
        // 执行副作用
        cleanup = callback();
        // 更新依赖
        dependencies = deps || [];
    }
}

// 模拟组件
function Component({ count }) {
    useEffect(() => {
        console.log('Effect 执行,count:', count);
        return () => {
            console.log('清理 Effect,count:', count);
        };
    }, [count]);

    return <div>Count: {count}</div>;
}

// 初始渲染
Component({ count: 0 }); // Effect 执行,count: 0

// 更新
Component({ count: 1 }); // 清理 Effect,count: 0 -> Effect 执行,count: 1

2.5 注意事项

  • 必须返回函数或 undefined:清理函数必须是同步的。
  • 避免无限循环 :确保依赖数组正确,避免在 useEffect 内部更新依赖项。
  • 异步操作 :不能直接在 useEffect 回调中使用 async,需要在内部创建异步函数。
javascript 复制代码
// ❌ 错误
useEffect(async () => {
    const data = await fetchData();
}, []);

// ✅ 正确
useEffect(() => {
    const fetchData = async () => {
        const data = await fetchData();
    };
    fetchData();
}, []);

3. useContext:跨组件传递数据

3.1 基本概念

useContext 用于订阅 React 的 Context。它接收一个 context 对象(React.createContext 的返回值),并返回当前 context 的值。

javascript 复制代码
const value = useContext(MyContext);

3.2 使用场景

主题切换是经典例子:

javascript 复制代码
import React, { createContext, useContext, useState } from 'react';

// 创建 Context
const ThemeContext = createContext();

// 主题提供者组件
function ThemeProvider({ children }) {
    const [theme, setTheme] = useState('light');

    const toggleTheme = () => {
        setTheme(prev => prev === 'light' ? 'dark' : 'light');
    };

    return (
        <ThemeContext.Provider value={{ theme, toggleTheme }}>
            {children}
        </ThemeContext.Provider>
    );
}

// 使用 Context 的组件
function Header() {
    const { theme, toggleTheme } = useContext(ThemeContext);

    return (
        <header style={{ background: theme === 'light' ? '#fff' : '#333', color: theme === 'light' ? '#000' : '#fff' }}>
            <h1>我的网站</h1>
            <button onClick={toggleTheme}>
                切换到 {theme === 'light' ? '暗色' : '亮色'} 主题
            </button>
        </header>
    );
}

// 根组件
function App() {
    return (
        <ThemeProvider>
            <Header />
            <MainContent />
        </ThemeProvider>
    );
}

3.3 手写实现(简化版)

javascript 复制代码
// 模拟 Context
const contextStack = [];

function createContext(defaultValue) {
    return { defaultValue };
}

function Provider({ context, value, children }) {
    contextStack.push(value);
    const result = children;
    contextStack.pop();
    return result;
}

function useContext(context) {
    return contextStack[contextStack.length - 1] || context.defaultValue;
}

// 使用
const ThemeContext = createContext('light');

function ThemeProvider({ children }) {
    const [theme] = useState('dark');
    return Provider({ context: ThemeContext, value: theme, children });
}

function ThemeDisplay() {
    const theme = useContext(ThemeContext);
    return <div>当前主题: {theme}</div>;
}

3.4 注意事项

  • useContext 会订阅 Context 的变化,当 Providervalue 变化时,所有使用该 Context 的组件都会重新渲染。
  • 避免将 Context 用于频繁变化的状态,可能导致性能问题。

4. useReducer:管理复杂状态逻辑

4.1 基本概念

useReduceruseState 的替代方案,适用于状态逻辑较复杂的情况(如包含多个子值或下一个状态依赖于前一个状态)。它接收一个 reducer 函数和初始状态,返回当前状态和 dispatch 函数。

javascript 复制代码
const [state, dispatch] = useReducer(reducer, initialState);
  • reducer(state, action) => newState,纯函数。
  • initialState:初始状态。

4.2 使用场景

管理表单或购物车状态:

javascript 复制代码
import React, { useReducer } from 'react';

// 定义 action 类型
const ADD_ITEM = 'ADD_ITEM';
const REMOVE_ITEM = 'REMOVE_ITEM';
const UPDATE_QUANTITY = 'UPDATE_QUANTITY';

// Reducer 函数
function cartReducer(state, action) {
    switch (action.type) {
        case ADD_ITEM:
            return {
                ...state,
                items: [...state.items, action.payload]
            };
        case REMOVE_ITEM:
            return {
                ...state,
                items: state.items.filter(item => item.id !== action.payload.id)
            };
        case UPDATE_QUANTITY:
            return {
                ...state,
                items: state.items.map(item =>
                    item.id === action.payload.id
                        ? { ...item, quantity: action.payload.quantity }
                        : item
                )
            };
        default:
            return state;
    }
}

function ShoppingCart() {
    const [state, dispatch] = useReducer(cartReducer, { items: [] });

    const addItem = (item) => {
        dispatch({ type: ADD_ITEM, payload: item });
    };

    const removeItem = (id) => {
        dispatch({ type: REMOVE_ITEM, payload: { id } });
    };

    return (
        <div>
            <h2>购物车</h2>
            <ul>
                {state.items.map(item => (
                    <li key={item.id}>
                        {item.name} - 数量: {item.quantity}
                        <button onClick={() => removeItem(item.id)}>删除</button>
                    </li>
                ))}
            </ul>
            <button onClick={() => addItem({ id: Date.now(), name: '商品', quantity: 1 })}>
                添加商品
            </button>
        </div>
    );
}

4.3 手写实现(简化版)

javascript 复制代码
function useReducer(reducer, initialState) {
    const [state, setState] = useState(initialState);

    const dispatch = (action) => {
        const newState = reducer(state, action);
        setState(newState);
    };

    return [state, dispatch];
}

// 使用
const [state, dispatch] = useReducer((state, action) => {
    switch (action.type) {
        case 'INCREMENT':
            return { count: state.count + 1 };
        default:
            return state;
    }
}, { count: 0 });

4.4 注意事项

  • useReducer 适合状态更新逻辑复杂或需要处理多个 action 的场景。
  • reducer 必须是纯函数。

5. useCallback:缓存函数

5.1 基本概念

useCallback 返回一个记忆化的回调函数。它接收一个内联回调函数和依赖数组,只有当依赖项变化时,才会返回新的函数。

javascript 复制代码
const memoizedCallback = useCallback(
    () => {
        doSomething(a, b);
    },
    [a, b],
);

5.2 使用场景

避免子组件不必要的重新渲染:

javascript 复制代码
import React, { useState, useCallback } from 'react';

// 子组件(使用 React.memo 优化)
const ExpensiveComponent = React.memo(({ onClick, value }) => {
    console.log('ExpensiveComponent 渲染');
    return (
        <button onClick={onClick}>
            点击我 ({value})
        </button>
    );
});

function Parent() {
    const [count, setCount] = useState(0);
    const [text, setText] = useState('');

    // 使用 useCallback 缓存函数
    const handleClick = useCallback(() => {
        setCount(prev => prev + 1);
    }, []); // 依赖数组为空,函数不会变化

    return (
        <div>
            <p>计数: {count}</p>
            <input value={text} onChange={e => setText(e.target.value)} />
            {/* 传递缓存的函数 */}
            <ExpensiveComponent onClick={handleClick} value={count} />
        </div>
    );
}

如果没有 useCallback,每次 Parent 重新渲染时,handleClick 都会是一个新的函数,导致 ExpensiveComponent 重新渲染。

5.3 手写实现(简化版)

javascript 复制代码
let memoizedCallback = null;
let deps = null;

function useCallback(callback, dependencies) {
    const hasChanged = !deps || 
        dependencies.some((dep, index) => !deps[index] || dep !== deps[index]);

    if (hasChanged) {
        memoizedCallback = callback;
        deps = dependencies;
    }

    return memoizedCallback;
}

5.4 注意事项

  • 只在需要传递给子组件且子组件使用 React.memo 时才使用 useCallback
  • 过度使用 useCallback 可能导致内存占用增加。

6. useMemo:缓存计算结果

6.1 基本概念

useMemo 返回一个记忆化的值。它接收一个计算函数和依赖数组,只有当依赖项变化时,才会重新计算。

javascript 复制代码
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

6.2 使用场景

优化昂贵的计算:

javascript 复制代码
import React, { useState, useMemo } from 'react';

function Fibonacci({ n }) {
    // 模拟昂贵的计算
    const fibonacci = (num) => {
        if (num <= 1) return num;
        return fibonacci(num - 1) + fibonacci(num - 2);
    };

    // 使用 useMemo 缓存计算结果
    const result = useMemo(() => fibonacci(n), [n]);

    return <div>斐波那契数列第 {n} 项: {result}</div>;
}

function App() {
    const [n, setN] = useState(10);
    const [input, setInput] = useState('');

    return (
        <div>
            <input value={input} onChange={e => setInput(e.target.value)} placeholder="输入文本" />
            <Fibonacci n={n} />
            <button onClick={() => setN(prev => prev + 1)}>增加 n</button>
        </div>
    );
}

如果没有 useMemo,每次 input 变化导致 App 重新渲染时,都会重新计算 fibonacci(n)

6.3 手写实现(简化版)

javascript 复制代码
let memoizedValue = null;
let deps = null;

function useMemo(factory, dependencies) {
    const hasChanged = !deps || 
        dependencies.some((dep, index) => !deps[index] || dep !== deps[index]);

    if (hasChanged) {
        memoizedValue = factory();
        deps = dependencies;
    }

    return memoizedValue;
}

6.4 注意事项

  • 只在计算确实昂贵时使用 useMemo
  • 不要为了"优化"而使用,React 的重新渲染本身并不慢。

7. useRef:获取 DOM 节点或保存可变值

7.1 基本概念

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数。ref 对象在组件的整个生命周期内保持不变。

javascript 复制代码
const refContainer = useRef(initialValue);

7.2 使用场景

场景一:访问 DOM 元素

javascript 复制代码
import React, { useRef, useEffect } from 'react';

function TextInputWithFocusButton() {
    const inputEl = useRef(null);

    const onButtonClick = () => {
        // 直接操作 DOM
        inputEl.current.focus();
    };

    return (
        <>
            <input ref={inputEl} type="text" />
            <button onClick={onButtonClick}>
                聚焦输入框
            </button>
        </>
    );
}

场景二:保存可变值(不触发重新渲染)

javascript 复制代码
import React, { useState, useEffect, useRef } from 'react';

function Timer() {
    const [count, setCount] = useState(0);
    // 使用 ref 保存计时器 ID
    const intervalRef = useRef();

    useEffect(() => {
        intervalRef.current = setInterval(() => {
            setCount(prev => prev + 1);
        }, 1000);

        return () => {
            // 清理时使用 ref
            clearInterval(intervalRef.current);
        };
    }, []);

    return <div>计数: {count}</div>;
}

7.3 手写实现(简化版)

javascript 复制代码
function useRef(initialValue) {
    const refObject = { current: initialValue };
    return refObject;
}

7.4 注意事项

  • useRef 返回的对象在组件生命周期内是同一个。
  • 修改 .current 不会触发组件重新渲染。
  • 可以用于保存任何可变值,如定时器 ID、上一次的 props 等。

8. 常见面试题

  1. useStatesetState 是同步还是异步?

    • 在 React 事件处理中是异步批处理的;在 setTimeout 或原生事件中是同步的。
  2. useEffect 的清理函数在什么时候执行?

    • 在组件卸载时,或在下一次 useEffect 执行前(如果依赖变化)。
  3. useCallbackuseMemo 的区别是什么?

    • useCallback 缓存函数,useMemo 缓存值。useCallback(fn, deps) 等价于 useMemo(() => fn, deps)
  4. 如何在 useEffect 中使用 async 函数?

    • useEffect 回调内部定义并立即调用一个 async 函数。
  5. useRefcreateRef 的区别?

    • useRef 在函数组件中使用,每次渲染返回同一个对象;createRef 每次调用都返回新对象,通常在类组件中使用。
  6. 为什么不能在条件中使用 Hook?

    • React 依赖 Hook 的调用顺序来正确匹配状态,条件使用会破坏这个顺序。
  7. useReducer 适合什么场景?

    • 状态逻辑复杂、包含多个子值、或下一个状态依赖于前一个状态时。
  8. useContext 会导致性能问题吗?

    • 如果 Providervalue 频繁变化,可能会导致所有订阅的组件重新渲染。可以使用 useMemo 缓存 value 或拆分 Context

结语

React Hook 让函数组件变得强大而灵活。掌握这些核心 Hook 的用法、原理和最佳实践,是成为一名优秀 React 开发者的关键。记住:

  • useState:管理简单状态。
  • useEffect:处理副作用,注意依赖和清理。
  • useContext:跨层级传递数据。
  • useReducer:管理复杂状态逻辑。
  • useCallback / useMemo:性能优化,按需使用。
  • useRef:访问 DOM 或保存可变值。

在实际项目中,多思考、多实践,你会发现 Hook 的组合能解决各种复杂的 UI 问题。希望这篇博客能成为你深入 React 的坚实阶梯。

相关推荐
布兰妮甜8 分钟前
CSS Houdini 与 React 19 调度器:打造极致流畅的网页体验
前端·css·react.js·houdini
FSHOW1 天前
记一次开源_大量SVG的高性能渲染
前端·react.js
萌萌哒草头将军1 天前
🔥🔥🔥 原来在字节写代码就是这么朴实无华!🔥🔥🔥
前端·javascript·react.js
托尔呢1 天前
从0到1实现react(二):函数组件、类组件和空标签(Fragment)的初次渲染流程
前端·react.js
Ratten1 天前
【taro react】 ---- 实现 RuiPaging 滚动到底部加载更多数据
react.js
艾小码1 天前
React Hooks时代:抛弃Class,拥抱函数式组件与状态管理
前端·javascript·react.js
CF14年老兵1 天前
构建闪电级i18n替代方案:我为何抛弃i18next选择原生JavaScript
前端·react.js·trae
轻语呢喃2 天前
useRef :掌握 DOM 访问与持久化状态的利器
前端·javascript·react.js
wwy_frontend2 天前
useState 的 9个常见坑与最佳实践
前端·react.js