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 的坚实阶梯。

相关推荐
Java陈序员9 小时前
轻量运维面板!一款现代化的服务器控制面板工具!
运维·服务器·python·react.js·github
光影少年12 小时前
RN首屏加载速度优化全方案
react native·react.js·掘金·金石计划
前端精髓14 小时前
React 更新 state 中的对象
javascript·react.js·ecmascript
10share20 小时前
我为什么用 React 重写了一个 VitePress
前端·react.js
天道kabuto20 小时前
前端每日知识点:React 与 Vue Diff 算法对比 · 完整总结
vue.js·react.js·前端框架
moMo20 小时前
React 全栈实战:从组件到鉴权的完整指南
react.js
光影少年2 天前
如何实现RN 多环境、多渠道打包
前端·react native·react.js
云上工程笔记2 天前
AstraFlow + React/Vite:如何用自然语言开发并部署一个 AI 咖啡网站原型
前端·人工智能·react.js
光影少年2 天前
React18 对RN 的影响
android·前端·react.js·ios·前端框架