Awesome-omni-skill solidstart-data-mutation
SolidStart data mutation: form submissions with actions, validation, error handling, pending states, optimistic UI, redirects, database operations, programmatic triggers.
install
source · Clone the upstream repo
git clone https://github.com/diegosouzapw/awesome-omni-skill
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/backend/solidstart-data-mutation" ~/.claude/skills/diegosouzapw-awesome-omni-skill-solidstart-data-mutation && rm -rf "$T"
manifest:
skills/backend/solidstart-data-mutation/SKILL.mdsource content
SolidStart Data Mutation
Complete guide to handling data mutations in SolidStart using actions, forms, validation, and error handling.
Basic Form Submission
Actions handle form submissions. Forms must use
method="post":
import { action } from "@solidjs/router"; const addPost = action(async (formData: FormData) => { const title = formData.get("title") as string; await fetch("https://my-api.com/posts", { method: "POST", body: JSON.stringify({ title }), }); }, "addPost"); export default function Page() { return ( <form action={addPost} method="post"> <input name="title" /> <button>Add Post</button> </form> ); }
Requirements:
- Action must have unique name (second parameter)
- Form must use
method="post" - Action receives
as first parameterFormData - Use
to extract field valuesFormData.get()
Passing Additional Arguments
Use
.with() to pass additional arguments to actions:
const addPost = action(async (userId: number, formData: FormData) => { const title = formData.get("title") as string; await fetch("https://my-api.com/posts", { method: "POST", body: JSON.stringify({ userId, title }), }); }, "addPost"); export default function Page() { const userId = 1; return ( <form action={addPost.with(userId)} method="post"> <input name="title" /> <button>Add Post</button> </form> ); }
Showing Pending UI
Use
useSubmission to track submission state and show pending UI:
import { action, useSubmission } from "@solidjs/router"; const addPost = action(async (formData: FormData) => { const title = formData.get("title") as string; await fetch("https://my-api.com/posts", { method: "POST", body: JSON.stringify({ title }), }); }, "addPost"); export default function Page() { const submission = useSubmission(addPost); return ( <form action={addPost} method="post"> <input name="title" /> <button disabled={submission.pending}> {submission.pending ? "Adding..." : "Add Post"} </button> </form> ); }
Submission properties:
- Boolean indicating if action is runningpending
- Successful return valueresult
- Error thrownerror
- Reactive input datainput
- Clear submission stateclear()
- Re-execute with same inputretry()
Handling Errors
Display errors from failed actions:
import { Show } from "solid-js"; import { action, useSubmission } from "@solidjs/router"; const addPost = action(async (formData: FormData) => { const title = formData.get("title") as string; const response = await fetch("https://my-api.com/posts", { method: "POST", body: JSON.stringify({ title }), }); if (!response.ok) { throw new Error("Failed to add post"); } }, "addPost"); export default function Page() { const submission = useSubmission(addPost); return ( <form action={addPost} method="post"> <Show when={submission.error}> <p class="error">{submission.error.message}</p> <button onClick={() => submission.retry()}>Retry</button> </Show> <input name="title" /> <button>Add Post</button> </form> ); }
Validating Form Fields
Return validation errors from actions and display them:
import { Show } from "solid-js"; import { action, useSubmission } from "@solidjs/router"; const addPost = action(async (formData: FormData) => { const title = formData.get("title") as string; // Validate if (!title || title.length < 2) { return { error: "Title must be at least 2 characters", }; } await fetch("https://my-api.com/posts", { method: "POST", body: JSON.stringify({ title }), }); return { success: true }; }, "addPost"); export default function Page() { const submission = useSubmission(addPost); return ( <form action={addPost} method="post"> <input name="title" /> <Show when={submission.result?.error}> <p class="error">{submission.result.error}</p> </Show> <button>Add Post</button> </form> ); }
Validation pattern:
- Return error object from action (don't throw)
- Check
in UIsubmission.result?.error - Action continues execution if validation passes
Optimistic UI
Show expected result immediately before server responds. See
solidstart-optimistic-ui rule for detailed patterns.
Basic pattern with
useSubmission:
import { For, Show } from "solid-js"; import { action, useSubmission, query, createAsync } from "@solidjs/router"; const getPosts = query(async () => { const posts = await fetch("https://my-api.com/blog"); return await posts.json(); }, "posts"); const addPost = action(async (formData: FormData) => { const title = formData.get("title") as string; await fetch("https://my-api.com/posts", { method: "POST", body: JSON.stringify({ title }), }); }, "addPost"); export default function Page() { const posts = createAsync(() => getPosts()); const submission = useSubmission(addPost); return ( <main> <form action={addPost} method="post"> <input name="title" /> <button>Add Post</button> </form> <ul> <For each={posts()}>{(post) => <li>{post.title}</li>}</For> <Show when={submission.pending}> <li>{submission.input?.[0]?.get("title")?.toString()} (pending)</li> </Show> </ul> </main> ); }
For multiple concurrent submissions, use
useSubmissions (see solidstart-optimistic-ui rule).
Redirecting After Mutation
Redirect users after successful mutation:
import { action, redirect } from "@solidjs/router"; const addPost = action(async (formData: FormData) => { const title = formData.get("title") as string; const response = await fetch("https://my-api.com/posts", { method: "POST", body: JSON.stringify({ title }), }); const post = await response.json(); // Throw redirect to navigate throw redirect(`/posts/${post.id}`); }, "addPost");
Important: Must
throw redirect(), not return it.
Using Database or ORM
Mark actions with
"use server" to safely access database:
import { action } from "@solidjs/router"; import { db } from "~/lib/db"; const addPost = action(async (formData: FormData) => { "use server"; const title = formData.get("title") as string; await db.insert("posts").values({ title }); }, "addPost");
Best practices:
- Always use
for database operations"use server" - Keeps API keys and database credentials secure
- Runs exclusively on server
- Can be called from client (automatically transformed to RPC)
Programmatic Action Triggers
Use
useAction to trigger actions programmatically (not just from forms):
import { createSignal } from "solid-js"; import { action, useAction } from "@solidjs/router"; const addPost = action(async (title: string) => { await fetch("https://my-api.com/posts", { method: "POST", body: JSON.stringify({ title }), }); }, "addPost"); export default function Page() { const [title, setTitle] = createSignal(""); const addPostAction = useAction(addPost); const handleSubmit = async () => { await addPostAction(title()); setTitle(""); // Clear input }; return ( <div> <input value={title()} onInput={(e) => setTitle(e.target.value)} /> <button onClick={handleSubmit}>Add Post</button> </div> ); }
Use cases:
- Custom form handling (not using native
)<form> - Button clicks that trigger mutations
- Complex validation before submission
- Multiple actions in sequence
Complete Example: Form with Validation and Error Handling
import { Show } from "solid-js"; import { action, useSubmission, redirect } from "@solidjs/router"; const createUser = action(async (formData: FormData) => { "use server"; const email = formData.get("email") as string; const name = formData.get("name") as string; // Validation if (!email || !email.includes("@")) { return { error: "Invalid email address" }; } if (!name || name.length < 2) { return { error: "Name must be at least 2 characters" }; } // Database operation const user = await db.users.create({ email, name }); // Redirect on success throw redirect(`/users/${user.id}`); }, "createUser"); export default function CreateUserPage() { const submission = useSubmission(createUser); return ( <form action={createUser} method="post"> <input name="email" type="email" /> <input name="name" /> <Show when={submission.result?.error}> <p class="error">{submission.result.error}</p> </Show> <Show when={submission.error}> <p class="error">Error: {submission.error.message}</p> <button onClick={() => submission.retry()}>Retry</button> </Show> <button disabled={submission.pending}> {submission.pending ? "Creating..." : "Create User"} </button> </form> ); }
Best Practices
- Always name actions: Second parameter to
must be uniqueaction() - Use
for database: Keeps credentials secure"use server" - Track submissions: Use
for better UX (pending, errors)useSubmission - Validate in actions: Return error objects, don't throw for validation errors
- Handle errors: Show error messages and provide retry options
- Use
for additional args: When forms need extra context.with() - Throw redirects: Must throw, not return, redirect responses
- Optimistic UI: Use
for multiple concurrent mutationsuseSubmissions - Programmatic triggers: Use
when not using native formsuseAction
Common Patterns
File Uploads
const uploadFile = action(async (formData: FormData) => { "use server"; const file = formData.get("file") as File; // Handle file upload }, "uploadFile"); <form action={uploadFile} method="post" enctype="multipart/form-data"> <input name="file" type="file" /> <button>Upload</button> </form>
Multiple Actions in Sequence
const saveDraft = useAction(saveDraftAction); const publish = useAction(publishAction); const handlePublish = async () => { await saveDraft(data); await publish(data.id); };
Conditional Redirects
const updatePost = action(async (formData: FormData) => { "use server"; const post = await db.posts.update(formData); if (post.published) { throw redirect(`/posts/${post.id}`); } else { throw redirect(`/posts/${post.id}/edit`); } }, "updatePost");