React从基础入门到高级实战:React 核心技术 - React Hooks 进阶

React Hooks 进阶

引言

React Hooks 自 React 16.8 引入以来,彻底改变了函数组件的开发方式。它不仅让函数组件具备了类组件的状态管理和生命周期功能,还通过灵活的组合性显著提升了代码的可复用性和可维护性。对于已经掌握基础 Hooks(如 useStateuseEffect)的开发者来说,学习高级 Hooks 和自定义 Hook 是进一步提升开发效率和应用性能的关键。

本文将深入探讨 React Hooks 的进阶技术,涵盖以下内容:

  • 高级 Hooks:useReduceruseCallbackuseMemo 的用法与场景。
  • 自定义 Hook:如何封装复用逻辑并提升代码可维护性。
  • Hooks 的性能优化:避免不必要的渲染和计算。
  • 实战案例:实现一个可拖拽的待办列表。
  • 练习:编写一个 useFetch Hook 封装数据请求。

无论你是希望提升技能的开发者,还是想在项目中灵活运用 Hooks 的工程师,本文都将为你提供全面的指导和实践经验。


1. 高级 Hooks 介绍

React 内置了一些高级 Hooks,专门用于处理复杂状态逻辑和性能优化需求。以下是三个最常用的高级 Hooks 的详细讲解。

1.1 useReducer:管理复杂状态

useReducer 是一个强大的 Hook,适用于需要管理复杂状态逻辑的场景。它类似于 Redux 中的 Reducer,通过将状态更新逻辑集中在一个纯函数中,提高了代码的可读性和可预测性。

基本用法
import { useReducer } from 'react';

const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
    </div>
  );
}
  • 参数
    • reducer:一个纯函数,接收当前状态和 action,返回新状态。
    • initialState:初始状态。
  • 返回值
    • state:当前状态。
    • dispatch:触发状态更新的函数。
适用场景
  • 状态逻辑复杂,涉及多个子状态或多步骤更新。
  • 状态更新依赖于先前的状态。
  • 需要集中管理状态逻辑以便于测试和调试。
进阶用法:惰性初始化

对于需要复杂初始状态的场景,可以使用惰性初始化:

const init = (initialCount) => ({ count: initialCount });

function Counter({ initialCount }) {
  const [state, dispatch] = useReducer(reducer, initialCount, init);
  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
    </div>
  );
}
  • 第三个参数是一个初始化函数,仅在组件首次渲染时调用。

1.2 useCallback:缓存函数引用

useCallback 用于缓存函数引用,避免在每次渲染时重新创建函数,从而优化性能,尤其是当函数作为 props 传递给子组件时。

基本用法
import { useState, useCallback } from 'react';

function Parent() {
  const [count, setCount] = useState(0);
  const handleClick = useCallback(() => {
    console.log('点击');
  }, []);

  return <Child onClick={handleClick} />;
}

const Child = React.memo(({ onClick }) => {
  console.log('Child 渲染');
  return <button onClick={onClick}>点击</button>;
});
  • 参数
    • fn:需要缓存的函数。
    • deps:依赖数组,决定函数是否重新创建。
  • 返回值:记忆化的函数引用。
优势
  • 防止不必要的函数创建。
  • 结合 React.memo,避免子组件因 props 变化而重渲染。
注意事项
  • 依赖数组必须准确,否则可能导致函数引用未更新,引发 bug。
  • 不建议滥用,只有在性能瓶颈时使用。

1.3 useMemo:缓存计算结果

useMemo 用于缓存昂贵计算的结果,避免在每次渲染时重复执行。

基本用法
import { useState, useMemo } from 'react';

function ExpensiveComponent({ a, b }) {
  const result = useMemo(() => {
    console.log('计算昂贵结果');
    return a + b;
  }, [a, b]);

  return <p>Result: {result}</p>;
}
  • 参数
    • fn:计算函数,返回需要缓存的值。
    • deps:依赖数组,决定是否重新计算。
  • 返回值:记忆化的计算结果。
适用场景
  • 昂贵的计算操作(如数据过滤、排序)。
  • 避免重复创建对象或数组,减少引用变化。
useCallback 的区别
  • useMemo 缓存任意值,useCallback 专为函数设计。
  • useCallback(fn, deps) 等价于 useMemo(() => fn, deps)

2. 自定义 Hook:封装复用逻辑

自定义 Hook 是 React Hooks 的核心优势之一,允许开发者将复用逻辑封装为独立函数,供多个组件使用。自定义 Hook 必须以 use 开头,内部可以调用其他 Hook。

2.1 基本用法

以下是一个简单的 useToggle 自定义 Hook:

function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);
  const toggle = () => setValue(prev => !prev);
  return [value, toggle];
}

function ToggleButton() {
  const [isOn, toggle] = useToggle(false);
  return <button onClick={toggle}>{isOn ? '开' : '关'}</button>;
}

2.2 进阶案例:可拖拽的待办列表

我们通过一个可拖拽的待办列表案例,展示如何使用自定义 Hook 封装拖拽逻辑。

实现思路
  • 使用 useState 管理拖拽状态。
  • 创建自定义 Hook useDragAndDrop 处理拖拽事件。
  • 在组件中结合拖拽逻辑实现列表项的重新排序。
完整代码
import { useState } from 'react';

function useDragAndDrop() {
  const [dragging, setDragging] = useState(null);
  const [dropTarget, setDropTarget] = useState(null);

  const handleDragStart = (id) => setDragging(id);
  const handleDragOver = (e, id) => {
    e.preventDefault();
    setDropTarget(id);
  };
  const handleDrop = (onDrop) => {
    if (dragging !== null && dropTarget !== null) {
      onDrop(dragging, dropTarget);
    }
    setDragging(null);
    setDropTarget(null);
  };

  return { handleDragStart, handleDragOver, handleDrop };
}

function DraggableTodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: '待办 1' },
    { id: 2, text: '待办 2' },
    { id: 3, text: '待办 3' },
  ]);

  const { handleDragStart, handleDragOver, handleDrop } = useDragAndDrop();

  const onDrop = (draggedId, targetId) => {
    const newTodos = [...todos];
    const draggedIndex = newTodos.findIndex(t => t.id === draggedId);
    const targetIndex = newTodos.findIndex(t => t.id === targetId);
    const [draggedItem] = newTodos.splice(draggedIndex, 1);
    newTodos.splice(targetIndex, 0, draggedItem);
    setTodos(newTodos);
  };

  return (
    <ul className="todo-list">
      {todos.map((todo) => (
        <li
          key={todo.id}
          draggable
          onDragStart={() => handleDragStart(todo.id)}
          onDragOver={(e) => handleDragOver(e, todo.id)}
          onDrop={() => handleDrop(onDrop)}
          className="todo-item"
        >
          {todo.text}
        </li>
      ))}
    </ul>
  );
}
  • 自定义 HookuseDragAndDrop 封装了拖拽的逻辑,包括拖拽开始、拖拽经过和放下的事件处理。
  • 组件DraggableTodoList 使用该 Hook 实现待办项的拖拽排序。
添加样式

为了让拖拽效果更直观,可以添加简单的 CSS:

.todo-list {
  list-style: none;
  padding: 0;
}

.todo-item {
  padding: 10px;
  margin: 5px 0;
  background: #f0f0f0;
  cursor: move;
}

.todo-item:hover {
  background: #e0e0e0;
}

3. Hooks 的性能优化

性能优化是 React 开发中的重要环节,Hooks 提供了多种工具帮助开发者避免不必要的渲染和计算。

3.1 避免重复渲染

以下是三种常用的优化手段:

  • React.memo:记忆化组件,仅在 props 变化时渲染。
  • useCallback:缓存回调函数,保持引用稳定。
  • useMemo:缓存计算结果,避免重复计算。
示例
import { useState, useCallback } from 'react';

const Child = React.memo(({ onClick }) => {
  console.log('Child 渲染');
  return <button onClick={onClick}>点击</button>;
});

function Parent() {
  const [count, setCount] = useState(0);
  const handleClick = useCallback(() => {
    console.log('点击');
  }, []);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>1</button>
      <p>Count: {count}</p>
      <Child onClick={handleClick} />
    </div>
  );
}
  • React.memo 确保 Child 仅在 onClick 变化时渲染。
  • useCallback 保证 handleClick 的引用不会因父组件渲染而改变。

3.2 优化建议

  • 谨慎使用 useMemouseCallback:它们本身有开销,仅在性能瓶颈时使用。
  • 依赖数组准确性:遗漏或多余的依赖会导致 bug 或无效优化。
  • 代码分割:结合 React.lazySuspense 实现按需加载,减少初次渲染负担。

4. 练习:编写 useFetch Hook

让我们实现一个 useFetch 自定义 Hook,用于封装数据请求逻辑。

要求

  • 接收 URL 和请求配置参数。
  • 返回数据、加载状态和错误信息。
  • 支持手动触发请求。

实现

import { useState, useEffect, useCallback } from 'react';

function useFetch(url, options = {}) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const fetchData = useCallback(async () => {
    setLoading(true);
    try {
      const response = await fetch(url, options);
      if (!response.ok) throw new Error('网络请求失败');
      const result = await response.json();
      setData(result);
    } catch (err) {
      setError(err);
    } finally {
      setLoading(false);
    }
  }, [url, JSON.stringify(options)]);

  useEffect(() => {
    fetchData();
  }, [fetchData]);

  return { data, loading, error, refetch: fetchData };
}

function UserProfile() {
  const { data, loading, error, refetch } = useFetch('https://jsonplaceholder.typicode.com/users/1');

  if (loading) return <p>加载中...</p>;
  if (error) return <p>错误: {error.message}</p>;
  return (
    <div>
      <p>用户: {data?.name}</p>
      <button onClick={refetch}>重新获取</button>
    </div>
  );
}
  • fetchData:使用 useCallback 缓存请求函数,确保其稳定性。
  • useEffect:组件挂载时自动触发请求。
  • 返回值:包含数据、加载状态、错误信息和手动触发函数。

5. 总结与进阶建议

通过本文,你已经掌握了 React Hooks 的进阶用法,包括高级 Hooks 的应用、自定义 Hook 的创建,以及性能优化技巧。以下是总结和建议:

  • useReducer:适合复杂状态管理。
  • useCallbackuseMemo:优化性能的关键工具。
  • 自定义 Hook:提升代码复用性和可维护性。
  • 性能优化:合理使用工具,避免过度优化。

进阶建议

  • 探索 useContextuseRef 的高级用法。
  • 学习 Hooks 的测试方法,使用 react-hooks-testing-library

希望这篇教程能帮助你在 React Hooks 的道路上更进一步!如有疑问,欢迎交流。


以下是将上述内容整合为一个完整的 React 单页应用的代码,包含所有案例和练习:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>React Hooks 进阶 Demo</title>
  <script src="https://cdn.jsdelivr.net/npm/react@18/umd/react.development.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/react-dom@18/umd/react-dom.development.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/@babel/standalone/babel.min.js"></script>
  <style>
    .todo-list { list-style: none; padding: 0; }
    .todo-item { padding: 10px; margin: 5px 0; background: #f0f0f0; cursor: move; }
    .todo-item:hover { background: #e0e0e0; }
    .section { margin: 20px 0; padding: 20px; border: 1px solid #ddd; border-radius: 5px; }
    button { margin: 5px; padding: 5px 10px; }
  </style>
</head>
<body>
  <div id="root"></div>
  <script type="text/babel">
    const { useState, useReducer, useCallback, useMemo, useEffect } = React;

    // useReducer 示例
    const initialState = { count: 0 };
    function reducer(state, action) {
      switch (action.type) {
        case 'increment': return { count: state.count + 1 };
        case 'decrement': return { count: state.count - 1 };
        default: return state;
      }
    }
    function Counter() {
      const [state, dispatch] = useReducer(reducer, initialState);
      return (
        <div className="section">
          <h2>useReducer 示例</h2>
          <p>Count: {state.count}</p>
          <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
          <button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
        </div>
      );
    }

    // useCallback 示例
    const Child = React.memo(({ onClick }) => {
      console.log('Child 渲染');
      return <button onClick={onClick}>点击</button>;
    });
    function CallbackExample() {
      const [count, setCount] = useState(0);
      const handleClick = useCallback(() => console.log('点击'), []);
      return (
        <div className="section">
          <h2>useCallback 示例</h2>
          <button onClick={() => setCount(count + 1)}>1</button>
          <p>Count: {count}</p>
          <Child onClick={handleClick} />
        </div>
      );
    }

    // useMemo 示例
    function MemoExample({ a, b }) {
      const result = useMemo(() => {
        console.log('计算昂贵结果');
        return a + b;
      }, [a, b]);
      return (
        <div className="section">
          <h2>useMemo 示例</h2>
          <p>Result: {result}</p>
        </div>
      );
    }

    // 自定义 Hook:useDragAndDrop
    function useDragAndDrop() {
      const [dragging, setDragging] = useState(null);
      const [dropTarget, setDropTarget] = useState(null);
      const handleDragStart = (id) => setDragging(id);
      const handleDragOver = (e, id) => {
        e.preventDefault();
        setDropTarget(id);
      };
      const handleDrop = (onDrop) => {
        if (dragging !== null && dropTarget !== null) {
          onDrop(dragging, dropTarget);
        }
        setDragging(null);
        setDropTarget(null);
      };
      return { handleDragStart, handleDragOver, handleDrop };
    }

    // 可拖拽待办列表
    function DraggableTodoList() {
      const [todos, setTodos] = useState([
        { id: 1, text: '待办 1' },
        { id: 2, text: '待办 2' },
        { id: 3, text: '待办 3' },
      ]);
      const { handleDragStart, handleDragOver, handleDrop } = useDragAndDrop();
      const onDrop = (draggedId, targetId) => {
        const newTodos = [...todos];
        const draggedIndex = newTodos.findIndex(t => t.id === draggedId);
        const targetIndex = newTodos.findIndex(t => t.id === targetId);
        const [draggedItem] = newTodos.splice(draggedIndex, 1);
        newTodos.splice(targetIndex, 0, draggedItem);
        setTodos(newTodos);
      };
      return (
        <div className="section">
          <h2>可拖拽待办列表</h2>
          <ul className="todo-list">
            {todos.map((todo) => (
              <li
                key={todo.id}
                draggable
                onDragStart={() => handleDragStart(todo.id)}
                onDragOver={(e) => handleDragOver(e, todo.id)}
                onDrop={() => handleDrop(onDrop)}
                className="todo-item"
              >
                {todo.text}
              </li>
            ))}
          </ul>
        </div>
      );
    }

    // useFetch Hook
    function useFetch(url, options = {}) {
      const [data, setData] = useState(null);
      const [loading, setLoading] = useState(false);
      const [error, setError] = useState(null);
      const fetchData = useCallback(async () => {
        setLoading(true);
        try {
          const response = await fetch(url, options);
          if (!response.ok) throw new Error('网络请求失败');
          const result = await response.json();
          setData(result);
        } catch (err) {
          setError(err);
        } finally {
          setLoading(false);
        }
      }, [url, JSON.stringify(options)]);
      useEffect(() => {
        fetchData();
      }, [fetchData]);
      return { data, loading, error, refetch: fetchData };
    }

    // 使用 useFetch
    function UserProfile() {
      const { data, loading, error, refetch } = useFetch('https://jsonplaceholder.typicode.com/users/1');
      if (loading) return <p>加载中...</p>;
      if (error) return <p>错误: {error.message}</p>;
      return (
        <div className="section">
          <h2>useFetch 示例</h2>
          <p>用户: {data?.name}</p>
          <button onClick={refetch}>重新获取</button>
        </div>
      );
    }

    // 主应用
    function App() {
      return (
        <div>
          <h1>React Hooks 进阶 Demo</h1>
          <Counter />
          <CallbackExample />
          <MemoExample a={5} b={10} />
          <DraggableTodoList />
          <UserProfile />
        </div>
      );
    }

    ReactDOM.render(<App />, document.getElementById('root'));
  </script>
</body>
</html>

这个单页应用包含了本文所有示例,直接在浏览器中运行即可体验。祝你学习愉快!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

EndingCoder

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值