Skip to content

Commit

Permalink
Use new RRv7 data loading + type safety features
Browse files Browse the repository at this point in the history
  • Loading branch information
gustavoguichard committed Dec 3, 2024
1 parent 5f933a3 commit 2e0e69d
Show file tree
Hide file tree
Showing 5 changed files with 47 additions and 56 deletions.
26 changes: 8 additions & 18 deletions examples/react-router/app/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Cookie, TypedResponse } from 'react-router';
import { data } from 'react-router';
import type { Cookie } from 'react-router'
import { data } from 'react-router'
import type { Result } from 'composable-functions'
import { catchFailure, serialize, fromSuccess } from 'composable-functions'

Expand All @@ -22,20 +22,10 @@ const safeReadCookie = catchFailure(

const ctxFromCookie = fromSuccess(safeReadCookie)

const actionResponse = <X>(result: Result<X>, opts?: RequestInit): Result<X> =>
data(serialize(result), { status: result.success ? 200 : 422, ...opts }) as unknown as Result<X>
const actionResponse = <X>(result: Result<X>, opts?: RequestInit): Result<X> =>
data(serialize(result), {
status: result.success ? 200 : 422,
...opts,
}) as unknown as Result<X>

const loaderResponseOrThrow = <T extends Result<unknown>>(
result: T,
opts?: RequestInit,
): T extends { data: infer X } ? TypedResponse<X> : never => {
if (!result.success) {
throw new Response(result.errors[0].message ?? 'Internal server error', {
status: 500,
})
}

return data(result.data, { status: 200, ...opts }) as never
}

export { ctxFromCookie, actionResponse, loaderResponseOrThrow }
export { ctxFromCookie, actionResponse }
28 changes: 13 additions & 15 deletions examples/react-router/app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,18 @@ import {
Outlet,
Scripts,
ScrollRestoration,
useActionData,
useLoaderData,
useRouteError,
} from 'react-router';
import type { ActionFunctionArgs, LinksFunction, LoaderFunctionArgs } from 'react-router';
} from 'react-router'

import styles from './tailwind.css?url'

import { actionResponse, ctxFromCookie, loaderResponseOrThrow } from '~/lib'
import { actionResponse, ctxFromCookie } from '~/lib'
import { agreeToGPD, cookie, getGPDInfo } from '~/business/gpd'
import { inputFromForm } from 'composable-functions'
import { Route } from './+types/root'

export const links: LinksFunction = () => [{ rel: 'stylesheet', href: styles }]
export const links: Route.LinksFunction = () => [
{ rel: 'stylesheet', href: styles },
]

export function Layout({ children }: { children: React.ReactNode }) {
return (
Expand All @@ -37,12 +36,13 @@ export function Layout({ children }: { children: React.ReactNode }) {
)
}

export const loader = async ({ request }: LoaderFunctionArgs) => {
export const loader = async ({ request }: Route.LoaderArgs) => {
const result = await getGPDInfo(null, await ctxFromCookie(request, cookie))
return loaderResponseOrThrow(result)
if (!result.success) throw new Error('Internal server error')
return result.data
}

export const action = async ({ request }: ActionFunctionArgs) => {
export const action = async ({ request }: Route.ActionArgs) => {
const result = await agreeToGPD(await inputFromForm(request))
if (!result.success || result.data.agreed === false) {
return actionResponse(result)
Expand All @@ -52,9 +52,8 @@ export const action = async ({ request }: ActionFunctionArgs) => {
})
}

export default function App() {
const { agreed } = useLoaderData<typeof loader>()
const actionData = useActionData<typeof action>()
export default function App({ loaderData, actionData }: Route.ComponentProps) {
const { agreed } = loaderData
const disagreed = actionData?.success && actionData.data.agreed === false
return (
<main className="isolate flex w-full grow flex-col items-center justify-center">
Expand Down Expand Up @@ -92,8 +91,7 @@ export default function App() {
)
}

export function ErrorBoundary() {
const error = useRouteError()
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
console.error(error)
return (
<div>
Expand Down
21 changes: 12 additions & 9 deletions examples/react-router/app/routes/color.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,30 @@
import { ActionFunctionArgs, LoaderFunctionArgs } from 'react-router';
import { Form, Link, useActionData, useLoaderData } from 'react-router';
import { Form, Link } from 'react-router'
import { applySchema, inputFromForm } from 'composable-functions'
import tinycolor from 'tinycolor2'
import { getColor, mutateColor } from '~/business/colors'
import { actionResponse, loaderResponseOrThrow } from '~/lib'
import { actionResponse } from '~/lib'
import { z } from 'zod'
import { Route } from '../routes/+types/color'

export const loader = async ({ params }: LoaderFunctionArgs) => {
export const loader = async ({ params }: Route.LoaderArgs) => {
const result = await applySchema(z.object({ id: z.string() }))(getColor)(
params,
)
return loaderResponseOrThrow(result)
if (!result.success) throw new Error('Could not load data')
return result.data
}

export const action = async ({ request }: ActionFunctionArgs) => {
export const action = async ({ request }: Route.ActionArgs) => {
const input = await inputFromForm(request)
const result = await mutateColor(input)
return actionResponse(result)
}

export default function Index() {
const { data } = useLoaderData<typeof loader>()
const actionData = useActionData<typeof action>()
export default function Index({
loaderData,
actionData,
}: Route.ComponentProps) {
const { data } = loaderData
const color = actionData?.success ? actionData.data.color : data.color
return (
<>
Expand Down
15 changes: 8 additions & 7 deletions examples/react-router/app/routes/home.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { LoaderFunctionArgs } from 'react-router';
import { Link, useLoaderData, useLocation } from 'react-router';
import { Link, useLocation } from 'react-router'
import { inputFromUrl, collect, map, applySchema } from 'composable-functions'
import { listColors } from '~/business/colors'
import { listUsers } from '~/business/users'
import { loaderResponseOrThrow } from '~/lib'
import { z } from 'zod'
import { Route } from '../routes/+types/home'

const getData = applySchema(
// We are applying a schema for runtime safety
Expand All @@ -19,14 +18,16 @@ const getData = applySchema(
users: listUsers,
}),
)
export const loader = async ({ request }: LoaderFunctionArgs) => {
export const loader = async ({ request }: Route.LoaderArgs) => {
// inputFromUrl gets the queryString out of the request and turns it into an object
const result = await getData(inputFromUrl(request))
return loaderResponseOrThrow(result)
if (!result.success) throw new Error('Could not load data')
return result.data
}

export default function Index() {
const { users, colors } = useLoaderData<typeof loader>()
export default function Index({
loaderData: { users, colors },
}: Route.ComponentProps) {
const location = useLocation()
const qs = new URLSearchParams(location.search)
return (
Expand Down
13 changes: 6 additions & 7 deletions examples/react-router/app/routes/user.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { LoaderFunctionArgs } from 'react-router';
import { Link, useLoaderData } from 'react-router';
import { Link } from 'react-router'
import { applySchema, pipe } from 'composable-functions'
import { formatUser, getUser } from '~/business/users'
import { loaderResponseOrThrow } from '~/lib'
import { z } from 'zod'
import { Route } from '../routes/+types/user'

const getData = applySchema(
// We are adding runtime validation because the Remix's `Params` object is not strongly typed
Expand All @@ -14,13 +13,13 @@ const getData = applySchema(
pipe(getUser, formatUser),
)

export const loader = async ({ params }: LoaderFunctionArgs) => {
export const loader = async ({ params }: Route.LoaderArgs) => {
const result = await getData(params)
return loaderResponseOrThrow(result)
if (!result.success) throw new Error('Could not load data')
return result.data
}

export default function Index() {
const { user } = useLoaderData<typeof loader>()
export default function Index({ loaderData: { user } }: Route.ComponentProps) {
return (
<>
<a
Expand Down

0 comments on commit 2e0e69d

Please sign in to comment.