还记得React早期的黑暗时代吗?函数组件就像是被削去了超能力的 superhero------只能展示UI,不能拥有状态,也不能处理副作用。直到Hooks横空出世,函数组件才终于"支棱"起来了!今天咱们就来聊聊这些让React开发变得如丝般顺滑的"钩子函数"。
一、单页应用:前端开发的"变形金刚"
在聊Hooks之前,咱们先复习一下单页应用(SPA)的概念。单页应用就像是一个变形金刚,整个项目只有唯一的一个HTML文件,所有的页面都做成组件的样子,就像是变形金刚的各个零件,需要的时候就组装到这个HTML文件中进行渲染。
这种方式的好处是显而易见的:页面切换不再需要重新加载整个页面,用户体验就像是在使用原生App一样流畅。而React,就是构建这种变形金刚的绝佳工具。
二、组件的两种形态:Class派 vs Function派
在React的世界里,组件主要有两种形态:
- Class组件:就像是传统的武林门派,讲究门派规矩,有自己的生命周期和状态管理方式。
jsx
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
render() {
return (
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Count: {this.state.count}
</button>
);
}
}
- Function组件:就像是现代的自由武者,简洁灵活,但在Hooks出现之前,只能做一些简单的展示工作。
jsx
function Greeting(props) {
return <h1>Hello, {props.name}!</h1>;
}
三、Hooks:函数组件的"超能力血清"
Hooks的出现,就像是给函数组件注入了超能力血清,让它们拥有了Class组件的所有能力,甚至更加强大!下面咱们就来认识一下这些神奇的"钩子"。
1. useState:状态管理的"遥控器"
useState
是最基础的Hook,它就像是一个遥控器,可以让你在函数组件中定义和修改状态。
jsx
import { useState } from 'react';
function Counter() {
// 定义一个名为count的状态,初始值为0,setCount是修改这个状态的方法
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
是不是比Class组件的this.setState
简洁多了?而且你可以定义多个state,每个state都有自己独立的修改方法,再也不用担心状态合并的问题了!
2. useEffect:副作用处理的"魔法盒子"
useEffect
是处理副作用的Hook,它就像是一个魔法盒子,可以让你在组件渲染后执行一些操作,比如数据获取、订阅、手动修改DOM等。
useEffect
有几个非常重要的特性,咱们来一一拆解:
jsx
import { useEffect } from 'react';
// 特性1:组件每次加载(挂载)就会触发
useEffect(() => {
console.log('组件挂载了!');
});
// 特性2:第二个参数为空数组时,只会在初次渲染(挂载)时触发
useEffect(() => {
console.log('只在初次渲染时执行一次!');
}, []);
// 特性3:第二个参数数组中传入变量时,该变量每次修改值都会触发
useEffect(() => {
console.log('count值变了:', count);
}, [count]);
// 特性4:返回一个清理函数,会在组件卸载时触发
useEffect(() => {
const subscription = props.source.subscribe();
return () => {
// 清理订阅
subscription.unsubscribe();
};
}, [props.source]);
useEffect
就像是组件的"生命周期管理器",用一个Hook就替代了Class组件中的componentDidMount
、componentDidUpdate
和componentWillUnmount
三个生命周期方法,是不是很强大?
3. useLayoutEffect:同步执行的"急先锋"
useLayoutEffect
和useEffect
很像,但它的effect函数是作为同步函数来执行的,会在浏览器绘制之前执行。这在某些需要立即看到效果的场景下非常有用,比如测量DOM元素的尺寸或位置。
jsx
import { useLayoutEffect, useRef } from 'react';
function MeasureExample() {
const ref = useRef(null);
useLayoutEffect(() => {
// 在DOM更新后,浏览器绘制前执行
if (ref.current) {
const rect = ref.current.getBoundingClientRect();
console.log('元素尺寸:', rect.width, rect.height);
}
});
return <div ref={ref}>测量我吧!</div>;
}
4. useReducer:复杂状态管理的"调度中心"
当修改state的逻辑比较复杂时,useReducer
就像是一个调度中心,可以帮你更好地管理状态。
jsx
import { useReducer } from 'react';
// 定义reducer函数
function counterReducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return { count: 0 };
default:
throw new Error();
}
}
function Counter() {
// 初始化reducer,第二个参数是初始状态
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
return (
<div>
Count: {state.count}
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
</div>
);
}
这里有个小技巧:配合immer
库使用useReducer
,可以让你直接"修改"state,而不需要返回新对象,大大简化代码!
5. useRef:DOM操作的"魔法手指"
useRef
就像是一根魔法手指,可以让你轻松获取DOM元素,或者保存任何可变值。
jsx
import { useRef } from 'react';
function TextInputWithFocusButton() {
// 创建一个ref
const inputRef = useRef(null);
// 点击按钮时,让输入框获得焦点
const focusInput = () => {
inputRef.current.focus();
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>聚焦输入框</button>
</div>
);
}
6. useContext:跨组件通信的"心灵传输器"
useContext
就像是一个心灵传输器,可以让你跨多层组件传递数据,再也不用一层一层地props drilling了!
jsx
import { createContext, useContext } from 'react';
// 创建一个Context
const ThemeContext = createContext('light');
// 父组件提供Context值
function App() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
// 中间组件不需要传递props
function Toolbar() {
return <ThemedButton />;
}
// 子组件直接使用Context值
function ThemedButton() {
const theme = useContext(ThemeContext);
return <button theme={theme}>我有主题啦!</button>;
}
四、实战演练:用Hooks打造一个简单的Todo应用
理论讲了这么多,咱们来实战一下!根据README中的todo列表,我们来实现一个简单的Todo应用:
- 启动后端服务:
bash
# 在_mock文件夹下执行
json-server db.json --watch --port 3001
- 使用Hooks实现CRUD操作:
jsx
import { useState, useEffect } from 'react';
function TodoList() {
const [todos, setTodos] = useState([]);
const [searchTerm, setSearchTerm] = useState('');
// 初次加载时获取所有数据
useEffect(() => {
fetchTodos();
}, []);
// 获取数据的函数
const fetchTodos = async () => {
const url = searchTerm
? `http://localhost:3001/data/?name=${searchTerm}`
: 'http://localhost:3001/data';
const response = await fetch(url);
const data = await response.json();
setTodos(data);
};
// 搜索功能
const handleSearch = (e) => {
setSearchTerm(e.target.value);
};
useEffect(() => {
// 当搜索词变化时,重新获取数据
if (searchTerm) {
fetchTodos();
}
}, [searchTerm]);
// 删除功能
const handleDelete = async (id) => {
await fetch(`http://localhost:3001/data/${id}`, { method: 'DELETE' });
// 删除后重新获取数据
fetchTodos();
};
return (
<div>
<input
type="text"
placeholder="搜索..."
value={searchTerm}
onChange={handleSearch}
/>
<ul>
{todos.map(todo => (
<li key={todo.id}>
{todo.name}
<button onClick={() => handleDelete(todo.id)}>删除</button>
</li>
))}
</ul>
</div>
);
}
五、写在最后:Hooks的最佳实践
Hooks虽然强大,但也有一些使用规则需要遵守:
- 只在函数组件的顶层调用Hooks,不要在循环、条件或嵌套函数中调用
- 只在React函数组件或自定义Hooks中调用Hooks
- 使用ESLint插件
eslint-plugin-react-hooks
来帮助你遵守这些规则
Hooks的出现,让React的开发体验提升了一个新台阶。它不仅让函数组件拥有了Class组件的所有能力,还让代码变得更加简洁、可复用。如果你还在使用Class组件,不妨尝试一下Hooks,相信你会爱上这种开发方式!
如果这篇文章对你有帮助,别忘了点赞收藏哦!关注我,持续分享前端开发的小技巧和大道理~