- 前言(共鸣 + 痛点)
- 核心原理(为什么要自定义 Hooks)
- 实战拆解(useTodos 怎么写)
- 高级进阶(alias、useContext 怎么优化)
- 总结(关键点)
- 彩蛋互动(拉评论)
“逻辑越来越多,组件越来越臃肿…”
如果你也在用 React 写 ToDo 应用,肯定遇到过:
- 状态管理写来写去,setState 到处飞
- 组件动不动几百行,根本找不到重点
- 重复逻辑没法复用,改个小需求全局牵一发而动全身
别怕,这一切都能用 自定义 Hooks 优雅解决!
咱今天就拿useTodos拆给你看,手把手教你:
- 怎么把逻辑单独拎出来
- 怎么保持状态持久化(localStorage)
- 怎么组合
useState+useEffect- 怎么让组件更纯粹,只管渲染
1️⃣ 自定义 Hooks 是啥?为啥一定要用?
先别急着上来就写。先搞懂自定义 Hooks 到底是啥?
一句话理解:
自定义 Hooks =use开头的函数,用来把组件里用到的状态逻辑提取出来,封装成可复用的函数。
它满足几个特点:
- ✅ 一定是
useXXX开头(不然 React 检测不到) - ✅ 内部可以用
useState、useEffect、useReducer、useContext等等内置 Hook - ✅ 可以返回值(状态)和函数(操作逻辑)
- ✅ 在组件里像普通函数一样调用
这样做的好处:
- 让组件只关注「模板渲染」
- 逻辑复用度高,换一个页面直接拿来用
- 管理复杂状态变得可控
2️⃣ 看一个最典型例子:useTodos
假如现在要写一个 Todo 应用,要支持:
- ✅ 添加 todo
- ✅ 切换完成状态
- ✅ 删除 todo
- ✅ 本地持久化(关掉页面也不丢)
看完整代码
import { useState, useEffect } from 'react';
export const useTodos = () => {
// 1️⃣ 初始化,从 localStorage 读
const [todos, setTodos] = useState(
JSON.parse(localStorage.getItem('todos')) || []
);
// 2️⃣ 监听 todos,每次变化都存到 localStorage
useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);
// 3️⃣ 新增 todo
const addTodo = (text) => {
setTodos([
...todos,
{
id: Date.now(),
text,
isComplete: false
}
]);
};
// 4️⃣ 切换 todo 状态
const onToggle = (id) => {
setTodos(
todos.map(todo =>
todo.id === id
? { ...todo, isComplete: !todo.isComplete }
: todo
)
);
};
// 5️⃣ 删除 todo
const onDelete = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
// 6️⃣ 统一暴露
return {
todos,
addTodo,
onToggle,
onDelete
};
};
这里面包含了几个重点
useState保存 todo 列表,状态和组件分离useEffect监听 todos 变化,实时同步到 localStorage- 通过返回对象,把增、删、改的操作都暴露给外部
- 组件用的时候,直接
const { todos, addTodo } = useTodos(),调用就行
3️⃣ 真实场景下,这样封装有多香?
假如现在写个 TodoList 组件
import { useTodos } from '@/hooks/useTodos';
export default function TodoList() {
const { todos, addTodo, onToggle, onDelete } = useTodos();
return (
<div>
<button onClick={() => addTodo('新待办')}>新增</button>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.isComplete}
onChange={() => onToggle(todo.id)}
/>
{todo.text}
<button onClick={() => onDelete(todo.id)}>删除</button>
</li>
))}
</ul>
</div>
);
}
✨ 组件本身就专注做一件事:渲染。
状态怎么来、怎么存、怎么切换,交给 useTodos 负责。
4️⃣ 高级场景优化:两点可别忽略!
你这段笔记里提到的两个“遗憾”,是真的常见
遗憾 1:路径山路十八弯
import { useTodos } from '../../../hooks/useTodos';
一旦层级深,就特别丑:
../../../../../../hooks/useTodos
解决方案:Vite + alias
在 vite.config.js 里加一条:
import { defineConfig } from 'vite';
import path from 'path';
export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
}
});
以后随便写:
import { useTodos } from '@/hooks/useTodos';
遗憾 2:操作跨层太麻烦
有时候 onToggle、onDelete 要传好几层 props,非常绕。
解决方案: 用 useContext 做全局状态共享
import { createContext, useContext } from 'react';
// 创建上下文
export const TodosContext = createContext(null);
// 封装一个 Provider
export const TodosProvider = ({ children }) => {
const todosHook = useTodos();
return (
<TodosContext.Provider value={todosHook}>
{children}
</TodosContext.Provider>
);
};
// 在任意子组件中:
export const useTodosContext = () => useContext(TodosContext);
以后子孙组件直接 useTodosContext(),不再层层 props 传递,简直不要太爽!
5️⃣ 写在最后:再也别把逻辑写死在组件里!
写到这儿,咱复盘下:
- 自定义 Hooks 本质就是把状态 + 副作用提出来
use开头、内部可用useState、useEffect等 Hook- 搭配
alias,告别丑路径 - 需要跨层通信,配
useContext完美联动
彩蛋互动
你平时会自己手写自定义 Hooks 吗?
有没有遇到跨组件层级很烦人的场景?
评论区告诉我,顺便点个赞支持一下,让更多人别再写“屎山组件”!