之前写代码总是把逻辑和 UI 混在一起,写出来的组件又长又臭,自己都不想看。今天我尝试了"自定义 Hook",感觉像打开了新世界的大门!
如果你也像我一样刚开始学 React,希望这篇笔记能帮到你。
1. 从一个简单的鼠标追踪开始
最开始,我想做一个简单的功能:在屏幕上显示鼠标的坐标。
按照以前的习惯,直接在组件里写了 useState 和 useEffect。
这是我一开始写在组件里的代码(虽然能跑,但是逻辑都堆在组件里):
function MouseMove() {
const [x, setX] = useState(0);
const [y, setY] = useState(0);
useEffect(() => {
const updata = (event) => {
console.log('/////////////////////');
setX(event.pageX);
setY(event.pageY);
}
window.addEventListener('mousemove',updata);
console.log('组件挂载时执行');
return () => {
console.log('组件卸载时执行');
window.removeEventListener('mousemove',updata); // 组件卸载时,消除事件监听,没有则内存泄漏
}
}, [])
return (
<>
<p>鼠标位置:{x}, {y}</p>
</>
)
}
2. 第一次尝试封装:useMouse
React 的核心思想之一就是"复用"。这种监听鼠标位置的逻辑,如果别的组件也要用,复制粘贴可行但在实际开发中不可取!
于是,我尝试把它抽离成一个自定义 Hook。文件放在 src/hooks/useMouse.js:
import { useState , useEffect } from 'react';
// 封装响应式mouse业务
// UI组件更简单 HTML+CSS,好维护
// 复用
export const useMouse = () => {
const [x, setX] = useState(0);
const [y, setY] = useState(0);
useEffect(() => {
const updata = (event) => {
console.log('/////////////////////');
setX(event.pageX);
setY(event.pageY);
}
window.addEventListener('mousemove',updata);
console.log('组件挂载时执行');
return () => {
console.log('组件卸载时执行');
window.removeEventListener('mousemove',updata); // 组件卸载时,消除事件监听,没有则内存泄漏
}
}, [])
// 把要向外部暴露的状态和方法返回
return {
x,
y
}
}
现在组件瞬间变干净了
// 现在的 MouseMove 组件
function MouseMove() {
// 引入自定义hook,一行代码搞定逻辑!
const {x,y} = useMouse();
return (
<>
<p>鼠标位置:{x}, {y}</p>
</>
)
}
3. 进阶挑战:做一个 TodoList
只有鼠标追踪太简单了,拿最经典的"待办事项列表"来练手。这次我决定彻底贯彻"UI 和逻辑分离"的思想。
第一步:先把 UI 组件写好
1. 输入框组件 (src/components/TodoInput.jsx)
负责接收用户输入,用户按回车或者提交时,把数据传出去。
import { useState } from 'react';
export default function TodoInput({onAddTodo}) {
const [text, setText] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (!text.trim()) {
return;
}
onAddTodo(text.trim());
setText('');
}
return (
<form className ="todo-input" onSubmit={handleSubmit} >
<input
type="text"
value={text}
onChange={e => setText(e.target.value)}
/>
</form>
)
}
2. 单个列表项组件 (src/components/TodoItem.jsx)
负责显示一条具体的任务,可以勾选完成,也可以删除。
export default function TodoItem({todo,onToggle,onDelete}) {
// console.log(todo,'//////');
return (
<li className='todo-item'>
<input
type='checkbox'
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span className={todo.completed ? 'completed' : ''}>
{todo.text}
</span>
<button onClick={() => onDelete(todo.id)}>删除</button>
</li>
)
}
3. 列表容器组件 (src/components/TodoList.jsx)
它只负责遍历数据,渲染一个个 TodoItem。
import TodoItem from './TodoItem.jsx';
export default function TodoList({todos,onToggle,onDelete}) {
// console.log(todos,'//////');
return (
<ul className='todo-list'>
{
todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={onToggle}
onDelete={onDelete}
/>
))
}
</ul>
)
}
第二步:编写大脑 useTodos
UI 写好了,现在最重要的问题来了:数据存在哪?增删改查的逻辑写在哪?
如果写在 App.jsx 里,那个文件很快就会爆掉。所以,要把这些逻辑封装进 src/hooks/useTodos.js。
这里还加了个功能:把数据自动保存到 localStorage,这样刷新页面数据还在!
// 封装响应式todos业务
import { useState , useEffect } from 'react';
const STORAGE_KEY = 'todos'; // 好维护
function loadFromStorage() {
const todos = localStorage.getItem(STORAGE_KEY);
return todos ? JSON.parse(todos) : [];
}
// 保存todos到localStorage
function saveToStorage(todos) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
}
export const useTodos = () => {
// useState 接收函数 计算 同步
const [todos, setTodos] = useState(loadFromStorage);
// 监听todos变化 保存到localStorage
useEffect(() => {
saveToStorage(todos);
}, [todos]);
// 添加todo
const addTodo = (text) => {
setTodos([...todos, {id: Date.now(), text, completed: false}]);
}
// 切换todo完成状态
const toggleTodo = (id) => {
setTodos(
todos.map(todo => {
if(todo.id === id) {
return {
...todo,
completed: !todo.completed
}
}
return todo;
})
)
}
// 删除todo
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
}
return {
todos,
addTodo,
toggleTodo,
deleteTodo,
}
}
4. 最后的组装:App.jsx
最后,在 src/App.jsx 里,我只需要像搭积木一样,把 UI 组件摆好,然后用 useTodos 把逻辑"注入"进去。
看看现在的 App.jsx 有多清爽!
import { useState , useEffect } from 'react';
import { useMouse } from './hooks/useMouse.js';
import { useTodos } from './hooks/useTodos.js';
import TodoList from './components/TodoList.jsx';
import TodoInput from './components/TodoInput.jsx';
function MouseMove() {
// 省略上面演示过的代码...
// 引入自定义hook
const {x,y} = useMouse();
return (
<>
<p>鼠标位置:{x}, {y}</p>
</>
)
}
export default function App() {
const [count, setCount] = useState(0);
// 这里直接使用我们封装好的 hook
const {todos, addTodo,toggleTodo,deleteTodo} = useTodos();
return (
<>
<TodoInput onAddTodo={addTodo} />
{
todos.length > 0 ?( <TodoList
todos={todos}
onToggle={toggleTodo}
onDelete={deleteTodo} />) : (
<div>暂无待办事项</div>
)
}
{/*
<button onClick={() => setCount((count) => count + 1)}>
点击增加
</button>
{count % 2 === 0 && <MouseMove />} */}
</>
)
}