Skip to content

Commit 39bca07

Browse files
authored
Merge pull request #33 from codeforjapan/feat/react-router-7
feat: migrate into React Router 7
2 parents 896e0b1 + 60052b0 commit 39bca07

16 files changed

+288
-2443
lines changed

.github/dependabot.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ updates:
3232
- 'patch'
3333
react-router:
3434
patterns:
35-
# TODO: add react-router patterns
36-
- '@remix-run*'
35+
- 'react-router*'
36+
- '@react-router*'
3737
mantine:
3838
patterns:
3939
- '@mantine*'

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@ node_modules
77
.vscode/*
88
!.vscode/settings.example.json
99
eslint-typegen.d.ts
10+
11+
.react-router/*

app/entry.client.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
* For more information, see https://remix.run/file-conventions/entry.client
55
*/
66

7-
import { RemixBrowser } from "@remix-run/react";
87
import { startTransition, StrictMode } from "react";
98
import { hydrateRoot } from "react-dom/client";
9+
import { HydratedRouter } from "react-router/dom";
1010

1111
startTransition(() => {
1212
hydrateRoot(
1313
document,
1414
<StrictMode>
15-
<RemixBrowser />
15+
<HydratedRouter />
1616
</StrictMode>,
1717
);
1818
});

app/entry.server.tsx

Lines changed: 13 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -1,142 +1,19 @@
1-
/**
2-
* By default, Remix will handle generating the HTTP Response for you.
3-
* You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
4-
* For more information, see https://remix.run/file-conventions/entry.server
5-
*/
6-
7-
import { PassThrough } from "node:stream";
8-
9-
import type { AppLoadContext, EntryContext } from "@remix-run/node";
10-
import { createReadableStreamFromReadable } from "@remix-run/node";
11-
import { RemixServer } from "@remix-run/react";
12-
import { isbot } from "isbot";
13-
import { renderToPipeableStream } from "react-dom/server";
14-
15-
const ABORT_DELAY = 5_000;
1+
import { handleRequest as vercelHandleRequest } from "@vercel/react-router/entry.server";
2+
import type { AppLoadContext, EntryContext } from "react-router";
163

174
export default async function handleRequest(
185
request: Request,
196
responseStatusCode: number,
207
responseHeaders: Headers,
21-
remixContext: EntryContext,
22-
// This is ignored so we can keep it in the template for visibility. Feel
23-
// free to delete this parameter in your app if you're not using it!
24-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
25-
loadContext: AppLoadContext,
26-
) {
27-
return isbot(request.headers.get("user-agent") ?? "")
28-
? handleBotRequest(
29-
request,
30-
responseStatusCode,
31-
responseHeaders,
32-
remixContext,
33-
)
34-
: handleBrowserRequest(
35-
request,
36-
responseStatusCode,
37-
responseHeaders,
38-
remixContext,
39-
);
40-
}
41-
42-
async function handleBotRequest(
43-
request: Request,
44-
responseStatusCode: number,
45-
responseHeaders: Headers,
46-
remixContext: EntryContext,
47-
) {
48-
return new Promise((resolve, reject) => {
49-
let shellRendered = false;
50-
const { pipe, abort } = renderToPipeableStream(
51-
<RemixServer
52-
abortDelay={ABORT_DELAY}
53-
context={remixContext}
54-
url={request.url}
55-
/>,
56-
{
57-
onAllReady() {
58-
shellRendered = true;
59-
const body = new PassThrough();
60-
const stream = createReadableStreamFromReadable(body);
61-
62-
responseHeaders.set("Content-Type", "text/html");
63-
64-
resolve(
65-
new Response(stream, {
66-
headers: responseHeaders,
67-
status: responseStatusCode,
68-
}),
69-
);
70-
71-
pipe(body);
72-
},
73-
onShellError(error: unknown) {
74-
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
75-
reject(error);
76-
},
77-
onError(error: unknown) {
78-
responseStatusCode = 500;
79-
// Log streaming rendering errors from inside the shell. Don't log
80-
// errors encountered during initial shell rendering since they'll
81-
// reject and get logged in handleDocumentRequest.
82-
if (shellRendered) {
83-
console.error(error);
84-
}
85-
},
86-
},
87-
);
88-
89-
setTimeout(abort, ABORT_DELAY);
90-
});
91-
}
92-
93-
async function handleBrowserRequest(
94-
request: Request,
95-
responseStatusCode: number,
96-
responseHeaders: Headers,
97-
remixContext: EntryContext,
98-
) {
99-
return new Promise((resolve, reject) => {
100-
let shellRendered = false;
101-
const { pipe, abort } = renderToPipeableStream(
102-
<RemixServer
103-
abortDelay={ABORT_DELAY}
104-
context={remixContext}
105-
url={request.url}
106-
/>,
107-
{
108-
onShellReady() {
109-
shellRendered = true;
110-
const body = new PassThrough();
111-
const stream = createReadableStreamFromReadable(body);
112-
113-
responseHeaders.set("Content-Type", "text/html");
114-
115-
resolve(
116-
new Response(stream, {
117-
headers: responseHeaders,
118-
status: responseStatusCode,
119-
}),
120-
);
121-
122-
pipe(body);
123-
},
124-
onShellError(error: unknown) {
125-
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
126-
reject(error);
127-
},
128-
onError(error: unknown) {
129-
responseStatusCode = 500;
130-
// Log streaming rendering errors from inside the shell. Don't log
131-
// errors encountered during initial shell rendering since they'll
132-
// reject and get logged in handleDocumentRequest.
133-
if (shellRendered) {
134-
console.error(error);
135-
}
136-
},
137-
},
138-
);
139-
140-
setTimeout(abort, ABORT_DELAY);
141-
});
8+
routerContext: EntryContext,
9+
loadContext?: AppLoadContext,
10+
): Promise<Response> {
11+
const response = await vercelHandleRequest(
12+
request,
13+
responseStatusCode,
14+
responseHeaders,
15+
routerContext,
16+
loadContext,
17+
);
18+
return response;
14219
}

app/feature/search/components/AdvancedSearchForm.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
TagsInput,
1111
Text,
1212
} from "@mantine/core";
13-
import { Form, useNavigation } from "@remix-run/react";
13+
import { Form, useNavigation } from "react-router";
1414

1515
import { FormError } from "../../../components/FormError";
1616
import { DateRangePicker } from "../../../components/input/DateRangePicker";

app/feature/search/components/SearchForm.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
UnstyledButton,
88
} from "@mantine/core";
99
import { useDisclosure } from "@mantine/hooks";
10-
import { Form, useNavigation } from "@remix-run/react";
10+
import { Form, useNavigation } from "react-router";
1111

1212
import { FormError } from "../../../components/FormError";
1313
import { DateRangePicker } from "../../../components/input/DateRangePicker";

app/feature/search/components/SearchPagination.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { GroupProps } from "@mantine/core";
22
import { ActionIcon, Group, Text } from "@mantine/core";
3-
import { Link } from "@remix-run/react";
43
import { useCallback, useMemo, useState } from "react";
4+
import { Link } from "react-router";
55
import { getQuery, withQuery } from "ufo";
66
import type { z } from "zod";
77

app/root.tsx

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,9 @@ import "./app.css";
55

66
import { ColorSchemeScript, MantineProvider } from "@mantine/core";
77
import { DatesProvider } from "@mantine/dates";
8-
import {
9-
Links,
10-
Meta,
11-
Outlet,
12-
Scripts,
13-
ScrollRestoration,
14-
} from "@remix-run/react";
158
import dayjs from "dayjs";
169
import customParseFormat from "dayjs/plugin/customParseFormat";
10+
import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";
1711

1812
import { Favicons } from "./components/Favicon";
1913
import { mantineTheme } from "./config/mantine";

app/routes.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { flatRoutes } from "@react-router/fs-routes";
2+
3+
export default flatRoutes();

app/routes/_index.tsx

Lines changed: 24 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,6 @@
11
import { parseWithZod } from "@conform-to/zod";
22
import { Anchor, Card, Container, Divider, Group, Stack } from "@mantine/core";
3-
import type {
4-
ActionFunctionArgs,
5-
LinksFunction,
6-
LoaderFunctionArgs,
7-
MetaFunction,
8-
} from "@remix-run/node";
9-
import {
10-
data,
11-
Link,
12-
redirect,
13-
useActionData,
14-
useLoaderData,
15-
useNavigation,
16-
} from "@remix-run/react";
3+
import { data, Link, redirect, useNavigation } from "react-router";
174
import { getQuery, withQuery } from "ufo";
185

196
import Fa6SolidMagnifyingGlass from "~icons/fa6-solid/magnifying-glass";
@@ -26,8 +13,10 @@ import {
2613
getTopicsApiV1DataTopicsGet,
2714
searchApiV1DataSearchGet,
2815
} from "../generated/api/client";
16+
import type { SearchedNote, Topic } from "../generated/api/schemas";
17+
import type { Route } from "./+types/_index";
2918

30-
export const meta: MetaFunction = () => {
19+
export const meta: Route.MetaFunction = () => {
3120
return [
3221
{ title: "BirdXplorer" },
3322
{
@@ -42,7 +31,7 @@ export const meta: MetaFunction = () => {
4231
];
4332
};
4433

45-
export const links: LinksFunction = () => {
34+
export const links: Route.LinksFunction = () => {
4635
return [
4736
{
4837
rel: "canonical",
@@ -51,7 +40,7 @@ export const links: LinksFunction = () => {
5140
];
5241
};
5342

54-
export const loader = async (args: LoaderFunctionArgs) => {
43+
export const loader = async (args: Route.LoaderArgs) => {
5544
const rawSearchParams = getQuery(args.request.url);
5645
const searchQuery =
5746
await noteSearchParamSchema.safeParseAsync(rawSearchParams);
@@ -95,17 +84,17 @@ export const loader = async (args: LoaderFunctionArgs) => {
9584
};
9685
};
9786

98-
export default function Index() {
99-
const { data } = useLoaderData<typeof loader>();
100-
const lastResult = useActionData<typeof action>();
87+
export default function Index({
88+
actionData,
89+
loaderData,
90+
}: Route.ComponentProps) {
91+
const isLoadingSearchResults = useNavigation().state !== "idle";
10192

10293
const {
10394
topics,
10495
searchQuery,
10596
searchResults: { data: notes, meta: paginationMeta },
106-
} = data;
107-
108-
const isLoadingSearchResults = useNavigation().state !== "idle";
97+
} = loaderData.data;
10998

11099
return (
111100
<>
@@ -121,8 +110,11 @@ export default function Index() {
121110
<h2 className="sr-only">コミュニティノートを検索する</h2>
122111
<SearchForm
123112
defaultValue={searchQuery ?? undefined}
124-
lastResult={lastResult}
125-
topics={topics}
113+
lastResult={actionData}
114+
topics={
115+
// react-router の型がうまく機能せず topics が unknown になったため
116+
topics as Topic[]
117+
}
126118
/>
127119
</div>
128120
<Divider className="md:hidden" />
@@ -141,7 +133,12 @@ export default function Index() {
141133
/>
142134
)}
143135
<Group gap="lg">
144-
<Notes notes={notes} />
136+
<Notes
137+
notes={
138+
// react-router の型がうまく機能せず notes[number].topics が unknown になったため
139+
notes as SearchedNote[]
140+
}
141+
/>
145142
</Group>
146143
{searchQuery && (
147144
<SearchPagination
@@ -195,7 +192,7 @@ export default function Index() {
195192
);
196193
}
197194

198-
export const action = async ({ request }: ActionFunctionArgs) => {
195+
export const action = async ({ request }: Route.ActionArgs) => {
199196
const formData = await request.formData();
200197
const submission = parseWithZod(formData, {
201198
schema: noteSearchParamSchema,

0 commit comments

Comments
 (0)