当前位置: 首页 > news >正文

React 第六十一节 Router 中 createMemoryRouter的使用详解及案例注意事项

前言

createMemoryRouterReact Router 提供的一种特殊路由器,它将路由状态存储在内存中而不是浏览器的 URL 地址栏中
这种路由方式特别适用于测试非浏览器环境(如 React Native)以及需要完全控制路由历史的场景。

一、createMemoryRouter 的主要用途

  1. 测试环境:在单元测试和集成测试中模拟路由行为
  2. 非浏览器环境:在 React Native、ElectronNode.js 服务器端渲染中使用
  3. 组件沙盒:在 Storybook 或类似工具中独立运行路由组件
  4. 路由历史控制:需要编程式控制完整路由历史的场景
  5. 无 URL 环境:在不需要地址栏显示路由变化的应用中使用

二、createMemoryRouter 与 createBrowserRouter 的关键区别

在这里插入图片描述

三、createMemoryRouter 完整代码示例

3.1、 基础路由配置

// src/MemoryRouterDemo.jsx
import React from 'react';
import {createMemoryRouter,RouterProvider,Link,Outlet,useLocation
} from 'react-router-dom';// 页面组件
function Home() {return (<div className="page"><h1>首页</h1><p>欢迎使用内存路由示例</p><div className="page-nav"><Link to="/about" className="nav-link">关于我们</Link><Link to="/products" className="nav-link">产品列表</Link></div></div>);
}function About() {return (<div className="page"><h1>关于我们</h1><p>我们是一家专注于前端技术的公司</p><Link to="/" className="back-link">返回首页</Link></div>);
}// 布局组件
function RootLayout() {const location = useLocation();return (<div className="app"><header className="app-header"><h1 className="logo">内存路由示例</h1><div className="url-display">当前路由: <code>{location.pathname || '/'}</code></div><nav className="main-nav"><Link to="/" className="nav-item">首页</Link><Link to="/about" className="nav-item">关于</Link><Link to="/products" className="nav-item">产品</Link></nav></header><main className="app-content"><Outlet /> {/* 子路由渲染位置 */}</main><footer className="app-footer"><p>当前使用: <code>createMemoryRouter</code> | 路由历史: {location.key}</p></footer></div>);
}// 创建内存路由配置
const router = createMemoryRouter([{path: "/",element: <RootLayout />,children: [{index: true,element: <Home />},{path: "about",element: <About />},{path: "products",element: <ProductsList />}]}
], {initialEntries: ["/"], // 初始路由initialIndex: 0 // 初始路由索引
});// 产品列表组件
function ProductsList() {const products = [{ id: 1, name: 'React 教程', price: 99 },{ id: 2, name: 'Node.js 实战', price: 129 },{ id: 3, name: 'TypeScript 指南', price: 89 }];return (<div className="page"><h1>产品列表</h1><div className="products-grid">{products.map(product => (<div key={product.id} className="product-card"><h3>{product.name}</h3><p>价格: ¥{product.price}</p></div>))}</div><Link to="/" className="back-link">返回首页</Link></div>);
}// 导出使用内存路由的应用
export default function MemoryRouterDemo() {return <RouterProvider router={router} />;
}

3.2、 在 Storybook 中使用 createMemoryRouter

// src/stories/UserProfile.stories.jsx
import React from 'react';
import { createMemoryRouter, RouterProvider } from 'react-router-dom';
import UserProfile from '../components/UserProfile';export default {title: 'UserProfile',component: UserProfile,
};// 模板函数
const Template = (args) => {// 创建内存路由const router = createMemoryRouter([{path: '/user/:userId',element: <UserProfile {...args} />,loader: () => ({user: {id: '123',name: '张三',email: 'zhangsan@example.com',joinDate: '2022-01-15'}})}], {initialEntries: ['/user/123'],initialIndex: 0});return <RouterProvider router={router} />;
};// 默认状态
export const Default = Template.bind({});
Default.args = {};// 加载状态
export const LoadingState = Template.bind({});
LoadingState.args = {// 模拟延迟加载loader: () => new Promise(() => {})
};// 错误状态
export const ErrorState = Template.bind({});
ErrorState.args = {loader: () => {throw new Error('无法加载用户数据');}
};

3.3、 在测试中使用 createMemoryRouter

// src/tests/LoginPage.test.jsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { createMemoryRouter, RouterProvider } from 'react-router-dom';
import LoginPage from '../components/LoginPage';describe('LoginPage', () => {// 创建登录页路由const createLoginRouter = (initialEntries = ['/login']) => {return createMemoryRouter([{path: '/login',element: <LoginPage />},{path: '/dashboard',element: <div>仪表盘页面</div>}], {initialEntries,initialIndex: 0});};test('渲染登录表单', () => {const router = createLoginRouter();render(<RouterProvider router={router} />);expect(screen.getByLabelText('用户名')).toBeInTheDocument();expect(screen.getByLabelText('密码')).toBeInTheDocument();expect(screen.getByRole('button', { name: '登录' })).toBeInTheDocument();});test('显示验证错误', async () => {const router = createLoginRouter();render(<RouterProvider router={router} />);fireEvent.click(screen.getByRole('button', { name: '登录' }));expect(await screen.findByText('请输入用户名')).toBeInTheDocument();expect(await screen.findByText('请输入密码')).toBeInTheDocument();});test('成功登录后导航到仪表盘', async () => {const router = createLoginRouter();render(<RouterProvider router={router} />);// 填写表单fireEvent.change(screen.getByLabelText('用户名'), {target: { value: 'testuser' }});fireEvent.change(screen.getByLabelText('密码'), {target: { value: 'password123' }});// 提交表单fireEvent.click(screen.getByRole('button', { name: '登录' }));// 验证导航expect(await screen.findByText('仪表盘页面')).toBeInTheDocument();});
});

3.4、 在 React Native 中使用 createMemoryRouter

// App.js (React Native)
import React from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
import { createMemoryRouter, RouterProvider, Link, Outlet } from 'react-router-dom-native';// 创建自定义导航组件
function NativeLink({ to, children }) {return (<Button title={children} onPress={() => router.navigate(to)} />);
}// 页面组件
function HomeScreen() {return (<View style={styles.container}><Text style={styles.title}>首页</Text><NativeLink to="/about">关于我们</NativeLink></View>);
}function AboutScreen() {return (<View style={styles.container}><Text style={styles.title}>关于我们</Text><Text>我们是一家移动应用开发公司</Text><NativeLink to="/">返回首页</NativeLink></View>);
}// 创建内存路由
const router = createMemoryRouter([{path: "/",element: (<View style={styles.app}><Outlet /></View>),children: [{index: true,element: <HomeScreen />},{path: "about",element: <AboutScreen />}]}
]);// 应用入口
export default function App() {return <RouterProvider router={router} />;
}const styles = StyleSheet.create({app: {flex: 1,padding: 20,backgroundColor: '#f5f5f5'},container: {flex: 1,justifyContent: 'center',alignItems: 'center',},title: {fontSize: 24,fontWeight: 'bold',marginBottom: 20}
});

3.5、 高级路由历史操作

// src/HistoryDemo.jsx
import React, { useState, useEffect } from 'react';
import {createMemoryRouter,RouterProvider,useLocation,useNavigate
} from 'react-router-dom';// 历史控制组件
function HistoryController() {const location = useLocation();const navigate = useNavigate();const [history, setHistory] = useState([]);useEffect(() => {setHistory(prev => [...prev, location]);}, [location]);const goBack = () => navigate(-1);const goForward = () => navigate(1);return (<div className="history-controls"><div className="history-buttons"><button onClick={goBack} disabled={history.length <= 1}>后退</button><button onClick={goForward} disabled={history.length <= 1}>前进</button></div><div className="history-list"><h3>路由历史:</h3><ul>{history.map((entry, index) => (<li key={entry.key} className={index === history.length - 1 ? 'current' : ''}>{entry.pathname}</li>))}</ul></div></div>);
}// 页面组件
function Dashboard() {return (<div className="page"><h1>仪表盘</h1><HistoryController /></div>);
}function Settings() {return (<div className="page"><h1>设置</h1><HistoryController /></div>);
}function Profile() {return (<div className="page"><h1>个人资料</h1><HistoryController /></div>);
}// 创建内存路由
const router = createMemoryRouter([{path: "/",element: <Dashboard />,},{path: "/settings",element: <Settings />,},{path: "/profile",element: <Profile />,}
], {initialEntries: ["/", "/settings", "/profile"],initialIndex: 0
});// 导出应用
export default function HistoryDemo() {return <RouterProvider router={router} />;
}

四、createMemoryRouter 核心功能详解

4.1、 创建和配置

const router = createMemoryRouter(routes, options);

参数:

  1. routes: 路由配置数组(与 createBrowserRouter 相同)
  2. options: 配置对象
  3. initialEntries: 初始历史记录数组(默认为 [“/”])
  4. initialIndex: 初始条目的索引(默认为最后一个索引)
  5. basename: 应用的基础路径
  6. future: 未来标志配置
  7. hydrationData: 水合数据(用于 SSR)

2.2、 路由历史操作

内存路由提供完整的编程式历史控制:

// 导航到新路径
router.navigate("/new-path");// 添加历史记录
router.navigate("/new-path", { state: { data: 123 }, replace: false });// 后退
router.navigate(-1);// 前进
router.navigate(1);// 替换当前历史记录
router.navigate("/replaced-path", { replace: true });// 获取当前历史记录
const currentLocation = router.state.location;// 获取完整历史
const historyStack = router.state.history.entries;

4.3、 在测试中使用

test("导航到关于页", async () => {// 创建内存路由const router = createMemoryRouter(routes, {initialEntries: ["/"],initialIndex: 0});render(<RouterProvider router={router} />);// 初始状态验证expect(screen.getByText("首页")).toBeInTheDocument();// 执行导航act(() => {router.navigate("/about");});// 验证导航结果expect(screen.getByText("关于我们")).toBeInTheDocument();
});

4.4. 在 Storybook 中使用

// 创建带特定状态的组件
const Template = (args) => {const router = createMemoryRouter([{path: "/product/:id",element: <ProductDetail {...args} />,loader: () => ({product: {id: "123",name: "高级产品",price: 299,description: "这是一个高级产品"}})}], {initialEntries: ["/product/123"],initialIndex: 0});return <RouterProvider router={router} />;
};// 错误状态
export const ErrorState = Template.bind({});
ErrorState.args = {loader: () => {throw new Error("产品加载失败");}
};

4.5、在 React Native 中集成

import { createMemoryRouter, RouterProvider } from 'react-router-dom-native';const router = createMemoryRouter([{path: "/",element: <HomeScreen />,},{path: "/details",element: <DetailsScreen />,}
]);export default function App() {return <RouterProvider router={router} />;
}

五、createMemoryRouter 最佳实践

5.1、 测试场景

describe("认证流程", () => {it("未登录用户重定向到登录页", async () => {const router = createMemoryRouter(routes, {initialEntries: ["/dashboard"],});render(<RouterProvider router={router} />);expect(await screen.findByText("登录页面")).toBeInTheDocument();});it("登录后导航到仪表盘", async () => {const router = createMemoryRouter(routes, {initialEntries: ["/login"],});render(<RouterProvider router={router} />);// 填写登录表单userEvent.type(screen.getByLabelText("用户名"), "testuser");userEvent.type(screen.getByLabelText("密码"), "password123");userEvent.click(screen.getByRole("button", { name: "登录" }));expect(await screen.findByText("仪表盘")).toBeInTheDocument();});
});

5.2、 模拟特定路由状态

// 模拟带状态的路由
const router = createMemoryRouter(routes, {initialEntries: [{ pathname: "/checkout", state: { cart: [{ id: 1, name: "产品A", price: 99 },{ id: 2, name: "产品B", price: 149 }] } }],initialIndex: 0
});// 在组件中访问状态
function Checkout() {const location = useLocation();const cart = location.state?.cart || [];// ...
}

5.3、 复杂历史操作

// 创建包含多个条目的历史
const router = createMemoryRouter(routes, {initialEntries: ["/", "/products", "/products/123"],initialIndex: 2 // 当前在 /products/123
});// 后退到产品列表
router.navigate(-1);
expect(router.state.location.pathname).toBe("/products");// 前进回产品详情
router.navigate(1);
expect(router.state.location.pathname).toBe("/products/123");

5.4、 错误边界测试

it("显示加载错误", async () => {// 创建会抛出错误的路由const router = createMemoryRouter([{path: "/",element: <Home />,errorElement: <ErrorPage />},{path: "/failing",element: <FailingComponent />,loader: () => {throw new Error("模拟加载错误");}}], {initialEntries: ["/failing"],initialIndex: 0});render(<RouterProvider router={router} />);expect(await screen.findByText("发生错误")).toBeInTheDocument();expect(await screen.findByText("模拟加载错误")).toBeInTheDocument();
});

六、使用场景推荐

  1. 组件测试:测试路由相关的组件逻辑
  2. Storybook 集成:在隔离环境中展示路由组件
  3. React Native 应用:在移动端实现路由功能
  4. Electron 应用:在桌面应用中管理路由
  5. 命令行工具:为 CLI 工具添加路由功能
  6. 服务端渲染测试:模拟路由行为进行 SSR 测试
  7. 游戏和多媒体应用:不需要 URL 的路由场景
  8. 教育演示:展示路由工作原理而不影响浏览器历史

总结

createMemoryRouter 是 React Router 提供的一种灵活的路由解决方案,
主要特点包括

  1. 内存存储:路由状态存储在内存中,不依赖浏览器 URL
  2. 完全控制:提供完整的编程式历史管理
  3. 环境无关:可在任何 JavaScript 环境中使用
  4. 测试友好:简化路由相关组件的测试
  5. 隔离环境:适合在 Storybook 等工具中展示组件

通过 createMemoryRouter,我们可以在脱离浏览器环境的情况下,获得完整的 React Router 功能,特别适合测试和特殊环境下的路由需求。

相关文章:

  • SpringBoot+vue前后端分离系统开发(期末)
  • ios 26发布:设计革新与智能整合
  • 26考研 | 王道 | 计算机组成原理 | 六、总线
  • 栈与队列:数据结构优劣全解析
  • react react-router-dom中获取自定义参数v6.4版本之后
  • 域名+nginx反向代理实现案例
  • frida对qt5(32位)实现简单HOOK
  • Oracle 中使用CONNECT BY、START WITH递归查询
  • Dockerfile - 自定义 Nginx 镜像构建
  • 面向对象-对象和属性描述符详解(一)
  • 使用HashMap或者List模拟数据库插入和查询数据
  • java集合(十) ---- LinkedList 类
  • 精准洞察位移变化,位移传感器开启测量新纪元
  • 自适应攻击的强大后门防御
  • 个典型的 Java 泛型在反序列化场景下“类型擦除 + 无法推断具体类型”导致的隐性 Bug
  • 解决 html2canvas 把svg转成jpg,无法把svg里的image图片正常显示的情况
  • uni-app项目实战笔记5--使用grid进行定位布局
  • Spring boot 的 maven 打包过程
  • 人工智能 倒底是 智能 还是 智障?
  • FastAPI如何用角色权限让Web应用安全又灵活?
  • 深圳购物网站建设价格/网站创建流程
  • 安阳做网站电话/广告图片
  • 物流门户网站源码/个人网站设计图片
  • 校园网站建设必要性/北京十大营销策划公司
  • 和硕网站建设/google竞价推广
  • 美工培训网课/南京关键词优化服务