Agent skill
solidstart-data-mutation
SolidStart data mutation: form submissions with actions, validation, error handling, pending states, optimistic UI, redirects, database operations, programmatic triggers.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/solidstart-data-mutation
Metadata
Additional technical details for this skill
- globs
-
[ "**/routes/**/*", "**/*action*", "**/*mutation*", "**/*form*" ]
SKILL.md
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
FormDataas first parameter - Use
FormData.get()to extract field values
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:
pending- Boolean indicating if action is runningresult- Successful return valueerror- Error throwninput- Reactive input dataclear()- Clear submission stateretry()- Re-execute with same input
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
submission.result?.errorin UI - 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
"use server"for database operations - 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
action()must be unique - Use
"use server"for database: Keeps credentials secure - Track submissions: Use
useSubmissionfor better UX (pending, errors) - Validate in actions: Return error objects, don't throw for validation errors
- Handle errors: Show error messages and provide retry options
- Use
.with()for additional args: When forms need extra context - Throw redirects: Must throw, not return, redirect responses
- Optimistic UI: Use
useSubmissionsfor multiple concurrent mutations - Programmatic triggers: Use
useActionwhen not using native forms
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");
Didn't find tool you were looking for?