Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: migrate into React Router 7 #33

Merged
merged 12 commits into from
Mar 9, 2025
4 changes: 2 additions & 2 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ updates:
- 'patch'
react-router:
patterns:
# TODO: add react-router patterns
- '@remix-run*'
- 'react-router*'
- '@react-router*'
mantine:
patterns:
- '@mantine*'
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ node_modules
.vscode/*
!.vscode/settings.example.json
eslint-typegen.d.ts

.react-router/*
4 changes: 2 additions & 2 deletions app/entry.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
* For more information, see https://remix.run/file-conventions/entry.client
*/

import { RemixBrowser } from "@remix-run/react";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import { HydratedRouter } from "react-router/dom";

startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<RemixBrowser />
<HydratedRouter />
</StrictMode>,
);
});
149 changes: 13 additions & 136 deletions app/entry.server.tsx
Original file line number Diff line number Diff line change
@@ -1,142 +1,19 @@
/**
* By default, Remix will handle generating the HTTP Response for you.
* 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` ✨
* For more information, see https://remix.run/file-conventions/entry.server
*/

import { PassThrough } from "node:stream";

import type { AppLoadContext, EntryContext } from "@remix-run/node";
import { createReadableStreamFromReadable } from "@remix-run/node";
import { RemixServer } from "@remix-run/react";
import { isbot } from "isbot";
import { renderToPipeableStream } from "react-dom/server";

const ABORT_DELAY = 5_000;
import { handleRequest as vercelHandleRequest } from "@vercel/react-router/entry.server";
import type { AppLoadContext, EntryContext } from "react-router";
Copy link
Collaborator Author

@sushichan044 sushichan044 Mar 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

現状の実装とほぼ同じことを Vercel のプリセットがやっていたので実装を差し替え
https://github.com/vercel/vercel/blob/main/packages/react-router/entry.server.ts


export default async function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
// This is ignored so we can keep it in the template for visibility. Feel
// free to delete this parameter in your app if you're not using it!
// eslint-disable-next-line @typescript-eslint/no-unused-vars
loadContext: AppLoadContext,
) {
return isbot(request.headers.get("user-agent") ?? "")
? handleBotRequest(
request,
responseStatusCode,
responseHeaders,
remixContext,
)
: handleBrowserRequest(
request,
responseStatusCode,
responseHeaders,
remixContext,
);
}

async function handleBotRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
) {
return new Promise((resolve, reject) => {
let shellRendered = false;
const { pipe, abort } = renderToPipeableStream(
<RemixServer
abortDelay={ABORT_DELAY}
context={remixContext}
url={request.url}
/>,
{
onAllReady() {
shellRendered = true;
const body = new PassThrough();
const stream = createReadableStreamFromReadable(body);

responseHeaders.set("Content-Type", "text/html");

resolve(
new Response(stream, {
headers: responseHeaders,
status: responseStatusCode,
}),
);

pipe(body);
},
onShellError(error: unknown) {
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
reject(error);
},
onError(error: unknown) {
responseStatusCode = 500;
// Log streaming rendering errors from inside the shell. Don't log
// errors encountered during initial shell rendering since they'll
// reject and get logged in handleDocumentRequest.
if (shellRendered) {
console.error(error);
}
},
},
);

setTimeout(abort, ABORT_DELAY);
});
}

async function handleBrowserRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
) {
return new Promise((resolve, reject) => {
let shellRendered = false;
const { pipe, abort } = renderToPipeableStream(
<RemixServer
abortDelay={ABORT_DELAY}
context={remixContext}
url={request.url}
/>,
{
onShellReady() {
shellRendered = true;
const body = new PassThrough();
const stream = createReadableStreamFromReadable(body);

responseHeaders.set("Content-Type", "text/html");

resolve(
new Response(stream, {
headers: responseHeaders,
status: responseStatusCode,
}),
);

pipe(body);
},
onShellError(error: unknown) {
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
reject(error);
},
onError(error: unknown) {
responseStatusCode = 500;
// Log streaming rendering errors from inside the shell. Don't log
// errors encountered during initial shell rendering since they'll
// reject and get logged in handleDocumentRequest.
if (shellRendered) {
console.error(error);
}
},
},
);

setTimeout(abort, ABORT_DELAY);
});
routerContext: EntryContext,
loadContext?: AppLoadContext,
): Promise<Response> {
const response = await vercelHandleRequest(
request,
responseStatusCode,
responseHeaders,
routerContext,
loadContext,
);
return response;
}
2 changes: 1 addition & 1 deletion app/feature/search/components/AdvancedSearchForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
TagsInput,
Text,
} from "@mantine/core";
import { Form, useNavigation } from "@remix-run/react";
import { Form, useNavigation } from "react-router";

import { FormError } from "../../../components/FormError";
import { DateRangePicker } from "../../../components/input/DateRangePicker";
Expand Down
2 changes: 1 addition & 1 deletion app/feature/search/components/SearchForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
UnstyledButton,
} from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { Form, useNavigation } from "@remix-run/react";
import { Form, useNavigation } from "react-router";

import { FormError } from "../../../components/FormError";
import { DateRangePicker } from "../../../components/input/DateRangePicker";
Expand Down
2 changes: 1 addition & 1 deletion app/feature/search/components/SearchPagination.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { GroupProps } from "@mantine/core";
import { ActionIcon, Group, Text } from "@mantine/core";
import { Link } from "@remix-run/react";
import { useCallback, useMemo, useState } from "react";
import { Link } from "react-router";
import { getQuery, withQuery } from "ufo";
import type { z } from "zod";

Expand Down
8 changes: 1 addition & 7 deletions app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,9 @@ import "./app.css";

import { ColorSchemeScript, MantineProvider } from "@mantine/core";
import { DatesProvider } from "@mantine/dates";
import {
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "@remix-run/react";
import dayjs from "dayjs";
import customParseFormat from "dayjs/plugin/customParseFormat";
import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";

import { Favicons } from "./components/Favicon";
import { mantineTheme } from "./config/mantine";
Expand Down
3 changes: 3 additions & 0 deletions app/routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { flatRoutes } from "@react-router/fs-routes";

export default flatRoutes();
51 changes: 24 additions & 27 deletions app/routes/_index.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,6 @@
import { parseWithZod } from "@conform-to/zod";
import { Anchor, Card, Container, Divider, Group, Stack } from "@mantine/core";
import type {
ActionFunctionArgs,
LinksFunction,
LoaderFunctionArgs,
MetaFunction,
} from "@remix-run/node";
import {
data,
Link,
redirect,
useActionData,
useLoaderData,
useNavigation,
} from "@remix-run/react";
import { data, Link, redirect, useNavigation } from "react-router";
import { getQuery, withQuery } from "ufo";

import Fa6SolidMagnifyingGlass from "~icons/fa6-solid/magnifying-glass";
Expand All @@ -26,8 +13,10 @@ import {
getTopicsApiV1DataTopicsGet,
searchApiV1DataSearchGet,
} from "../generated/api/client";
import type { SearchedNote, Topic } from "../generated/api/schemas";
import type { Route } from "./+types/_index";

export const meta: MetaFunction = () => {
export const meta: Route.MetaFunction = () => {
return [
{ title: "BirdXplorer" },
{
Expand All @@ -42,7 +31,7 @@ export const meta: MetaFunction = () => {
];
};

export const links: LinksFunction = () => {
export const links: Route.LinksFunction = () => {
return [
{
rel: "canonical",
Expand All @@ -51,7 +40,7 @@ export const links: LinksFunction = () => {
];
};

export const loader = async (args: LoaderFunctionArgs) => {
export const loader = async (args: Route.LoaderArgs) => {
const rawSearchParams = getQuery(args.request.url);
const searchQuery =
await noteSearchParamSchema.safeParseAsync(rawSearchParams);
Expand Down Expand Up @@ -95,17 +84,17 @@ export const loader = async (args: LoaderFunctionArgs) => {
};
};

export default function Index() {
const { data } = useLoaderData<typeof loader>();
const lastResult = useActionData<typeof action>();
export default function Index({
actionData,
loaderData,
}: Route.ComponentProps) {
const isLoadingSearchResults = useNavigation().state !== "idle";

const {
topics,
searchQuery,
searchResults: { data: notes, meta: paginationMeta },
} = data;

const isLoadingSearchResults = useNavigation().state !== "idle";
} = loaderData.data;

return (
<>
Expand All @@ -121,8 +110,11 @@ export default function Index() {
<h2 className="sr-only">コミュニティノートを検索する</h2>
<SearchForm
defaultValue={searchQuery ?? undefined}
lastResult={lastResult}
topics={topics}
lastResult={actionData}
topics={
// react-router の型がうまく機能せず topics が unknown になったため
topics as Topic[]
}
/>
</div>
<Divider className="md:hidden" />
Expand All @@ -141,7 +133,12 @@ export default function Index() {
/>
)}
<Group gap="lg">
<Notes notes={notes} />
<Notes
notes={
// react-router の型がうまく機能せず notes[number].topics が unknown になったため
notes as SearchedNote[]
}
/>
</Group>
{searchQuery && (
<SearchPagination
Expand Down Expand Up @@ -195,7 +192,7 @@ export default function Index() {
);
}

export const action = async ({ request }: ActionFunctionArgs) => {
export const action = async ({ request }: Route.ActionArgs) => {
const formData = await request.formData();
const submission = parseWithZod(formData, {
schema: noteSearchParamSchema,
Expand Down
Loading