Skip to content

11React Hooks核心知识点:从使用到原理的全面解析

引言:Hooks解决了什么问题?

在React 16.8之前,函数组件无法拥有状态,也不能处理副作用(如数据请求、事件监听),复杂逻辑的复用只能通过高阶组件(HOC)或Render Props实现,导致代码嵌套过深("嵌套地狱")。

Hooks的出现彻底改变了这一现状:它允许函数组件拥有状态和副作用,同时提供了更简洁的逻辑复用方式 。如今,Hooks已成为React开发的核心,理解Hooks是掌握现代React的关键。

一、常用Hooks的使用场景与注意事项

React提供了多个内置Hooks,每个Hooks都有明确的设计目的,掌握它们的使用场景是基础。

1.1 useState:函数组件的状态管理

作用:为函数组件添加状态(state),并提供更新状态的方法。

基本用法

jsx
const [count, setCount] = useState(0);
// count:当前状态值
// setCount:更新状态的函数(参数为新状态或计算新状态的函数)

使用场景:管理组件内部的简单状态(如表单输入、开关状态、计数器等)。

注意事项

  • 状态更新是异步的setCount不会立即改变count,而是触发组件重新渲染,新状态在下次渲染中生效。
    jsx
    const handleClick = () => {
      setCount(count + 1);
      console.log(count); // 输出旧值(异步更新未生效)
    };
  • 函数式更新:当新状态依赖旧状态时,用函数形式确保获取最新状态:
    jsx
    // 正确:依赖旧状态时用函数式更新
    setCount(prevCount => prevCount + 1);
  • 初始值只执行一次useState(initialValue)initialValue仅在组件首次渲染时计算,后续渲染忽略。若初始值计算昂贵,用函数形式延迟计算:
    jsx
    // 初始值计算昂贵时,用函数形式(只执行一次)
    const [data, setData] = useState(() => computeExpensiveData());

1.2 useEffect:处理副作用

作用:在函数组件中处理副作用(如数据请求、事件监听、DOM操作等),替代类组件的生命周期方法。

基本用法

jsx
useEffect(() => {
    // 副作用逻辑(如请求数据、添加事件监听)
    const timer = setInterval(() => console.log('tick'), 1000);

    // 清理函数(可选):在组件卸载或依赖变化前执行
    return () => {
        clearInterval(timer); // 清除定时器,防止内存泄漏
    };
}, [deps]); // 依赖数组:当依赖变化时,重新执行副作用

使用场景:处理需要在渲染后执行的逻辑(如数据请求)、需要清理的资源(如定时器、事件监听)。

注意事项

  • 依赖数组控制执行时机
    • 空数组[]:仅在组件挂载后执行一次(类似componentDidMount);
    • 无依赖数组:每次渲染后都执行;
    • 含依赖[a, b]:首次渲染+ab变化时执行。
  • 清理函数的作用:防止内存泄漏(如组件卸载后定时器仍在运行、事件监听未移除)。
  • 不要遗漏依赖:若副作用中使用了组件内的变量/函数,必须加入依赖数组,否则可能捕获旧值(详见下文"闭包陷阱")。

1.3 useRef:保存跨渲染周期的值

作用:创建一个"容器",用于存储跨渲染周期不变的引用值(如DOM元素、定时器ID、不需要触发重渲染的状态)。

基本用法

jsx
// 1. 引用DOM元素
const inputRef = useRef(null);
// 渲染后:inputRef.current指向<input>元素
return <input ref={inputRef}/>;

// 2. 存储跨渲染的值(不触发重渲染)
const timerRef = useRef(null);
const startTimer = () => {
    timerRef.current = setInterval(() => {
    }, 1000); // 存储定时器ID
};

使用场景

  • 获取DOM元素(如操作输入框焦点:inputRef.current.focus());
  • 存储不需要触发重渲染的数据(如定时器ID、上一次的状态值)。

注意事项

  • ref.current的变化不会触发组件重渲染(与useState不同);
  • 不要在渲染阶段修改ref.current(可能导致不可预测的行为),应在事件处理或useEffect中修改。

1.4 useContext:简化上下文访问

作用:在函数组件中直接访问React Context的值,避免通过Consumer或层层传递props

基本用法

jsx
// 1. 创建Context
const ThemeContext = React.createContext('light');

// 2. 提供Context值(祖先组件)
const App = () => (
    <ThemeContext.Provider value="dark">
        <Child/>
    </ThemeContext.Provider>
);

// 3. 消费Context(子组件)
const Child = () => {
    const theme = useContext(ThemeContext); // 直接获取Context值
    return <div>当前主题:{theme}</div>;
};

使用场景:跨组件共享数据(如主题、用户信息、权限等),替代"props透传"。

注意事项

  • Providervalue变化时,所有使用useContext的组件都会重渲染(即使父组件用了React.memo);
  • 避免过度使用Context(可能导致不必要的重渲染),局部状态优先用useState

1.5 useReducer:复杂状态逻辑的管理

作用:通过" reducer函数"管理复杂状态(多个相关状态、复杂更新逻辑),类似Redux的简化版。

基本用法

jsx
// 1. 定义reducer(纯函数:接收state和action,返回新state)
const todoReducer = (state, action) => {
    switch (action.type) {
        case 'ADD_TODO':
            return [...state, {id: Date.now(), text: action.text, done: false}];
        case 'TOGGLE_TODO':
            return state.map(todo =>
                todo.id === action.id ? {...todo, done: !todo.done} : todo
            );
        default:
            return state;
    }
};

// 2. 使用useReducer
const [todos, dispatch] = useReducer(todoReducer, []);
// todos:当前状态
// dispatch:发送action的函数(用于触发状态更新)

// 3. 触发状态更新
const addTodo = (text) => {
    dispatch({type: 'ADD_TODO', text}); // 发送action
};

使用场景

  • 状态逻辑复杂(如多个子值组成的状态,如{ users: [], loading: false, error: null });
  • 状态更新依赖前一个状态(避免useState的函数式更新嵌套);
  • 需要通过Context共享状态更新逻辑(比传递多个setState函数更简洁)。

注意事项

  • reducer必须是纯函数(不修改入参state,无副作用);
  • dispatch函数的引用在组件生命周期内保持不变(可安全地加入useEffect依赖)。

二、useEffect深度解析:副作用与生命周期

useEffect是最常用也最容易出错的Hooks,理解其执行机制是掌握Hooks的关键。

2.1 依赖项:控制副作用的执行时机

useEffect的第二个参数(依赖数组)决定了副作用何时执行:

  • 无依赖数组:每次组件渲染后都执行副作用,清理函数在每次执行前触发(首次渲染后只执行副作用,无清理)。

    jsx
    useEffect(() => {
      console.log('每次渲染后执行');
      return () => console.log('下次渲染前清理');
    });
  • 空依赖数组[]:仅在组件挂载后执行一次副作用,清理函数在组件卸载时执行(模拟componentDidMountcomponentWillUnmount)。

    jsx
    useEffect(() => {
      console.log('组件挂载后执行');
      return () => console.log('组件卸载时清理');
    }, []);
  • 含依赖的数组[a, b]:首次渲染后执行,且当ab的值发生变化时重新执行(浅比较),清理函数在依赖变化前或组件卸载时执行(模拟 componentDidUpdate)。

    jsx
    useEffect(() => {
      console.log(`userId变化:${userId}`);
      return () => console.log(`userId即将更新:${userId}`);
    }, [userId]); // 仅userId变化时执行

2.2 清理函数:防止内存泄漏

清理函数是useEffect返回的函数,作用是在副作用失效前清理资源,避免内存泄漏。常见场景:

  • 事件监听:移除事件监听,防止组件卸载后仍触发回调。

    jsx
    useEffect(() => {
      const handleScroll = () => console.log('滚动了');
      window.addEventListener('scroll', handleScroll);
      return () => {
        window.removeEventListener('scroll', handleScroll); // 清理
      };
    }, []);
  • 定时器/间隔器:清除定时器,避免组件卸载后仍执行。

    jsx
    useEffect(() => {
      const timer = setInterval(() => console.log('tick'), 1000);
      return () => clearInterval(timer); // 清理
    }, []);
  • 订阅/请求取消:取消API请求(如Axios的CancelToken),避免组件卸载后收到过时响应。

2.3 模拟类组件生命周期

useEffect可通过依赖数组模拟类组件的所有生命周期:

类组件生命周期useEffect实现方式
componentDidMount(挂载后)useEffect(() => { ... }, [])
componentDidUpdate(更新后)useEffect(() => { ... }, [deps])(依赖变化时)
componentWillUnmount(卸载前)useEffect(() => { return () => { ... } }, [])(清理函数)

示例:模拟完整生命周期

jsx
const LifecycleDemo = ({userId}) => {
    // 挂载后执行(componentDidMount)
    useEffect(() => {
        console.log('组件挂载完成');
    }, []);

    // userId变化时执行(componentDidUpdate)
    useEffect(() => {
        console.log(`userId更新为:${userId}`);
    }, [userId]);

    // 卸载前清理(componentWillUnmount)
    useEffect(() => {
        return () => {
            console.log('组件即将卸载,清理资源');
        };
    }, []);

    return <div>userId: {userId}</div>;
};

2.4 闭包陷阱:useEffect中的旧值问题

useEffect的副作用函数会"捕获"当前渲染周期的状态和变量,若依赖数组设置不当,可能导致访问旧值:

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

    useEffect(() => {
        // 副作用捕获了初始的count(0)
        const timer = setInterval(() => {
            console.log('当前count:', count); // 始终输出0(闭包陷阱)
        }, 1000);
        return () => clearInterval(timer);
    }, []); // 空依赖:副作用只执行一次,捕获初始count

    return <button onClick={() => setCount(c => c + 1)}>count: {count}</button>;
};

解决方法:将依赖加入依赖数组,让副作用在依赖变化时重新执行,捕获新值:

jsx
useEffect(() => {
    const timer = setInterval(() => {
        console.log('当前count:', count); // 正确输出最新count
    }, 1000);
    return () => clearInterval(timer);
}, [count]); // 依赖count:count变化时重新执行副作用

三、自定义Hooks:逻辑复用的最佳实践

自定义Hooks是抽取和复用组件逻辑的利器,它允许你将组件中重复的逻辑(如数据请求、表单处理、订阅)封装成可复用的函数。

3.1 设计原则:什么是合格的自定义Hook?

  • 命名必须以use开头:这是React的约定,确保React能识别它是Hooks(从而检查Hook调用规则)。
  • 内部可调用其他Hooks:如useStateuseEffect等,但必须遵循Hook调用规则(不能在条件语句中调用)。
  • 目的是复用逻辑,而非复用UI:自定义Hooks返回数据或函数,不返回JSX(UI复用用组件)。
  • 独立作用域:每个组件使用自定义Hook时,内部的状态和副作用都是独立的(不会共享状态)。

3.2 实用案例:常用自定义Hooks

3.2.1 useFetch:封装数据请求逻辑

jsx
// 自定义Hook:封装API请求逻辑
const useFetch = (url) => {
    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);

    useEffect(() => {
        // 取消请求的控制器
        const abortController = new AbortController();

        const fetchData = async () => {
            try {
                setLoading(true);
                const res = await fetch(url, {signal: abortController.signal});
                if (!res.ok) throw new Error('请求失败');
                const json = await res.json();
                setData(json);
                setError(null);
            } catch (err) {
                if (err.name !== 'AbortError') { // 忽略取消请求的错误
                    setError(err.message);
                }
            } finally {
                setLoading(false);
            }
        };

        fetchData();

        // 清理函数:取消未完成的请求
        return () => abortController.abort();
    }, [url]); // url变化时重新请求

    return {data, loading, error}; // 返回请求状态
};

// 使用自定义Hook
const UserList = () => {
    const {data, loading, error} = useFetch('/api/users');

    if (loading) return <div>加载中...</div>;
    if (error) return <div>错误:{error}</div>;

    return (
        <ul>
            {data.map(user => (
                <li key={user.id}>{user.name}</li>
            ))}
        </ul>
    );
};

3.2.2 useLocalStorage:封装本地存储逻辑

jsx
// 自定义Hook:同步state与localStorage
const useLocalStorage = (key, initialValue) => {
    // 从localStorage读取初始值(首次渲染)
    const [value, setValue] = useState(() => {
        try {
            const item = localStorage.getItem(key);
            return item ? JSON.parse(item) : initialValue;
        } catch (err) {
            console.error('读取localStorage失败:', err);
            return initialValue;
        }
    });

    // 当value变化时,同步到localStorage
    useEffect(() => {
        try {
            localStorage.setItem(key, JSON.stringify(value));
        } catch (err) {
            console.error('写入localStorage失败:', err);
        }
    }, [key, value]);

    return [value, setValue]; // 类似useState的返回值
};

// 使用:持久化存储用户偏好
const UserSettings = () => {
    const [theme, setTheme] = useLocalStorage('theme', 'light');

    return (
        <div>
            <p>当前主题:{theme}</p>
            <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
                切换主题
            </button>
        </div>
    );
};

四、Hooks底层原理:为什么Hooks必须按顺序调用?

Hooks的设计依赖于**"调用顺序固定"**,理解其底层实现能帮你避免常见错误(如在条件语句中使用Hooks)。

4.1 依赖链表:Hooks的存储方式

React内部通过单向链表存储组件的Hooks状态,每个Hook对应链表中的一个节点,节点中存储状态值、更新函数、依赖等信息。

  • 首次渲染时,React按Hooks的调用顺序创建链表节点(如useStateuseEffectuseState对应三个节点);
  • 重新渲染时,React按相同顺序遍历链表,读取/更新每个节点的状态。

4.2 为什么不能在条件语句中使用Hooks?

若在条件语句中调用Hooks,会导致两次渲染的Hooks调用顺序不一致,破坏链表结构,React无法正确匹配状态:

jsx
// 错误示例:在条件中调用Hooks,导致顺序不一致
const BadExample = () => {
    const [count, setCount] = useState(0);

    if (count > 0) {
        // 首次渲染count=0:不执行此Hook,链表长度1
        // 后续count>0:执行此Hook,链表长度2 → 顺序混乱
        const [name, setName] = useState('');
    }

    return <button onClick={() => setCount(c => c + 1)}>count: {count}</button>;
};

错误原因:首次渲染时链表只有count一个节点,当count>0时,第二次渲染会新增name 节点,导致后续Hooks(若有)的顺序与链表节点不匹配,React抛出错误。

4.3 为什么useEffect的依赖要完整?

useEffect的依赖数组用于判断是否需要重新执行副作用,其底层通过浅比较依赖项与上一次的快照:

  • 若依赖项未变化(浅比较相等),复用上次的副作用(不执行);
  • 若依赖项变化,执行清理函数(若有),再执行新的副作用。

若遗漏依赖,React会使用旧的依赖快照,导致副作用中访问的变量可能已过时(闭包陷阱)。

五、总结:Hooks的核心价值与最佳实践

Hooks的出现彻底改变了React的开发模式,其核心价值在于:

  1. 简化状态管理:函数组件无需转为类组件即可拥有状态;
  2. 逻辑复用更优雅:自定义Hooks替代高阶组件和Render Props,避免嵌套地狱;
  3. 代码更简洁:将相关逻辑(如状态+副作用)聚合在一处,而非分散在不同生命周期。

最佳实践

  • 遵循Hooks调用规则:只在函数组件或自定义Hooks中调用,不要在条件/循环中调用;
  • useEffect依赖数组要完整,避免闭包陷阱;
  • 复杂状态用useReducer,跨组件数据用useContext
  • 提取重复逻辑为自定义Hooks,命名以use开头。

掌握Hooks不仅是记住API,更要理解其"状态与逻辑分离"的设计思想,写出更清晰、更可复用的React代码。