当我们最初开始将 Remix 数据 API 迁移到 React Router 时,我们意识到它们带来了构建路由的一种截然不同的方式。我们不再通过 <Routes>
组件在 React 渲染组件树时发现路由,而是需要提升路由定义,以便我们能够 将获取数据与渲染解耦。
这带来了一个有趣的难题。我们有大量的 v6 BrowserRouter
应用程序,它们通过 <Routes>
组件愉快地定义了它们的路由——我们如何为它们提供平滑的升级体验,而无需进行大规模迁移到新方法?这排除了新的主要版本,我们专注于以完全向后兼容的方式添加这些新功能,这将为用户提供从 BrowserRouter
到 RouterProvider
的增量升级路径。
首先要注意的是,存在一些新的 数据 API,它们只适用于通过新的 数据路由器(即 createBrowserRouter
)定义的路由。这些 API 包括以下几类:
loader
、action
、shouldRevalidate
、handle
和 lazy
useLoaderData
、useActionData
、useFetcher
、useMatches
、useNavigation
等route.errorElement
、route.ErrorBoundary
和 useRouteError
在 v6.4.0 之前存在的其余 API 仍然可以在两种 BrowserRouter
和 RouterProvider
应用程序中使用。这些包括常见的钩子/组件,例如 useNavigate
、useLocation
、useParams
、<Link>
、<Outlet />
等。
我们构建了新的 <RouterProvider>
组件,以便它能够在根路由器定义的路由上启用新的数据 API,同时不排除 BrowserRouter
应用程序中常用的后代 <Routes>
树。这样做是为了明确地允许从一种方式到另一种方式的增量迁移。让我们看看如何做到这一点。
假设我们有一个当前应用程序,它有两个后代路由树,并假设这些路由都在进行组件内数据获取,并渲染它们自己的加载和错误状态。
import {
BrowserRouter,
Link,
Route,
Routes,
} from "react-router-dom";
export default function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/blog/*" element={<BlogApp />} />
<Route path="/users/*" element={<UserApp />} />
</Routes>
</BrowserRouter>
);
}
function Home() {
return (
<>
<h1>Welcome!</h1>
<p>
Check out the <Link to="/blog">blog</Link> or the{" "}
<Link to="users">users</Link> section
</p>
</>
);
}
function BlogApp() {
return (
<Routes>
<Route index element={<h1>Blog Index</h1>} />
<Route path="posts" element={<h1>Blog Posts</h1>} />
</Routes>
);
}
function UserApp() {
return (
<Routes>
<Route index element={<h1>Users Index</h1>} />
</Routes>
);
}
我们只需进行一些小的更改,就可以将此应用程序渲染到 RouterProvider
中。
App
组件更改为 Root
<BrowserRouter>
组件Root
元素创建一个数据路由器单例App
组件,渲染一个 <RouterProvider>
import {
createBrowserRouter,
Link,
Route,
RouterProvider,
Routes,
} from "react-router-dom";
// 3️⃣ Router singleton created
const router = createBrowserRouter([
{ path: "*", element: <Root /> },
]);
// 4️⃣ RouterProvider added
export default function App() {
return <RouterProvider router={router} />;
}
// 1️⃣ Changed from App to Root
function Root() {
// 2️⃣ `BrowserRouter` component removed, but the <Routes>/<Route>
// component below are unchanged
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/blog/*" element={<BlogApp />} />
<Route path="/users/*" element={<UserApp />} />
</Routes>
);
}
function Home() {
/* Unchanged */
}
function BlogApp() {
/* Unchanged */
}
function UserApp() {
/* Unchanged */
}
🥳 恭喜 - 您现在正在渲染一个数据路由器应用程序!但是等等 - 我们还不能使用任何新功能,因为我们的路由都没有在顶部使用 createBrowserRouter
定义 😢。要访问新的 API,我们需要开始逐个将路由提升到数据路由器。
让我们从 <Home>
元素的 /
路由开始。我们只需要将 <Route>
定义提升到数据路由器即可
const router = createBrowserRouter([
{ path: "/", element: <Home /> }, // 🆕
{ path: "*", element: <Root /> },
]);
export default function App() {
return <RouterProvider router={router} />;
}
function Root() {
return (
<Routes>
{/* ⬆️ Home route lifted up to the data router */}
<Route path="/blog/*" element={<BlogApp />} />
<Route path="/users/*" element={<UserApp />} />
</Routes>
);
}
现在,您可以将数据 API 添加到您的主页路由(loader
、action
、errorElement
),并开始在您的 Home 组件中利用数据钩子(useLoaderData
、useActionData
、useFetcher
、<Form>
等)。
现在让我们看看如何将博客应用程序向上提升,但仍然一次提升一个叶子路由。为了将 /blog
索引路由提升,我们需要将 /blog/*
通配符路由也提升,但我们仍然可以将 /blog/posts
路由渲染在当前位置,并单独进行处理。
const router = createBrowserRouter([
{ path: "/", element: <Home /> },
{
// Lifted blog splat route
path: "/blog/*",
children: [
// New blog index route
{ index: true, element: <h1>Blog Index</h1> },
// Blog subapp splat route added for /blog/posts matching
{ path: "*", element: <BlogApp /> },
],
},
{ path: "*", element: <Root /> },
]);
export default function App() {
return <RouterProvider router={router} />;
}
function Root() {
return (
<Routes>
{/* ⬆️ Blog splat route lifted */}
<Route path="/users/*" element={<UserApp />} />
</Routes>
);
}
function BlogApp() {
return (
<Routes>
{/* ⬆️ Blog index route lifted */}
<Route path="posts" element={<h1>Blog Posts</h1>} />
</Routes>
);
}
现在,您的博客索引路由可以参与数据加载了。
您可以继续这样做,一次提升一个路由,直到最终将所有路由转换为数据路由,并且不再使用任何嵌套的 <Routes>
来定义您的路由树。为了避免捆绑包膨胀,建议利用 route.lazy 属性来延迟加载您的路由。
<BrowserRouter>
和 <Routes>
之间有一些东西许多人通过以下方式在他们的 <Routes>
周围渲染一个应用程序外壳
export default function App() {
return (
<BrowserRouter>
<header>
<h1>My Super Cool App</h1>
<NavMenu />
</header>
<main>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/blog/*" element={<BlogApp />} />
<Route path="/users/*" element={<UserApp />} />
</Routes>
</main>
<footer>©️ me 2023</footer>
</BrowserRouter>
);
}
如果您发现自己处于这种情况,请不要担心 - 您可以在开始上述迁移之前执行一个简单的解决方案。
这很常见,但在上述迁移方法中存在问题,因为我们需要将东西逐个提升到 RouterProvider
,但这个“应用程序外壳”不是路由的一部分……但它可以是!这个“应用程序外壳”实际上只是一个带有 <Outlet>
的无路径布局路由!因此,在开始上述迁移之前,只需将这个“应用程序外壳”移动到您的路由周围的无路径布局路由中,如下所示
export default function App() {
return (
<BrowserRouter>
<Routes>
{/* 1️⃣ Wrap your routes in a pathless layout route */}
<Route element={<Layout />}>
<Route path="/" element={<Home />} />
<Route path="/blog/*" element={<BlogApp />} />
<Route path="/users/*" element={<UserApp />} />
</Route>
</Routes>
</BrowserRouter>
);
}
function Layout() {
return (
<>
<header>
<h1>My Super Cool App</h1>
<NavMenu />
</header>
<main>
{/* 2️⃣ Render the app routes via the Layout Outlet */}
<Outlet />
</main>
<footer>©️ me 2023</footer>
</>
);
}
完成此操作后,您可以继续使用上述迁移策略,并开始将路由逐个提升到您的 RouterProvider
中。您可能希望首先提升布局路由,以便所有子路由都嵌套在其中。