Skip to content

Commit c17512d

Browse files
authored
fix: Remove the internal router singleton (#9227)
* feat: remove singleton in favor of manual router creaton * update examples * fix browser/hash tests * Fix memory tests * Rename DataRouter -> RouterProvider * rename in examples * support multiple subscribers * add changeset * alias createRoutesFromElements * Add consistent-type-imports eslint rule * DataStaticRouter -> StaticRouterProvider * Add PR number to changeset
1 parent 112c02c commit c17512d

32 files changed

+712
-560
lines changed

.changeset/calm-lies-destroy.md

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
---
2+
"react-router": patch
3+
"react-router-dom": patch
4+
"@remix-run/router": patch
5+
---
6+
7+
fix: remove internal router singleton (#9227)
8+
9+
This change removes the internal module-level `routerSingleton` we create and maintain inside our data routers since it was causing a number of headaches for non-simple use cases:
10+
11+
- Unit tests are a pain because you need to find a way to reset the singleton in-between tests
12+
- Use use a `_resetModuleScope` singleton for our tests
13+
- ...but this isn't exposed to users who may want to do their own tests around our router
14+
- The JSX children `<Route>` objects cause non-intuitive behavior based on idiomatic react expectations
15+
- Conditional runtime `<Route>`'s won't get picked up
16+
- Adding new `<Route>`'s during local dev won't get picked up during HMR
17+
- Using external state in your elements doesn't work as one might expect (see #9225)
18+
19+
Instead, we are going to lift the singleton out into user-land, so that they create the router singleton and manage it outside the react tree - which is what react 18 is encouraging with `useSyncExternalStore` anyways! This also means that since users create the router - there's no longer any difference in the rendering aspect for memory/browser/hash routers (which only impacts router/history creation) - so we can get rid of those and trim to a simple `RouterProvider`
20+
21+
```jsx
22+
// Before
23+
function App() {
24+
<DataBrowserRouter>
25+
<Route path="/" element={<Layout />}>
26+
<Route index element={<Home />}>
27+
</Route>
28+
<DataBrowserRouter>
29+
}
30+
31+
// After
32+
let router = createBrowserRouter([{
33+
path: "/",
34+
element: <Layout />,
35+
children: [{
36+
index: true,
37+
element: <Home />,
38+
}]
39+
}]);
40+
41+
function App() {
42+
return <RouterProvider router={router} />
43+
}
44+
```
45+
46+
If folks still prefer the JSX notation, they can leverage `createRoutesFromElements` (aliased from `createRoutesFromChildren` since they are not "children" in this usage):
47+
48+
```jsx
49+
let routes = createRoutesFromElements(
50+
<Route path="/" element={<Layout />}>
51+
<Route index element={<Home />}>
52+
</Route>
53+
);
54+
let router = createBrowserRouter(routes);
55+
56+
function App() {
57+
return <RouterProvider router={router} />
58+
}
59+
```
60+
61+
And now they can also hook into HMR correctly for router disposal:
62+
63+
```
64+
if (import.meta.hot) {
65+
import.meta.hot.dispose(() => router.dispose());
66+
}
67+
```
68+
69+
And finally since `<RouterProvider>` accepts a router, it makes unit testing easer since you can create a fresh router with each test.
70+
71+
**Removed APIs**
72+
73+
- `<DataMemoryRouter>`
74+
- `<DataBrowserRouter>`
75+
- `<DataHashRouter>`
76+
- `<DataRouterProvider>`
77+
- `<DataRouter>`
78+
79+
**Modified APIs**
80+
81+
- `createMemoryRouter`/`createBrowserRouter`/`createHashRouter` used to live in `@remix-run/router` to prevent devs from needing to create their own `history`. These are now moved to `react-router`/`react-router-dom` and handle the `RouteObject -> AgnosticRouteObject` conversion.
82+
83+
**Added APIs**
84+
85+
- `<RouterProvider>`
86+
- `createRoutesFromElements` (alias of `createRoutesFromChildren`)

.eslintrc

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
{
22
"extends": ["react-app"],
33
"rules": {
4-
"import/first": 0
4+
"import/first": "off",
5+
"@typescript-eslint/consistent-type-imports": "error"
56
},
67
"overrides": [
78
{

examples/data-router/src/app.tsx

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import React from "react";
2+
import {
3+
createBrowserRouter,
4+
createRoutesFromElements,
5+
Route,
6+
RouterProvider,
7+
} from "react-router-dom";
8+
9+
import {
10+
Fallback,
11+
Layout,
12+
homeLoader,
13+
Home,
14+
deferredLoader,
15+
DeferredPage,
16+
deferredChildLoader,
17+
deferredChildAction,
18+
DeferredChild,
19+
todosAction,
20+
todosLoader,
21+
TodosList,
22+
TodosBoundary,
23+
todoLoader,
24+
Todo,
25+
sleep,
26+
AwaitPage,
27+
} from "./routes";
28+
import "./index.css";
29+
30+
let router = createBrowserRouter(
31+
createRoutesFromElements(
32+
<Route path="/" element={<Layout />}>
33+
<Route index loader={homeLoader} element={<Home />} />
34+
<Route path="deferred" loader={deferredLoader} element={<DeferredPage />}>
35+
<Route
36+
path="child"
37+
loader={deferredChildLoader}
38+
action={deferredChildAction}
39+
element={<DeferredChild />}
40+
/>
41+
</Route>
42+
<Route id="await" path="await" element={<AwaitPage />} />
43+
<Route
44+
path="long-load"
45+
loader={() => sleep(3000)}
46+
element={<h1>👋</h1>}
47+
/>
48+
<Route
49+
path="todos"
50+
action={todosAction}
51+
loader={todosLoader}
52+
element={<TodosList />}
53+
errorElement={<TodosBoundary />}
54+
>
55+
<Route path=":id" loader={todoLoader} element={<Todo />} />
56+
</Route>
57+
</Route>
58+
)
59+
);
60+
61+
if (import.meta.hot) {
62+
import.meta.hot.dispose(() => router.dispose());
63+
}
64+
65+
export default function App() {
66+
return <RouterProvider router={router} fallbackElement={<Fallback />} />;
67+
}

examples/data-router/src/main.tsx

+2-54
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,9 @@
11
import React from "react";
22
import ReactDOM from "react-dom/client";
3-
import { DataBrowserRouter, Route } from "react-router-dom";
4-
5-
import {
6-
Fallback,
7-
Layout,
8-
homeLoader,
9-
Home,
10-
deferredLoader,
11-
DeferredPage,
12-
deferredChildLoader,
13-
deferredChildAction,
14-
DeferredChild,
15-
todosAction,
16-
todosLoader,
17-
TodosList,
18-
TodosBoundary,
19-
todoLoader,
20-
Todo,
21-
sleep,
22-
AwaitPage,
23-
} from "./routes";
24-
import "./index.css";
3+
import App from "./app";
254

265
ReactDOM.createRoot(document.getElementById("root")).render(
276
<React.StrictMode>
28-
<DataBrowserRouter fallbackElement={<Fallback />}>
29-
<Route path="/" element={<Layout />}>
30-
<Route index loader={homeLoader} element={<Home />} />
31-
<Route
32-
path="deferred"
33-
loader={deferredLoader}
34-
element={<DeferredPage />}
35-
>
36-
<Route
37-
path="child"
38-
loader={deferredChildLoader}
39-
action={deferredChildAction}
40-
element={<DeferredChild />}
41-
/>
42-
</Route>
43-
<Route id="await" path="await" element={<AwaitPage />} />
44-
<Route
45-
path="long-load"
46-
loader={() => sleep(3000)}
47-
element={<h1>👋</h1>}
48-
/>
49-
<Route
50-
path="todos"
51-
action={todosAction}
52-
loader={todosLoader}
53-
element={<TodosList />}
54-
errorElement={<TodosBoundary />}
55-
>
56-
<Route path=":id" loader={todoLoader} element={<Todo />} />
57-
</Route>
58-
</Route>
59-
</DataBrowserRouter>
7+
<App />
608
</React.StrictMode>
619
);

examples/data-router/src/routes.tsx

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React from "react";
2+
import type { ActionFunctionArgs, LoaderFunctionArgs } from "react-router-dom";
23
import {
34
Await,
45
Form,
@@ -16,8 +17,6 @@ import {
1617
useRouteError,
1718
json,
1819
useActionData,
19-
ActionFunctionArgs,
20-
LoaderFunctionArgs,
2120
} from "react-router-dom";
2221

2322
import type { Todos } from "./todos";

examples/error-boundaries/src/app.tsx

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import React from "react";
2+
import { createBrowserRouter, Outlet, RouterProvider } from "react-router-dom";
3+
4+
import "./index.css";
5+
import {
6+
Fallback,
7+
Layout,
8+
RootErrorBoundary,
9+
Project,
10+
ProjectErrorBoundary,
11+
projectLoader,
12+
} from "./routes";
13+
14+
let router = createBrowserRouter([
15+
{
16+
path: "/",
17+
element: <Layout />,
18+
children: [
19+
{
20+
path: "",
21+
element: <Outlet />,
22+
errorElement: <RootErrorBoundary />,
23+
children: [
24+
{
25+
path: "projects/:projectId",
26+
element: <Project />,
27+
errorElement: <ProjectErrorBoundary />,
28+
loader: projectLoader,
29+
},
30+
],
31+
},
32+
],
33+
},
34+
]);
35+
36+
if (import.meta.hot) {
37+
import.meta.hot.dispose(() => router.dispose());
38+
}
39+
40+
export default function App() {
41+
return <RouterProvider router={router} fallbackElement={<Fallback />} />;
42+
}
+2-26
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,10 @@
11
import React from "react";
22
import ReactDOM from "react-dom/client";
33

4-
import { DataBrowserRouter, Outlet, Route } from "react-router-dom";
5-
import "./index.css";
6-
import {
7-
Fallback,
8-
Layout,
9-
RootErrorBoundary,
10-
Project,
11-
ProjectErrorBoundary,
12-
projectLoader,
13-
} from "./routes";
4+
import App from "./app";
145

156
ReactDOM.createRoot(document.getElementById("root")).render(
167
<React.StrictMode>
17-
<DataBrowserRouter fallbackElement={<Fallback />}>
18-
<Route path="/" element={<Layout />}>
19-
<Route
20-
path=""
21-
element={<Outlet />}
22-
errorElement={<RootErrorBoundary />}
23-
>
24-
<Route
25-
path="projects/:projectId"
26-
element={<Project />}
27-
errorElement={<ProjectErrorBoundary />}
28-
loader={projectLoader}
29-
/>
30-
</Route>
31-
</Route>
32-
</DataBrowserRouter>
8+
<App />
339
</React.StrictMode>
3410
);

examples/error-boundaries/src/routes.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import React from "react";
22

3+
import type { LoaderFunctionArgs } from "react-router-dom";
34
import {
45
isRouteErrorResponse,
56
json,
67
Link,
7-
LoaderFunctionArgs,
88
Outlet,
99
useLoaderData,
1010
useRouteError,

examples/notes/src/app.jsx

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import "./index.css";
2+
import { createBrowserRouter, RouterProvider } from "react-router-dom";
3+
4+
import Root, { loader as rootLoader } from "./routes/root";
5+
import NewNote, { action as newNoteAction } from "./routes/new";
6+
import Note, {
7+
loader as noteLoader,
8+
action as noteAction,
9+
} from "./routes/note";
10+
11+
let router = createBrowserRouter([
12+
{
13+
path: "/",
14+
element: <Root />,
15+
loader: rootLoader,
16+
children: [
17+
{
18+
path: "new",
19+
element: <NewNote />,
20+
action: newNoteAction,
21+
},
22+
{
23+
path: "note/:noteId",
24+
element: <Note />,
25+
loader: noteLoader,
26+
action: noteAction,
27+
errorElement: <h2>Note not found</h2>,
28+
},
29+
],
30+
},
31+
]);
32+
33+
if (import.meta.hot) {
34+
import.meta.hot.dispose(() => router.dispose());
35+
}
36+
37+
export default function App() {
38+
return <RouterProvider router={router} />;
39+
}

0 commit comments

Comments
 (0)