Skip to content

Commit 158ed99

Browse files
DennisKraaijeveldkentcdoddshakimLyon
authored
Feat/migrate to react router 7 (#897)
Co-authored-by: Kent C. Dodds <me@kentcdodds.com> Co-authored-by: Hakim Gueye <beatsjordy@gmail.com> Co-authored-by: Hakim Gueye <64654633+hakimLyon@users.noreply.github.com> Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
1 parent b667696 commit 158ed99

File tree

83 files changed

+10158
-15451
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

83 files changed

+10158
-15451
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,4 @@ node_modules
2323

2424
# generated files
2525
/app/components/ui/icons
26+
.react-router/

app/components/error-boundary.tsx

+8-5
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1+
import { captureException } from '@sentry/react'
2+
import { useEffect, type ReactElement } from 'react'
13
import {
24
type ErrorResponse,
35
isRouteErrorResponse,
46
useParams,
57
useRouteError,
6-
} from '@remix-run/react'
7-
import { captureRemixErrorBoundaryError } from '@sentry/remix'
8-
import { type ReactElement } from 'react'
9-
import { getErrorMessage } from '#app/utils/misc.tsx'
8+
} from 'react-router'
9+
import { getErrorMessage } from '#app/utils/misc'
1010

1111
type StatusHandler = (info: {
1212
error: ErrorResponse
@@ -27,13 +27,16 @@ export function GeneralErrorBoundary({
2727
unexpectedErrorHandler?: (error: unknown) => ReactElement | null
2828
}) {
2929
const error = useRouteError()
30-
captureRemixErrorBoundaryError(error)
3130
const params = useParams()
3231

3332
if (typeof document !== 'undefined') {
3433
console.error(error)
3534
}
3635

36+
useEffect(() => {
37+
captureException(error)
38+
}, [error])
39+
3740
return (
3841
<div className="container flex items-center justify-center p-20 text-h2">
3942
{isRouteErrorResponse(error)

app/components/progress-bar.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useNavigation } from '@remix-run/react'
1+
import { useNavigation } from 'react-router'
22
import { useEffect, useRef, useState } from 'react'
33
import { useSpinDelay } from 'spin-delay'
44
import { cn } from '#app/utils/misc.tsx'

app/components/search-bar.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { Form, useSearchParams, useSubmit } from '@remix-run/react'
21
import { useId } from 'react'
2+
import { Form, useSearchParams, useSubmit } from 'react-router'
33
import { useDebounce, useIsPending } from '#app/utils/misc.tsx'
44
import { Icon } from './ui/icon.tsx'
55
import { Input } from './ui/input.tsx'
@@ -23,8 +23,8 @@ export function SearchBar({
2323
formAction: '/users',
2424
})
2525

26-
const handleFormChange = useDebounce((form: HTMLFormElement) => {
27-
submit(form)
26+
const handleFormChange = useDebounce(async (form: HTMLFormElement) => {
27+
await submit(form)
2828
}, 400)
2929

3030
return (

app/entry.client.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { RemixBrowser } from '@remix-run/react'
21
import { startTransition } from 'react'
32
import { hydrateRoot } from 'react-dom/client'
3+
import { HydratedRouter } from 'react-router/dom'
44

55
if (ENV.MODE === 'production' && ENV.SENTRY_DSN) {
66
void import('./utils/monitoring.client.tsx').then(({ init }) => init())
77
}
88

99
startTransition(() => {
10-
hydrateRoot(document, <RemixBrowser />)
10+
hydrateRoot(document, <HydratedRouter />)
1111
})

app/entry.server.tsx

+17-17
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
11
import { PassThrough } from 'node:stream'
2+
import { createReadableStreamFromReadable } from '@react-router/node'
3+
4+
import * as Sentry from '@sentry/node'
5+
import chalk from 'chalk'
6+
import { isbot } from 'isbot'
7+
import { renderToPipeableStream } from 'react-dom/server'
28
import {
3-
createReadableStreamFromReadable,
9+
ServerRouter,
410
type LoaderFunctionArgs,
511
type ActionFunctionArgs,
612
type HandleDocumentRequestFunction,
7-
} from '@remix-run/node'
8-
import { RemixServer } from '@remix-run/react'
9-
import * as Sentry from '@sentry/remix'
10-
import chalk from 'chalk'
11-
import { isbot } from 'isbot'
12-
import { renderToPipeableStream } from 'react-dom/server'
13+
} from 'react-router'
1314
import { getEnv, init } from './utils/env.server.ts'
1415
import { getInstanceInfo } from './utils/litefs.server.ts'
1516
import { NonceProvider } from './utils/nonce-provider.ts'
1617
import { makeTimings } from './utils/timing.server.ts'
1718

18-
const ABORT_DELAY = 5000
19+
export const streamTimeout = 5000
1920

2021
init()
2122
global.ENV = getEnv()
@@ -27,7 +28,7 @@ export default async function handleRequest(...args: DocRequestArgs) {
2728
request,
2829
responseStatusCode,
2930
responseHeaders,
30-
remixContext,
31+
reactRouterContext,
3132
loadContext,
3233
] = args
3334
const { currentInstance, primaryInstance } = await getInstanceInfo()
@@ -53,7 +54,11 @@ export default async function handleRequest(...args: DocRequestArgs) {
5354

5455
const { pipe, abort } = renderToPipeableStream(
5556
<NonceProvider value={nonce}>
56-
<RemixServer context={remixContext} url={request.url} />
57+
<ServerRouter
58+
nonce={nonce}
59+
context={reactRouterContext}
60+
url={request.url}
61+
/>
5762
</NonceProvider>,
5863
{
5964
[callbackName]: () => {
@@ -78,7 +83,7 @@ export default async function handleRequest(...args: DocRequestArgs) {
7883
},
7984
)
8085

81-
setTimeout(abort, ABORT_DELAY)
86+
setTimeout(abort, streamTimeout + 5000)
8287
})
8388
}
8489

@@ -103,12 +108,7 @@ export function handleError(
103108
}
104109
if (error instanceof Error) {
105110
console.error(chalk.red(error.stack))
106-
void Sentry.captureRemixServerException(
107-
error,
108-
'remix.server',
109-
request,
110-
true,
111-
)
111+
void Sentry.captureException(error)
112112
} else {
113113
console.error(error)
114114
Sentry.captureException(error)

app/root.tsx

+10-11
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1+
import { wrapUseRoutesV7 } from '@sentry/react'
2+
import { useRef } from 'react'
13
import {
2-
json,
4+
data,
35
type LoaderFunctionArgs,
46
type HeadersFunction,
57
type LinksFunction,
68
type MetaFunction,
7-
} from '@remix-run/node'
8-
import {
99
Form,
1010
Link,
1111
Links,
@@ -16,9 +16,7 @@ import {
1616
useLoaderData,
1717
useMatches,
1818
useSubmit,
19-
} from '@remix-run/react'
20-
import { withSentry } from '@sentry/remix'
21-
import { useRef } from 'react'
19+
} from 'react-router'
2220
import { HoneypotProvider } from 'remix-utils/honeypot/react'
2321
import appleTouchIconAssetUrl from './assets/favicons/apple-touch-icon.png'
2422
import faviconAssetUrl from './assets/favicons/favicon.svg'
@@ -121,7 +119,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
121119
const { toast, headers: toastHeaders } = await getToast(request)
122120
const honeyProps = honeypot.getInputProps()
123121

124-
return json(
122+
return data(
125123
{
126124
user,
127125
requestInfo: {
@@ -161,7 +159,8 @@ function Document({
161159
children: React.ReactNode
162160
nonce: string
163161
theme?: Theme
164-
env?: Record<string, string>
162+
env?: Record<string, string | undefined>
163+
allowIndexing?: boolean
165164
}) {
166165
const allowIndexing = ENV.ALLOW_INDEXING !== 'false'
167166
return (
@@ -271,7 +270,7 @@ function AppWithProviders() {
271270
)
272271
}
273272

274-
export default withSentry(AppWithProviders)
273+
export default wrapUseRoutesV7(AppWithProviders)
275274

276275
function UserDropdown() {
277276
const user = useUser()
@@ -317,9 +316,9 @@ function UserDropdown() {
317316
<DropdownMenuItem
318317
asChild
319318
// this prevents the menu from closing before the form submission is completed
320-
onSelect={(event) => {
319+
onSelect={async (event) => {
321320
event.preventDefault()
322-
submit(formRef.current)
321+
await submit(formRef.current)
323322
}}
324323
>
325324
<Form action="/logout" method="POST" ref={formRef}>

app/routes.ts

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { type RouteConfig } from '@react-router/dev/routes'
2+
import { remixRoutesOptionAdapter } from '@react-router/remix-routes-option-adapter'
3+
import { flatRoutes } from 'remix-flat-routes'
4+
5+
export default remixRoutesOptionAdapter((defineRoutes) => {
6+
return flatRoutes('routes', defineRoutes, {
7+
ignoredRouteFiles: [
8+
'.*',
9+
'**/*.css',
10+
'**/*.test.{js,jsx,ts,tsx}',
11+
'**/__*.*',
12+
// This is for server-side utilities you want to colocate
13+
// next to your routes without making an additional
14+
// directory. If you need a route that includes "server" or
15+
// "client" in the filename, use the escape brackets like:
16+
// my-route.[server].tsx
17+
'**/*.server.*',
18+
'**/*.client.*',
19+
],
20+
})
21+
}) satisfies RouteConfig

app/routes/$.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
// ensure the user gets the right status code and we can display a nicer error
66
// message for them than the Remix and/or browser default.
77

8-
import { Link, useLocation } from '@remix-run/react'
8+
import { Link, useLocation } from 'react-router'
99
import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
1010
import { Icon } from '#app/components/ui/icon.tsx'
1111

app/routes/_auth+/auth.$provider.callback.ts

+11-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { redirect, type LoaderFunctionArgs } from '@remix-run/node'
1+
import { redirect, type LoaderFunctionArgs } from 'react-router'
22
import {
33
authenticator,
44
getSessionExpirationDate,
@@ -39,8 +39,16 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
3939
const authResult = await authenticator
4040
.authenticate(providerName, request, { throwOnError: true })
4141
.then(
42-
(data) => ({ success: true, data }) as const,
43-
(error) => ({ success: false, error }) as const,
42+
(data) =>
43+
({
44+
success: true,
45+
data,
46+
}) as const,
47+
(error) =>
48+
({
49+
success: false,
50+
error,
51+
}) as const,
4452
)
4553

4654
if (!authResult.success) {

app/routes/_auth+/auth.$provider.ts renamed to app/routes/_auth+/auth_.$provider.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { redirect, type ActionFunctionArgs } from '@remix-run/node'
1+
import { redirect, type ActionFunctionArgs } from 'react-router'
22
import { authenticator } from '#app/utils/auth.server.ts'
33
import { handleMockAction } from '#app/utils/connections.server.ts'
44
import { ProviderNameSchema } from '#app/utils/connections.tsx'

app/routes/_auth+/forgot-password.tsx

+6-5
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ import { getZodConstraint, parseWithZod } from '@conform-to/zod'
33
import { type SEOHandle } from '@nasa-gcn/remix-seo'
44
import * as E from '@react-email/components'
55
import {
6-
json,
6+
data,
77
redirect,
88
type ActionFunctionArgs,
99
type MetaFunction,
10-
} from '@remix-run/node'
11-
import { Link, useFetcher } from '@remix-run/react'
10+
Link,
11+
useFetcher,
12+
} from 'react-router'
1213
import { HoneypotInputs } from 'remix-utils/honeypot/react'
1314
import { z } from 'zod'
1415
import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
@@ -54,7 +55,7 @@ export async function action({ request }: ActionFunctionArgs) {
5455
async: true,
5556
})
5657
if (submission.status !== 'success') {
57-
return json(
58+
return data(
5859
{ result: submission.reply() },
5960
{ status: submission.status === 'error' ? 400 : 200 },
6061
)
@@ -84,7 +85,7 @@ export async function action({ request }: ActionFunctionArgs) {
8485
if (response.status === 'success') {
8586
return redirect(redirectTo.toString())
8687
} else {
87-
return json(
88+
return data(
8889
{ result: submission.reply({ formErrors: [response.error.message] }) },
8990
{ status: 500 },
9091
)

app/routes/_auth+/login.server.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { invariant } from '@epic-web/invariant'
2-
import { redirect } from '@remix-run/node'
2+
import { redirect } from 'react-router'
33
import { safeRedirect } from 'remix-utils/safe-redirect'
44
import { twoFAVerificationType } from '#app/routes/settings+/profile.two-factor.tsx'
55
import { getUserId, sessionKey } from '#app/utils/auth.server.ts'

app/routes/_auth+/login.tsx

+8-5
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@ import { getFormProps, getInputProps, useForm } from '@conform-to/react'
22
import { getZodConstraint, parseWithZod } from '@conform-to/zod'
33
import { type SEOHandle } from '@nasa-gcn/remix-seo'
44
import {
5-
json,
5+
data,
66
type ActionFunctionArgs,
77
type LoaderFunctionArgs,
88
type MetaFunction,
9-
} from '@remix-run/node'
10-
import { Form, Link, useActionData, useSearchParams } from '@remix-run/react'
9+
Form,
10+
Link,
11+
useActionData,
12+
useSearchParams,
13+
} from 'react-router'
1114
import { HoneypotInputs } from 'remix-utils/honeypot/react'
1215
import { z } from 'zod'
1316
import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
@@ -37,7 +40,7 @@ const LoginFormSchema = z.object({
3740

3841
export async function loader({ request }: LoaderFunctionArgs) {
3942
await requireAnonymous(request)
40-
return json({})
43+
return {}
4144
}
4245

4346
export async function action({ request }: ActionFunctionArgs) {
@@ -64,7 +67,7 @@ export async function action({ request }: ActionFunctionArgs) {
6467
})
6568

6669
if (submission.status !== 'success' || !submission.value.session) {
67-
return json(
70+
return data(
6871
{ result: submission.reply({ hideFields: ['password'] }) },
6972
{ status: submission.status === 'error' ? 400 : 200 },
7073
)

app/routes/_auth+/logout.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { redirect, type ActionFunctionArgs } from '@remix-run/node'
1+
import { redirect, type ActionFunctionArgs } from 'react-router'
22
import { logout } from '#app/utils/auth.server.ts'
33

44
export async function loader() {

app/routes/_auth+/onboarding.server.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { invariant } from '@epic-web/invariant'
2-
import { redirect } from '@remix-run/node'
2+
import { redirect } from 'react-router'
33
import { verifySessionStorage } from '#app/utils/verification.server.ts'
44
import { onboardingEmailSessionKey } from './onboarding.tsx'
55
import { type VerifyFunctionArgs } from './verify.server.ts'

0 commit comments

Comments
 (0)