🎣 拒绝面条代码!手把手带你用自定义 Hooks 重构 React 世界

💡 写在前面 :你是否还在为 React 组件里那一堆乱糟糟的 useStateuseEffect 感到头秃?是否觉得业务逻辑和 UI 代码像缠在一起的耳机线一样难解难分?

别慌,今天咱们不聊虚的。我们要化身"代码外科医生",拿起 Custom Hooks(自定义 Hooks) 这把手术刀,把业务逻辑从组件里漂亮地剥离出来。

本文将通过两个实战案例------"鼠标追踪器""硬核 TodoList",带你从零开始领悟 Hooks 的设计哲学。准备好了吗?发车!🚗💨


🧐 第一章:Hooks 到底是个啥?

在 React 16.8 之前,函数组件就是个"花瓶",只负责渲染 UI,没有状态(State),也没有生命周期。如果你想搞点复杂的逻辑,就得写那个笨重的 Class 组件,this 指针指来指去,指到你怀疑人生。

Hooks 的出现,就是为了给函数组件注入灵魂。

它是一种函数式编程思想的体现。简单来说,Hooks 就是一堆以 use 开头的魔法函数,它们让函数组件也能拥有状态管理和生命周期处理的能力。

常用"双子星"

在我们开始自定义 Hooks 之前,必须先复习一下两个最基础的 Hooks,因为自定义 Hooks 本质上就是对它们的封装复用

  1. useState状态的容器

    • 它让函数组件有了"记忆"。
    • const [state, setState] = useState(initialValue);
    • 记住:React 的状态更新是**不可变(Immutable)**的,不要直接修改 state,要用 setState 传入新值。
  2. useEffect副作用的管家

    • 什么是副作用?数据获取、订阅事件、修改 DOM... 凡是跟渲染 UI 没直接关系的事儿,都叫副作用。
    • 它相当于 Class 组件里的 componentDidMountcomponentDidUpdatecomponentWillUnmount 的合体。
    • useEffect(() => { ... return () => cleanup }, [dependencies])

好,基础复习完毕。现在我们要搞点高级的------自定义 Hooks

🌟 核心概念:自定义 Hooks 就是一个普通的 JavaScript 函数,但它遵循两个规则:

  1. 名字必须以 use 开头(这是给 React 插件看的,也是给队友看的)。
  2. 它可以调用其他的 Hooks(这是它强大的根本原因)。

🐭 第二章:初试牛刀------打造"如影随形"的鼠标追踪器

想象一下,你接到了一个需求:在页面的任何地方,都要实时显示当前鼠标的坐标。

如果你把逻辑直接写在组件里,你的组件很快就会变得臃肿。如果好几个组件都需要这个功能呢?难道要复制粘贴代码?No! DRY (Don't Repeat Yourself)!

我们要封装一个 Hook。

2.1 顺藤摸瓜:从 App.jsx 看起

先来看看我们在 App.jsx 里是想怎么使用它的:

javascript 复制代码
// 引入我们即将编写的神器
import { useMouse } from './hooks/useMouse.js';

// ... 其他引入

function MouseMove() {
 
  const { X, Y } = useMouse();
  
  return (
    <>
      <div>
        鼠标位置:{X}, {Y}
      </div>
    </>
  )
}

😲 看!这里多么清爽!

我们不需要关心鼠标怎么监听,不需要关心事件怎么销毁

我们只管"拿"数据,这就是"声明式"编程的美妙

组件变得极其纯粹,它只负责渲染 。所有的脏活累活,都扔给了 useMouse

2.2 核心解密:useMouse.js

接下来,我们潜入 useMouse.js,看看这个 Hook 内部到底长什么样。


🎯 封装响应式的 mouse 业务

  • 为什么要封装?
  • 因为 UI 组件应该更简单,只负责 HTML + CSS。 逻辑复用,是前端团队的核心资产!
javascript 复制代码
import {
    useState,
    useEffect
} from 'react';


export const useMouse = () => {

1️⃣ 定义状态:我们需要记录 X 和 Y 坐标

scss 复制代码
    const [X, setX] = useState(0);
    const [Y, setY] = useState(0);

2️⃣ 定义事件处理函数

  • 这个函数会在每次鼠标移动时被调用
scss 复制代码
   useEffect(() => {
     
     const update = (event) => {
       // 更新状态,这将触发使用了该 Hook 的组件重新渲染
       setX(event.clientX);
       setY(event.clientY);
     }

3️⃣ 绑定事件监听

javascript 复制代码
     
      // 相当于 componentDidMount
      window.addEventListener('mousemove', update);

4️⃣ ⚠️ 极其重要:清理副作用!

  • 如果不写这个 return 函数,当组件卸载时,事件监听器依然存在。
  • 这会导致严重的【内存泄漏】,控制台会疯狂报错,浏览器会变卡。
javascript 复制代码
      // 相当于 componentWillUnmount
      return () => {
        window.removeEventListener('mousemove', update);
      }
    }, []); // 👈 注意这个空数组
    // 依赖项为空数组 [],意味着这个 effect 只在组件挂载时执行一次,
    // 并且在组件卸载时执行清理函数。

5️⃣ 返回数据

kotlin 复制代码
    // 把组件需要的状态暴露出去
    return {
        X,
        Y
    }
}

🔍 深度解析:

  1. 状态驱动 :我们要追踪鼠标,本质上就是追踪 xy 两个数字的变化。所以用了两个 useState
  2. 副作用管理 :监听 windowmousemove 事件是一个典型的副作用。
  3. 依赖项陷阱
    • 如果 useEffect 的第二个参数不传,它会在每次渲染后都执行。如果你在这里绑定事件,那完了,你会绑定几千个监听器。
    • 传入 [],告诉 React:"嘿,这事儿只在组件出生和死亡时做一次,中间别烦我。"
  4. 内存泄漏(Memory Leak)
    • 这是 React 面试必考题。
    • readme.md 中也提到了这一点:"组件卸载时需要清除事件监听/定时器,否则会导致内存泄漏"
    • React 的 useEffect 允许返回一个函数,这个函数就是专门用来擦屁股的。一定要记得 removeEventListener

📝 第三章:进阶实战------企业级 TodoList 逻辑分离

鼠标追踪只是热身,现在我们要搞点真家伙。我们要写一个 TodoList(待办事项清单)。

你可能会说:"切,TodoList 我闭着眼都能写。"

别急,这次我们不写面条代码。我们要把所有的业务逻辑 (增、删、改、查、持久化)全部抽离到一个 useTodos Hook 中。这就叫 Headless UI(无头组件) 设计思想------逻辑与视图分离。

3.1 顶层设计:App.jsx 的视角

先看 App.jsx,它是怎么组织这个应用的。

javascript 复制代码
import { useTodos } from './hooks/useTodos.js';
// ... 引入组件

export default function App() {
  // ...
scss 复制代码
  const {
    todos,       // 数据列表
    addTodo,     // 添加方法
    toggleTodo,  // 切换状态方法
    deleteTodo   // 删除方法
  } = useTodos();

✨ 魔法时刻 ✨

  • 一行代码,获取了整个 Todo 应用所需的所有数据和方法!
  • 就像去超市买了一个"Todo大礼包",回家拆开就能用。
javascript 复制代码
  return (
    <>
      {/* 把方法传给输入组件 */}
      <TodoInput addTodo={addTodo} />
      
      {/* 条件渲染:有数据才显示列表 */}
      {
        todos.length > 0 ? 
        (<TodoList 
          todos={todos} 
          toggleTodo={toggleTodo} 
          deleteTodo={deleteTodo}
        />) : 
        (<div>暂无待办事项</div>)
      }
    </>
  )
}

妙啊! App.jsx 变得极其干净。它根本不知道 Todo 是怎么存的,也不知道删除逻辑是 filter 还是 splice,它只负责传递

3.2 核心引擎:useTodos.js

这是本篇文章的重头戏。我们深入 useTodos.js 看看它是怎么运作的。

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

⭐常量提取,好维护

ini 复制代码
const STORAGE_KEY = 'todos'; 

🛠️ 辅助函数:从 LocalStorage 读取数据

  • 放在组件外面,因为它不依赖组件内的任何状态,纯函数
javascript 复制代码
function loadFromStorage() {
    const storedTodos = localStorage.getItem(STORAGE_KEY);
    return storedTodos ? JSON.parse(storedTodos) : [];
}

🛠️ 辅助函数:保存数据到 LocalStorage

javascript 复制代码
function saveToStorage(todos) {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
}

1️⃣. 惰性初始化 (Lazy Initialization)

这里的 useState 接收了一个函数 loadFromStorage,而不是直接通过 loadFromStorage() 调用。

为什么?

  • 因为 localStorage 读取是昂贵的 IO 操作。
  • 如果直接写 loadFromStorage(),每次组件渲染都会读一次 Storage。
  • 传函数引用,React 只会在组件【首次渲染】时调用它。 这是一个非常高级且实用的优化技巧!🚀
ini 复制代码
export const useTodos = () => {
    
    const [todos, setTodos] = useState(loadFromStorage);

2️⃣ 数据持久化

每当 todos 状态变化时,自动同步到 localStorage。 这样用户刷新页面,数据也不会丢。

scss 复制代码
    
    useEffect(() => {
        saveToStorage(todos);
    }, [todos]); // 依赖项是 todos

3️⃣ 业务逻辑:

多使用es6新特性如结构,map,filter

javascript 复制代码
    // 添加 Todo~~~~~~~~~
    const addTodo = (text) => {
        // ⚠️ 永远不要直接修改 state,比如 todos.push(...) 是绝对禁止的!
        // 必须创建一个新数组。
        setTodos([
            ...todos, // 展开旧数据
            {
                id: Date.now(), // 用时间戳做 ID
                text,
                completed: false
            }
        ])
    }

    //  切换完成状态~~~~~~~~~~
    const toggleTodo = (id) => {
        setTodos(
            todos.map(todo => {
                if(todo.id === id) {
                    // 同样,不要直接修改 todo.completed = !todo.completed
                    // 要返回一个新的对象
                    return {
                        ...todo,
                        completed: !todo.completed
                    }
                }
                return todo;
            })
        )
    }

    //  删除 Todo~~~~~~~~~
    const deleteTodo = (id) => {
        // filter 返回的是新数组,完美符合 React 的不可变性要求
        setTodos(
            todos.filter(todo => todo.id !== id)
        );
    }

4️⃣ 暴露接口

kotlin 复制代码
    // 返回一个对象,方便使用者解构
    return {
        todos,
        addTodo,
        toggleTodo,
        deleteTodo
    }
}

💡 知识点总结:

  • Lazy Initialization(惰性初始化)useState(() => heavyComputation())。这招在处理大数据初始化时非常有用,能显著提升性能。
  • Immutability(不可变性) :你看 addTodo 用了 [...todos]toggleTodo 用了 mapdeleteTodo 用了 filter。这些都是生成新数组的方法,而不是修改原数组。这是 React 状态更新的金科玉律。
  • 关注点分离useTodos 只管数据怎么变 ,不管数据怎么展示

3.3 组件落地:三剑客的配合

逻辑有了,现在看看 UI 组件怎么消费这些逻辑。

1. 也是"大脑"的延伸:TodoInput.jsx

TodoInput.jsx 负责收集用户输入。

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

export default function TodoInput ({addTodo}) {
    // 这个 state 是 UI 状态(输入框里的字),属于组件私有
    const [text, setText] = useState('');

    const handleSubmit = (e) => {
        e.preventDefault(); // 阻止表单默认提交刷新页面
        if(!text.trim()) return; // 空校验
        
        // 调用父组件(其实是 Hook)传来的方法
        addTodo(text.trim());
        
        // 清空输入框
        setText('');
    }

    return (
        <form className="todo-input" onSubmit={handleSubmit}>
            <input 
                type="text"
                value={text} // 受控组件:value 绑定 state
                onChange={e => setText(e.target.value)} // onChange 更新 state
            />
            <button type="submit">添加</button>
        </form>
    )
}

这里体现了 React 的**受控组件(Controlled Components)**思想:Input 的值由 React 的 State 掌控,而不是 DOM 自身。

2. 中转站:TodoList.jsx

TodoList.jsx 其实是个"傻瓜组件"(Dumb Component),它只负责遍历。

javascript 复制代码
import TodoItem from './TodoItem';

export default function TodoList({
    todos,
    toggleTodo,
    deleteTodo
}) {
    return (
        <ul className="todo-list">
            {
                todos.map(todo => (
                    // 🔑 key 属性至关重要!
                    // 它帮助 React 识别哪些元素改变了、添加了或删除了。
                    // 这里的 key={todo.id} 是最佳实践,千万别用 key={index}!
                    <TodoItem
                        key={todo.id}
                        todo={todo}
                        toggleTodo={toggleTodo}
                        deleteTodo={deleteTodo}
                    />
                ))
            }
        </ul>
    )
}

3. 最终呈现:TodoItem.jsx

TodoItem.jsx 负责渲染单条数据。

javascript 复制代码
export default function TodoItem({
    todo,
    toggleTodo,
    deleteTodo
}) {
    return (
        <li className="todo-item">
            {/* 复选框:控制完成状态 */}
            <input 
                type="checkbox" 
                checked={todo.completed} 
                onChange={() => toggleTodo(todo.id)} 
            />
            
            {/* 动态类名:控制样式 */}
            <span className={todo.completed ? 'completed' : ''}>
                {todo.text}
            </span>
            
            {/* 删除按钮 */}
            <button onClick={() => deleteTodo(todo.id)}>删除</button>
        </li>
    )
}

🎩 总结:自定义 Hooks 的"心法"

通过这两个例子,我们其实只做了一件事:把逻辑从 View 层剥离

为什么要这么做?

  1. 复用性(Reusability) :如果明天老板让你在侧边栏也做一个 Todo 列表,你只需要在侧边栏组件里 const { ... } = useTodos(),一秒搞定。
  2. 可测试性(Testability) :测试 useTodos 里的纯逻辑,比测试一个混合了 DOM 操作的组件要简单得多。
  3. 清晰度(Readability):你的组件代码量减少了,逻辑更清晰了,不管是自己看还是同事看,都更舒服。

Hooks 是一种心智模型。当你看到一段复杂的逻辑时,下意识地想:"能不能把它抽成一个 Hook?" 恭喜你,你已经从 React 萌新进阶了!


希望这篇文章能帮你打通 Hooks 的任督二脉!我们下期见!👋

相关推荐
xiaoxue..2 小时前
高频事件的“冷静剂” 闭包的实用场景:防抖与节流
前端·javascript·面试·html·编程思想
.try-2 小时前
cssTab卡片式
java·前端·javascript
怕浪猫3 小时前
2026最新React技术栈梳理,全栈必备
前端·javascript·面试
Bigger3 小时前
Tauri (24)——窗口在隐藏期间自动收起导致了位置漂移
前端·react.js·app
雲墨款哥3 小时前
从一行好奇的代码说起:Vue怎么没有React的props.children
前端·vue.js·react.js
用户8168694747253 小时前
Effect 执行时机与事件循环交错关系
前端·react.js
triumph_passion4 小时前
React Hook Form 状态下沉最佳实践
前端·react.js
CDwenhuohuo4 小时前
uniapp去掉手机状态栏 全屏展示
开发语言·javascript·uni-app
千寻girling4 小时前
面试官: “ 说一下怎么做到前端图片尺寸的响应式适配 ”
前端·javascript·面试