Skip to content

Instantly share code, notes, and snippets.

@itsMapleLeaf
Last active May 24, 2024 03:53
Show Gist options
  • Save itsMapleLeaf/0521a21e5e24419ce90d447e7ceede70 to your computer and use it in GitHub Desktop.
Save itsMapleLeaf/0521a21e5e24419ce90d447e7ceede70 to your computer and use it in GitHub Desktop.
Typed remix helpers

This is no longer needed! Remix's built-in types have improved significantly. But I'll keep this here for historical reasons.


Typed helpers for low-boilerplate type inference with Remix data.

  • I suffix them with *Typed so I don't accidentally import the core remix helpers.
  • This doesn't support regular Response objects to prevent accidentally using them with the typed helpers.
import type { DataFunctionArgs } from "@remix-run/node"
import {
DeferredTyped,
deferredTyped,
useLoaderDataTyped,
} from "~/modules/remix-typed"
function loadData() {
return new Promise<number>((resolve) => {
setTimeout(() => {
resolve(42)
}, 1000)
})
}
export async function loader({ request }: DataFunctionArgs) {
return deferredTyped({
data: loadData(),
})
}
export default function Example() {
const { data } = useLoaderDataTyped<typeof loader>()
return (
<DeferredTyped data={data} fallback={<p>loading...</p>}>
{(data) => <p>the secret is {data}</p>}
</DeferredTyped>
)
}
import type { DataFunctionArgs } from "@remix-run/node"
import { Form } from "@remix-run/react"
import {
jsonTyped,
redirectTyped,
useActionDataTyped,
useLoaderDataTyped,
} from "~/remix-typed"
let message = "hi"
export async function loader({ request }: DataFunctionArgs) {
return jsonTyped({ message })
}
export async function action({ request }: DataFunctionArgs) {
const body = new URLSearchParams(await request.text())
const newMessage = body.get("message")
if (typeof newMessage !== "string") {
return jsonTyped({ error: "no message" }, 400)
}
message = newMessage
return "" // stay on the same page
}
export default function Page() {
const { message } = useLoaderDataTyped<typeof loader>()
const { error } = useActionDataTyped<typeof action>() ?? {}
return (
<main>
<p>{message}</p>
<Form method="post">
<input name="message" />
<button type="submit">Submit</button>
</Form>
{error && <p style={{ color: "crimson" }}>{error}</p>}
</main>
)
}
import type { DataFunctionArgs } from "@remix-run/node"
import { json, redirect } from "@remix-run/node"
import { useActionData, useFetcher, useLoaderData } from "@remix-run/react"
export type ResponseTyped<Data> = Omit<Response, "json"> & {
json(): Promise<Data>
}
export type JsonValue =
| string
| number
| boolean
| null
| { [key: string]: JsonValue | undefined }
| JsonValue[]
export function jsonTyped<Input extends JsonValue>(
data: Input,
init?: ResponseInit | number,
) {
return json(data, init) as ResponseTyped<Input>
}
export function redirectTyped(url: string, init?: ResponseInit | number) {
return redirect(url, init) as ResponseTyped<never>
}
export type DataFunctionTyped<Result extends JsonValue> = (
args: DataFunctionArgs,
) => MaybePromise<ResponseTyped<Result>>
export type DefaultDataFunction = DataFunctionTyped<JsonValue>
export function useLoaderDataTyped<DataFunction extends DefaultDataFunction>() {
return useLoaderData<InferDataFunctionResult<DataFunction>>()
}
export function useActionDataTyped<DataFunction extends DefaultDataFunction>() {
return useActionData<InferDataFunctionResult<DataFunction>>()
}
export function useFetcherTyped<DataFunction extends DefaultDataFunction>() {
return useFetcher<InferDataFunctionResult<DataFunction>>()
}
export type InferDataFunctionResult<DataFunction> =
DataFunction extends DataFunctionTyped<infer Data> ? Data : unknown
export type MaybePromise<Value> = Value | PromiseLike<Value>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment