Skip to content

Instantly share code, notes, and snippets.

@itsMapleLeaf
Last active February 15, 2023 12:32
Show Gist options
  • Save itsMapleLeaf/705e2989f91d49912a3ffd9d0520ec68 to your computer and use it in GitHub Desktop.
Save itsMapleLeaf/705e2989f91d49912a3ffd9d0520ec68 to your computer and use it in GitHub Desktop.
SolidStart-style server actions in Remix
function CreateTodoButton() {
const submit = CreateTodoAction.useSubmit()
const isSubmitting = CreateTodoAction.useSubmissions().length > 0
const params = useParams()
return (
<button
type="button"
title="Create Todo"
disabled={isSubmitting}
onClick={() => submit({ projectId: params.projectId!, text: "New Todo" })}
>
{isSubmitting ? <LoadingSpinner size={6} /> : <Plus />}
</button>
)
}
const CreateTodoAction = serverAction(
"create-todo",
async (input: { projectId: string; text: string }, request) => {
const user = await getSessionUser(request)
const todo = await db.todos.create({
data: { ...input, creatorId: user.id },
})
// because this runs in the server, can't import this at the top level (for now?)
const { redirect } = await import("@remix-run/node")
return redirect(`/projects/${input.projectId}/todos/${todo.id}`)
},
)
import { type ActionArgs } from "@remix-run/node"
import { handleServerAction } from "~/server-actions"
// set up the resource route to handle action submissions
export async function action({ request, params }: ActionArgs) {
return handleServerAction(request, params["actionName"]!)
}
import { type TypedResponse } from "@remix-run/node"
import {
useFetcher,
useFetchers,
useSubmit,
useTransition,
} from "@remix-run/react"
import { type MaybePromise } from "./helpers/types"
declare global {
var __serverActionRegistry:
| Map<
string,
(input: any, request: Request) => MaybePromise<TypedResponse<any>>
>
| undefined
}
const registry = (globalThis.__serverActionRegistry ??= new Map<
string,
(input: any, request: Request) => MaybePromise<TypedResponse<any>>
>())
// for my app-like needs, I'm relying more on submit calls than <Form> elements,
// which is better for type safety anyway,
// but an ideal implementation would include an alternate `formServerAction`
// for cases where that's easier
export function serverAction<Input, Return>(
actionName: string,
callback: (
input: Input,
request: Request,
) => MaybePromise<TypedResponse<Return>>,
) {
registry.set(actionName, callback)
const actionUrl = `/server-actions/${actionName}`
return {
useSubmit: function useServerActionSubmit() {
const submit = useSubmit()
return (data: Input) => {
submit(
{ data: JSON.stringify(data) },
{ method: "post", action: actionUrl },
)
}
},
useFetcher: function useServerActionFetcher() {
const fetcher = useFetcher<Return>()
return {
...fetcher,
Form: undefined,
submit: (data: Input) => {
fetcher.submit(
{ data: JSON.stringify(data) },
{ method: "post", action: actionUrl },
)
},
}
},
useSubmissions: function useServerActionSubmissions() {
const transition = useTransition()
const fetchers = useFetchers()
return [transition, ...fetchers].flatMap((state) => {
if (state.submission?.action !== actionUrl) return []
return JSON.parse(
state.submission.formData.get("data") as string,
) as Input
})
},
}
}
export async function handleServerAction(request: Request, actionName: string) {
const action = registry.get(actionName)
if (!action) {
return new Response(undefined, {
status: 404,
statusText: `No action found for ${actionName}`,
})
}
const formData = await request.formData()
const input = JSON.parse(formData.get("data") as string)
return await action(input, request)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment