hooks
useState
- 函数式更新的主要好处
- 基于最新状态更新,函数式更新总是基于最新的状态值,而不是更新时的闭包值:
// 可能有问题的方式
setCount(count + 1); // 依赖当前闭包中的count值
// 更好的方式
setCount(prevCount => prevCount + 1); // 基于最新状态值
- 解决多次更新问题,当需要连续多次更新同一状态时,函数式更新能确保所有更新都生效
// 这样只会增加1,因为两次setCount使用的count值相同
setCount(count + 1);
setCount(count + 1);
// 这样会增加2,因为每次更新都基于前一次的结果
setCount(prev => prev + 1);
setCount(prev => prev + 1);
3.函数式更新配合 useEffect 可以有效地避免某些情况下的无限循环问题
import { useEffect, useState } from "react";
function App() {
const [count, setCount] = useState(0);
const [age, setAge] = useState(18);
// 以下代码会导致死循环
useEffect(() => {
setCount(count + 1);
}, [age, count]);
// 以下代码使用函数格式更新从而避免直接依赖count
useEffect(() => {
setCount((pre) => pre + 1);
}, [age]);
const onSetAge = () => {
setAge((pre) => pre + 1);
};
return (
<>
<p>count:{count}</p>
<p>age:{age}</p>
<button onClick={onSetAge}>setAge</button>
</>
);
}
export default App;
useImmer
用于以不可变的方式更轻松地更新复杂的状态对象。
因为useState 状态管理, setState()设置时引起页面重新渲染的机制是地址值发生改变。对于基本类型来说就是值改变,对于对象或数组等引用类型来说,就是前后地址值需要不一样,才会更新视图。
// 安装库
npm install immer use-immer
与 useState 对比的优势
| 场景 | useState | useImmer |
|---|---|---|
| 深层更新 | 需要多层展开操作符 | 直接修改路径 |
| 数组操作 | 需要创建新数组 | 直接使用 push/pop/splice |
| 可读性 | 代码冗长 | 代码简洁直观 |
| 维护性 | 容易出错(忘记展开) | 不易出错 |
- 传统方式使用useState更新嵌套对象:
const [state, setState] = useState({
user: {
name: "John",
address: {
city: "New York",
zip: "10001"
}
}
});
// 传统方式 - 需要多层展开
setState(prev => ({
...prev,
user: {
...prev.user,
address: {
...prev.user.address,
city: "Boston"
}
}
}));
- 使用
useImmer
import { useImmer } from "use-immer";
function UserProfile() {
const [user, updateUser] = useImmer({
name: "Alice",
age: 25,
hobbies: ["reading", "coding"],
profile: {
bio: "Developer",
social: {
twitter: "@alice",
github: "alice"
}
}
});
const updateAge = () => {
updateUser(draft => {
draft.age += 1;
});
};
const addHobby = (hobby) => {
updateUser(draft => {
draft.hobbies.push(hobby);
});
};
const updateTwitter = (handle) => {
updateUser(draft => {
draft.profile.social.twitter = handle;
});
};
}
useEffect
- 依赖数组的三种形式
// 1. 空依赖数组 - 只在挂载时运行
useEffect(() => {
// 初始化逻辑
}, []);
// 2. 无依赖数组 - 每次渲染都运行(谨慎使用)
useEffect(() => {
// 每次渲染都执行
});
// 3. 有具体依赖 - 挂载时运行 + 依赖变化时 运行
useEffect(() => {
}, [count, name]);
2.依赖项的比较机制,React 使用 Object.is() 比较依赖项的前后值,为了提高性能,尽量避免依赖项的非必要改变。譬如使用useCallback来包装函数或者把使用到函数定义在useEffect内部,再或者碰到对象类型(普通对象或数组)使用useMemo来缓存。
- 基本类型:值比较
- 引用类型:引用比较(对象、数组、函数每次都会创建新引用)
const [user, setUser] = useState({ id: 1, name: '张三', age: 25 });
// 对象依赖的问题(危险!)
useEffect(() => {
setLogs(prev => [...prev, `⚠️ 依赖user对象: ${user.name}`]);
}, [user]); // 每次渲染user对象引用都不同!
const stableUser = useMemo(() => ({ ...user }), [user.name, user.age]);
useEffect(() => {
setLogs(prev => [...prev, `🎯 使用useMemo优化的依赖`]);
}, [stableUser]);
const handleClick = () => {
setCount(prev => prev + 1);
};
useEffect(() => {
setLogs(prev => [...prev, `🔄 依赖函数: 每次都会运行(函数引用变化)`]);
}, [handleClick]); // 函数引用每次渲染都不同
const stableHandleClick = useCallback(() => {
setCount(prev => prev + 1);
}, []); // 空依赖,函数引用稳定
useEffect(() => {
setLogs(prev => [...prev, `🏆 使用useCallback优化的函数依赖`]);
}, [stableHandleClick]);
3.useEffect清理函数执行时机
// 1. 组件卸载时
useEffect(() => {
// effect逻辑
return () => {
// 组件卸载时执行清理
};
}, []);
// 2. 依赖变化时
useEffect(() => {
// effect逻辑
return () => {
// 依赖变化时先执行清理,再执行新effect
};
}, [dependency]);
// 3. 每次渲染前(无依赖数组)
useEffect(() => {
// 每次渲染都执行
return () => {
// 每次渲染前都执行清理
};
});
// 声明多个effect时
useEffect(() => {
console.log('Effect 1');
return () => console.log('Cleanup 1');
}, []);
useEffect(() => {
console.log('Effect 2');
return () => console.log('Cleanup 2');
}, []);
// 执行顺序:
// 挂载: Effect 1 → Effect 2
// 卸载: Cleanup 2 → Cleanup 1 (反向顺序)
4.useEffect处理竞态条件和异步请求
- 问题演示,如下代码当 userId 变化频繁譬,譬如是输入框的某个绑定值时,且网络状态不稳定时,userId由2变成3,且3返回的速度快于2。最终页面错误的显示userId为2时的脏数据。
// ❌ 有竞态条件的代码
const UserProfile = ({ userId }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
const fetchUser = async () => {
setLoading(true);
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
setUser(userData); // 如果旧的请求后完成,会覆盖新的
setLoading(false);
};
fetchUser();
}, [userId]); // userId 变化时重新请求
return (
<div>
{loading ? '加载中...' : user?.name}
</div>
);
};
解决方案
- 4.1 使用 AbortController
const UserProfile = ({ userId }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
// 创建 AbortController
const abortController = new AbortController();
const signal = abortController.signal;
const fetchUser = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(`/api/users/${userId}`, {
signal // 传递 signal
});
if (!response.ok) {
throw new Error('请求失败');
}
const userData = await response.json();
// 检查请求是否已被取消
if (!signal.aborted) {
setUser(userData);
}
} catch (err) {
// 如果是取消错误,不更新状态
if (err.name !== 'AbortError') {
setError(err.message);
setUser(null);
}
} finally {
if (!signal.aborted) {
setLoading(false);
}
}
};
fetchUser();
// cleanup 函数:取消未完成的请求
return () => {
abortController.abort();
};
}, [userId]); // 依赖 userId
if (loading) return <div>加载中...</div>;
if (error) return <div>错误: {error}</div>;
return (
<div>
<h2>{user?.name}</h2>
<p>{user?.email}</p>
</div>
);
};
- 4.2 使用标志变量 [核心原理:每次渲染都是独立的闭包]
const UserProfile = ({ userId }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
let isMounted = true; // 标志变量
const fetchUser = async () => {
setLoading(true);
try {
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
// 只有组件仍然挂载时才更新状态
if (isMounted) {
setUser(userData);
setLoading(false);
}
} catch (error) {
if (isMounted) {
console.error('请求失败:', error);
setLoading(false);
}
}
};
fetchUser();
// cleanup 函数:设置标志为 false
return () => {
isMounted = false;
};
}, [userId]);
// 渲染逻辑...
};
使用标志变量能解决的核心在于 每次渲染都是新的函数调用
// 第一次渲染
function Component() {
let isMounted = true; // 闭包变量1
// ... effect 逻辑1
return () => { isMounted = false; }; // cleanup1 清理的是闭包变量1
}
// 第二次渲染(完全是新的函数调用)
function Component() {
let isMounted = true; // 闭包变量2 - 全新的变量!
// ... effect 逻辑2
return () => { isMounted = false; }; // cleanup2 清理的是闭包变量2
}
useRef
| 特性 | useRef | useState |
|---|---|---|
| 触发重新渲染 | ❌ 不会触发组件重新渲染 | ✅ 会触发组件重新渲染 |
| 数据可变性 | ✅ 可变,直接修改 .current | ❌ 不可变,必须通过 setter 函数 |
| 返回值 | { current: value } 对象 | [value, setter] 数组 |
| 主要用途 | 访问 DOM、存储可变值、保持引用 | 管理组件状态、UI 响应数据 |
| 同步/异步 | 同步更新 | 异步更新(批量处理) |
| 使用场景 | 副作用、DOM 操作、计时器 ID | 用户界面、表单数据、业务逻辑 |
import { useState, useRef } from "react";
const Home = () => {
console.log("Home---start");
const [user, setUser] = useState({ name: "张三", age: 25 });
const userRef = useRef({ name: "张三", age: 25 });
const updateState = () => {
// ❌ 错误:直接修改不会触发重新渲染
// user.name = '李四';
// ✅ 正确:创建新对象, 会触发组件渲染, 会引起页面响应式变化
setUser((prev) => ({ ...prev, name: "李四" }));
};
const updateRef = () => {
// ✅ 正确:直接修改 ref 的值, 不会触发组件渲染, 当然也不会引起页面响应式变化
userRef.current.name = "李四";
userRef.current.age = 30;
console.log("Ref 更新:", userRef.current); // { name: "李四", age: 30 }
};
return (
<div>
<h3>数据可变性对比</h3>
<div>
<h4>useState (不可变)</h4>
<p>
姓名: {user.name}, 年龄: {user.age}
</p>
<button onClick={updateState}>更新 State</button>
</div>
<div>
<h4>useRef (可变)</h4>
<p>
姓名: {userRef.current.name}, 年龄: {userRef.current.age}
</p>
<button onClick={updateRef}>更新 Ref</button>
<span style={{ color: "gray", marginLeft: "10px" }}>
(界面不会更新,但值已改变)
</span>
</div>
</div>
);
};
export default Home;
- 1.访问dom元素
const DomAccessDemo = () => {
const inputRef = useRef(null);
const divRef = useRef(null);
const focusInput = () => {
inputRef.current.focus();
inputRef.current.style.border = '2px solid red';
};
const measureDiv = () => {
const rect = divRef.current.getBoundingClientRect();
console.log('Div 尺寸:', rect);
divRef.current.textContent = `宽度: ${rect.width}px, 高度: ${rect.height}px`;
};
return (
<div>
<input ref={inputRef} placeholder="点击按钮聚焦我" />
<button onClick={focusInput}>聚焦输入框</button>
<div
ref={divRef}
style={{ width: '200px', height: '100px', border: '1px solid black', margin: '10px 0' }}
>
测量这个 div
</div>
<button onClick={measureDiv}>测量尺寸</button>
</div>
);
};
- 2. 存储计时器 ID 或事件监听器
const TimerDemo = () => {
const [time, setTime] = useState(0);
const timerRef = useRef(null);
const startTimer = () => {
if (timerRef.current) return; // 防止重复启动
timerRef.current = setInterval(() => {
setTime(prev => prev + 1);
}, 1000);
};
const stopTimer = () => {
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
};
const resetTimer = () => {
stopTimer();
setTime(0);
};
// 组件卸载时清理
useEffect(() => {
return () => stopTimer();
}, []);
return (
<div>
<h3>计时器: {time} 秒</h3>
<button onClick={startTimer}>开始</button>
<button onClick={stopTimer}>停止</button>
<button onClick={resetTimer}>重置</button>
</div>
);
};
useRef || forwardRef + useImperativeHandle
- 通过
useRef获取DOM标签, 并通过.current操作DOM的属性和方法 - 通过
useRef获取子组件实例:如果子组件是类组件,那能通过useRef获取子组件实例以及可以直接调用子组件定义的方法; 但子组件若是函数组件,通过.current访问到的是null。必须通过forwardRef包裹子组件+useImperativeHandle暴露子组件内部方法才行。
| 子组件类型 | 能否获取实例 | 默认能获取什么 | 需要什么条件 |
|---|---|---|---|
| 类组件 | ✅ 可以 | 类组件的完整实例 | 直接使用 ref |
| 函数组件 | ❌ 不能(默认) | null 或 undefined | 需要使用 forwardRef + useImperativeHandle |
父组件
import { useState, useRef } from "react";
import Child from "./Child";
const Home = () => {
console.log("Home---start");
const childRef = useRef(null);
const h3Ref = useRef(null);
const onSetMessage = () => {
childRef.current?.showMessage("haha");
};
return (
<>
<h3 ref={h3Ref}>Home page</h3>
<button onClick={onSetMessage}>设置子组件内容</button>
<Child ref={childRef}></Child>
</>
);
};
export default Home;
子组件
import React, { forwardRef, useImperativeHandle, useState } from "react";
const Child = (props, ref) => {
const [count, setCount] = useState(0);
const [message, setMessage] = useState("Hello from Child component");
// 使用 useImperativeHandle 暴露特定的方法和值
useImperativeHandle(
ref,
() => ({
// 暴露给父组件的方法
incrementCount: () => {
setCount((prev) => prev + 1);
},
showMessage: (text) => {
setMessage(text);
return `Message set to: ${text}`;
},
// 暴露给父组件的值
getComponentInfo: () => {
return {
count,
message,
timestamp: new Date().toISOString(),
};
},
// 只读的当前值
currentCount: count,
// 重置方法
reset: () => {
setCount(0);
setMessage("Hello from function component");
},
}),
[count, message]
); // 依赖数组,当 count 或 message 变化时重新创建暴露的方法
return (
<div style={{ border: "2px solid green", padding: "10px", margin: "10px" }}>
<h3>函数子组件(使用 forwardRef)</h3>
<p>Count: {count}</p>
<p>Message: {message}</p>
<button onClick={() => setCount((c) => c + 1)}>内部增加</button>
</div>
);
};
// 使用 forwardRef 包装函数组件
export default forwardRef(Child);
createPortal
createPortal 允许你将子元素渲染到 DOM 节点中的不同位置,常用于模态框、提示框、全局通知等场景。这是因为 position: fixed 在理论上是相对于浏览器窗口定位的,但父元素如果加了 transform perspective filter等属性时,他就相对于父元素而定位了。这样就导致模态框、提示框、全局通知等全局显示组件定位不准了。
import React, { useState } from "react";
import { createPortal } from "react-dom";
// 模态框组件
function Modal({ children, isOpen, onClose }) {
if (!isOpen) return null;
return createPortal(
<div
style={{
position: "fixed",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: "rgba(0, 0, 0, 0.5)",
display: "flex",
alignItems: "center",
justifyContent: "center",
zIndex: 1000,
}}
>
<div
style={{
backgroundColor: "white",
padding: "20px",
borderRadius: "8px",
minWidth: "300px",
maxWidth: "500px",
}}
>
{children}
<button onClick={onClose} style={{ marginTop: "20px" }}>
关闭
</button>
</div>
</div>,
document.body // 渲染到 body 下
);
}
function App() {
const [isModalOpen, setIsModalOpen] = useState(false);
return (
// 父元素加了 transform 样式属性,但是 Modal 使用了 createPortal api包裹,使其父元素是body,所以能处于浏览器正中间显示;
// 父元素加了 transform 样式属性,Modal 不使用 createPortal , Modal 定位的参考点是div,导致 Modal 无法在浏览器正中间显示。
<div style={{ padding: "20px", transform: "scale(0.5)" }}>
<h1>基础模态框示例</h1>
<button onClick={() => setIsModalOpen(true)}>打开模态框</button>
<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
<h2>这是模态框内容</h2>
<p>这个模态框是使用 createPortal 渲染到 body 元素下的。</p>
<p>即使父元素有 overflow: hidden,模态框也不会被裁剪。</p>
</Modal>
</div>
);
}
export default App;
路由 react-router-dom
- 一个简单的路由配置,
Routes和Route来定义路由
// main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'
import { BrowserRouter } from 'react-router-dom' // 导入 BrowserRouter
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<BrowserRouter> {/* 使用 BrowserRouter 包裹 App */}
<App />
</BrowserRouter>
</React.StrictMode>,
)
// App.jsx
import { Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
import About from './pages/About';
import NotFound from './pages/NotFound';
function App() {
return (
<div className="App">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="*" element={<NotFound />} /> {/* 404 页面 */}
</Routes>
</div>
);
}
export default App;
- useRoutes,允许你使用 JavaScript 对象 来定义路由,而不是使用
<Route>组件
1.创建路由配置对象 src/routes/index.jsx
// src/routes/index.jsx
import { useRoutes } from 'react-router-dom';
// 1. 引入你的页面组件
import Home from '../pages/Home';
import About from '../pages/About';
import UserList from '../pages/UserList';
import UserProfile from '../pages/UserProfile';
import NotFound from '../pages/NotFound';
// 2. 定义路由配置数组
const routes = [
{
path: '/',
element: <Home />,
},
{
path: '/about',
element: <About />,
},
{
path: '/users',
element: <UserList />,
// 3. 可以嵌套路由(子路由)
// children: [...]
},
{
path: '/users/:userId', // 4. 动态路由参数
element: <UserProfile />,
},
{
path: '*', // 5. 404 通配符路由,匹配所有未定义的路径
element: <NotFound />,
},
];
// 6. 创建一个自定义 Hook 来使用路由配置
export default function AppRoutes() {
// useRoutes Hook 接收路由配置数组,并返回匹配当前 URL 的路由元素
const element = useRoutes(routes);
return element;
}
2.在主应用组件中使用, src/App.jsx,使用上面创建的 AppRoutes 组件。
// src/App.jsx
import './App.css';
// 1. 引入路由组件
import AppRoutes from './routes';
// 2. 引入其他需要的组件,比如导航栏
import Navigation from './components/Navigation';
function App() {
return (
<div className="App">
{/* 3. 导航栏,通常包含 <Link> 组件 */}
<Navigation />
{/* 4. 这里是路由渲染的位置 */}
<main>
<AppRoutes />
</main>
</div>
);
}
export default App;
3.在入口文件设置 Router src/main.jsx,用 <BrowserRouter> 包裹了 App 组件
// src/main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'
// 1. 引入 BrowserRouter
import { BrowserRouter } from 'react-router-dom'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
{/* 2. 用 BrowserRouter 包裹 App,为其下属组件提供路由上下文 */}
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>,
)
useRoutes+<Outlet />嵌套路由
- 配置children子路由选项
// src/routes/index.jsx (部分代码)
import { useRoutes } from 'react-router-dom';
const routes = [
{
path: '/',
element: <Home />,
},
{
path: '/dashboard',
element: <DashboardLayout />, // 布局组件,内部包含 <Outlet/>
children: [ // 子路由将在布局组件的 <Outlet/> 位置渲染
{
index: true, // 等同于 path: '',匹配 /dashboard
element: <DashboardHome />,
},
{
path: 'settings', // 匹配 /dashboard/settings
element: <DashboardSettings />,
},
{
path: 'analytics', // 匹配 /dashboard/analytics
element: <DashboardAnalytics />,
},
],
},
// ... other routes
];
// 创建路由 Hook
export default function AppRoutes() {
const element = useRoutes(routes);
return element;
}
2.父组件需要包含一个 <Outlet /> 来渲染子路由
// src/layouts/DashboardLayout.jsx
import { Outlet, Link } from 'react-router-dom';
function DashboardLayout() {
return (
<div>
<h1>Dashboard</h1>
<nav>
<Link to="/dashboard">Home</Link>
<Link to="/dashboard/settings">Settings</Link>
<Link to="/dashboard/analytics">Analytics</Link>
</nav>
<hr />
{/* 子路由对应的组件将在这里渲染 */}
<Outlet />
</div>
);
}
export default DashboardLayout;
- 路由其他Api
<Link to="/dashboard">Home</Link>
import { useNavigate, useSearchParams, useParams, useLocation } from 'react-router-dom';
// useSearchParams: 获取查询参数,例如获取?后面的 `http://example.com/path?name=Alice&age=25`。
const [searchParams, setSearchParams] = useSearchParams();
// 使用 get 方法获取特定参数
const name = searchParams.get('name');
const age = searchParams.get('age');
// useParams: 获取 URL 中的参数,例如 /users/123 中的 '123'
const { userId } = useParams();
const navigate = useNavigate();
const handleClick = () => {
// 跳转到关于页面
navigate('/about');
// 或者使用 replace: true 替换当前历史记录
// navigate('/about', { replace: true });
};
在跳转时,你需要通过 `state` 属性传递参数:
// 或使用 <Navigate to="/user-detail" state={}>
navigate('/user-detail', {
state: {
user: { name: 'Alice', email: 'alice@example.com' },
from: 'homepage'
}
});
// 获取 location 对象
const location = useLocation(); // { pathname: '/dashboard', search: '?name=1&age=12', hash: '', state: null }
// 从 location.state 中获取传递的状态参数
const user = location.state?.user; // 假设传递了一个 user 对象
const from = location.state?.from; // 或其他状态数据
通过 state 传递的参数在页面刷新后通常会丢失,因为它们通常存储在内存中(如 React Router 的 history 状态),而不是 URL 中。如果参数需要持久化,应优先考虑使用动态路由参数或查询参数。
- 路由守卫
- routes/index.jsx
import { useRoutes } from "react-router-dom";
import ProtectedRoute from "./ProtectedRoute";
import Login from "../pages/Login.jsx";
import Dashboard from "../pages/Dashboard";
import Profile from "../pages/Profile";
const routes = [
{
path: "/login",
element: <Login />,
},
{
// 保护路由组
element: <ProtectedRoute />,
children: [
{
path: "/dashboard",
element: <Dashboard />,
},
{
path: "/profile",
element: <Profile />,
},
],
},
];
// 创建路由 Hook
export default function AppRoutes() {
const element = useRoutes(routes);
return element;
}
- ProtectedRoute.jsx
import { Navigate, Outlet, useLocation } from "react-router-dom";
const ProtectedRoute = () => {
// 这里可以根据你的认证逻辑进行修改
const isAuthenticated = !!localStorage.getItem("authToken");
const location = useLocation();
console.log("location", location); // { pathname: '/dashboard', search: '?name=1&age=12', hash: '', state: null, key: 'nedxjr1g' }
if (!isAuthenticated) {
// 未认证,重定向到登录页
return <Navigate to="/login" replace state={{ from: location }} />;
}
// 认证通过,渲染子路由
return <Outlet />;
};
export default ProtectedRoute;
- Login.jsx
import { useNavigate, useLocation } from "react-router-dom";
function Login() {
const navigate = useNavigate();
const location = useLocation();
const from = location.state?.from || "/dashboard"; // :cite[4]
const onLogin = () => {
localStorage.setItem("authToken", "fake-jwt-token");
console.log("from", from); // { pathname: '/dashboard', search: '?name=1&age=12', hash: '', state: null, key: 'nedxjr1g' }
navigate(from);
};
return (
<>
<button onClick={onLogin}>登录</button>
</>
);
}
export default Login;
状态管理 Zustand
- 创建一个 Store
// store/useCounterStore.js
import { create } from 'zustand';
// 使用 create 函数创建 store,其参数是一个返回状态和操作的函数
const useCounterStore = create((set, get) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
// 也可以获取当前状态
doubleCount: () => get().count * 2,
}));
export default useCounterStore;
- 在组件中使用
// components/Counter.jsx
import useCounterStore from '../store/useCounterStore';
function Counter() {
// 从 store 中提取需要的状态和动作
// 你可以选择整个 store,或者只选择需要的部分,Zustand 会进行自动的优化
const { count, increment, decrement } = useCounterStore();
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
}
export default Counter;
- 持久化解决方案
// store/useStoreWithPersist.js
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
// 使用 persist middleware
const usePersistedStore = create(
persist(
(set, get) => ({
count: 0,
user: null,
theme: 'light',
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
setUser: (userData) => set({ user: userData }),
setTheme: (theme) => set({ theme: theme }),
// 计算属性
getDoubleCount: () => get().count * 2,
}),
{
name: 'app-storage', // 存储的键名
storage: createJSONStorage(() => localStorage), // 使用 localStorage
// storage: createJSONStorage(() => sessionStorage), // 或者使用 sessionStorage
}
)
);
export default usePersistedStore;