Skip to content

Commit

Permalink
Merge pull request #33 from codeforjapan/feat/react-router-7
Browse files Browse the repository at this point in the history
feat: migrate into React Router 7
  • Loading branch information
yu23ki14 authored Mar 9, 2025
2 parents 896e0b1 + 60052b0 commit 39bca07
Show file tree
Hide file tree
Showing 16 changed files with 288 additions and 2,443 deletions.
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";

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

0 comments on commit 39bca07

Please sign in to comment.