Files
Devour/devour_data/docs/data-mutation-b5a27c41.json
T
Tomas Dvorak 898a3c303f update
2026-02-24 10:33:59 +01:00

17 lines
18 KiB
JSON

{
"id": "b5a27c418c217634e26c9104",
"source": "solid:signals",
"type": "github-document",
"title": "data-mutation",
"content": "---\ntitle: Data mutation\nuse_cases: \u003e-\n form submission, data updates, crud operations, user input handling, database\n writes, api posts, validation, optimistic ui\ntags:\n - forms\n - actions\n - mutations\n - validation\n - database\n - api\n - crud\nversion: '1.0'\ndescription: \u003e-\n Learn how to handle form submissions, validate data, and perform mutations\n with SolidStart actions. Complete guide with examples.\n---\n\nThis guide provides practical examples of using actions to mutate data in SolidStart.\n\n## Handling form submission\n\nTo handle [`\u003cform\u003e`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form) submissions with an action:\n\n1. Ensure the action has a unique name.\n See the [Action API reference](/solid-router/reference/data-apis/action#notes-of-form-implementation-and-ssr) for more information.\n2. Pass the action to the `\u003cform\u003e` element using the `action` prop.\n3. Ensure the `\u003cform\u003e` element uses the `post` method for submission.\n4. Use the [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData/FormData) object in the action to extract field data using the native `FormData` methods.\n\n```tsx tab title=\"TypeScript\" {4-10} {14}\n// src/routes/index.tsx\nimport { action } from \"@solidjs/router\";\n\nconst addPost = action(async (formData: FormData) =\u003e {\n\tconst title = formData.get(\"title\") as string;\n\tawait fetch(\"https://my-api.com/posts\", {\n\t\tmethod: \"POST\",\n\t\tbody: JSON.stringify({ title }),\n\t});\n}, \"addPost\");\n\nexport default function Page() {\n\treturn (\n\t\t\u003cform action={addPost} method=\"post\"\u003e\n\t\t\t\u003cinput name=\"title\" /\u003e\n\t\t\t\u003cbutton\u003eAdd Post\u003c/button\u003e\n\t\t\u003c/form\u003e\n\t);\n}\n```\n\n```jsx tab title=\"JavaScript\" {4-10} {14}\n// src/routes/index.jsx\nimport { action } from \"@solidjs/router\";\n\nconst addPost = action(async (formData) =\u003e {\n\tconst title = formData.get(\"title\");\n\tawait fetch(\"https://my-api.com/posts\", {\n\t\tmethod: \"POST\",\n\t\tbody: JSON.stringify({ title }),\n\t});\n}, \"addPost\");\n\nexport default function Page() {\n\treturn (\n\t\t\u003cform action={addPost} method=\"post\"\u003e\n\t\t\t\u003cinput name=\"title\" /\u003e\n\t\t\t\u003cbutton\u003eAdd Post\u003c/button\u003e\n\t\t\u003c/form\u003e\n\t);\n}\n```\n\n## Passing additional arguments\n\nTo pass additional arguments to your action, use the `with` method:\n\n```tsx tab title=\"TypeScript\" {4} {15}\n// src/routes/index.tsx\nimport { action } from \"@solidjs/router\";\n\nconst addPost = action(async (userId: number, formData: FormData) =\u003e {\n\tconst title = formData.get(\"title\") as string;\n\tawait fetch(\"https://my-api.com/posts\", {\n\t\tmethod: \"POST\",\n\t\tbody: JSON.stringify({ userId, title }),\n\t});\n}, \"addPost\");\n\nexport default function Page() {\n\tconst userId = 1;\n\treturn (\n\t\t\u003cform action={addPost.with(userId)} method=\"post\"\u003e\n\t\t\t\u003cinput name=\"title\" /\u003e\n\t\t\t\u003cbutton\u003eAdd Post\u003c/button\u003e\n\t\t\u003c/form\u003e\n\t);\n}\n```\n\n```jsx tab title=\"JavaScript\" {4} {15}\n// src/routes/index.jsx\nimport { action } from \"@solidjs/router\";\n\nconst addPost = action(async (userId, formData) =\u003e {\n\tconst title = formData.get(\"title\");\n\tawait fetch(\"https://my-api.com/posts\", {\n\t\tmethod: \"POST\",\n\t\tbody: JSON.stringify({ userId, title }),\n\t});\n}, \"addPost\");\n\nexport default function Page() {\n\tconst userId = 1;\n\treturn (\n\t\t\u003cform action={addPost.with(userId)} method=\"post\"\u003e\n\t\t\t\u003cinput name=\"title\" /\u003e\n\t\t\t\u003cbutton\u003eAdd Post\u003c/button\u003e\n\t\t\u003c/form\u003e\n\t);\n}\n```\n\n## Showing pending UI\n\nTo display a pending UI during action execution:\n\n1. Import [`useSubmission`](/solid-router/reference/data-apis/use-submission) from `@solidjs/router`.\n2. Call `useSubmission` with your action, and use the returned `pending` property to display pending UI.\n\n```tsx tab title=\"TypeScript\" {13} {17-19}\n// src/routes/index.tsx\nimport { action, useSubmission } from \"@solidjs/router\";\n\nconst addPost = action(async (formData: FormData) =\u003e {\n\tconst title = formData.get(\"title\") as string;\n\tawait fetch(\"https://my-api.com/posts\", {\n\t\tmethod: \"POST\",\n\t\tbody: JSON.stringify({ title }),\n\t});\n}, \"addPost\");\n\nexport default function Page() {\n\tconst submission = useSubmission(addPost);\n\treturn (\n\t\t\u003cform action={addPost} method=\"post\"\u003e\n\t\t\t\u003cinput name=\"title\" /\u003e\n\t\t\t\u003cbutton disabled={submission.pending}\u003e\n\t\t\t\t{submission.pending ? \"Adding...\" : \"Add Post\"}\n\t\t\t\u003c/button\u003e\n\t\t\u003c/form\u003e\n\t);\n}\n```\n\n```jsx tab title=\"JavaScript\" {13} {17-19}\n// src/routes/index.jsx\nimport { action, useSubmission } from \"@solidjs/router\";\n\nconst addPost = action(async (formData) =\u003e {\n\tconst title = formData.get(\"title\");\n\tawait fetch(\"https://my-api.com/posts\", {\n\t\tmethod: \"POST\",\n\t\tbody: JSON.stringify({ title }),\n\t});\n}, \"addPost\");\n\nexport default function Page() {\n\tconst submission = useSubmission(addPost);\n\treturn (\n\t\t\u003cform action={addPost} method=\"post\"\u003e\n\t\t\t\u003cinput name=\"title\" /\u003e\n\t\t\t\u003cbutton disabled={submission.pending}\u003e\n\t\t\t\t{submission.pending ? \"Adding...\" : \"Add Post\"}\n\t\t\t\u003c/button\u003e\n\t\t\u003c/form\u003e\n\t);\n}\n```\n\n## Handling errors\n\nTo handle errors that occur within an action:\n\n1. Import [`useSubmission`](/solid-router/reference/data-apis/use-submission) from `@solidjs/router`.\n2. Call `useSubmission` with your action, and use the returned `error` property to handle the error.\n\n```tsx tab title=\"TypeScript\" {14} {17-19}\n// src/routes/index.tsx\nimport { Show } from \"solid-js\";\nimport { action, useSubmission } from \"@solidjs/router\";\n\nconst addPost = action(async (formData: FormData) =\u003e {\n\tconst title = formData.get(\"title\") as string;\n\tawait fetch(\"https://my-api.com/posts\", {\n\t\tmethod: \"POST\",\n\t\tbody: JSON.stringify({ title }),\n\t});\n}, \"addPost\");\n\nexport default function Page() {\n\tconst submission = useSubmission(addPost);\n\treturn (\n\t\t\u003cform action={addPost} method=\"post\"\u003e\n\t\t\t\u003cShow when={submission.error}\u003e\n\t\t\t\t\u003cp\u003e{submission.error.message}\u003c/p\u003e\n\t\t\t\u003c/Show\u003e\n\t\t\t\u003cinput name=\"title\" /\u003e\n\t\t\t\u003cbutton\u003eAdd Post\u003c/button\u003e\n\t\t\u003c/form\u003e\n\t);\n}\n```\n\n```jsx tab title=\"JavaScript\" {14} {17-19}\n// src/routes/index.jsx\nimport { Show } from \"solid-js\";\nimport { action, useSubmission } from \"@solidjs/router\";\n\nconst addPost = action(async (formData) =\u003e {\n\tconst title = formData.get(\"title\");\n\tawait fetch(\"https://my-api.com/posts\", {\n\t\tmethod: \"POST\",\n\t\tbody: JSON.stringify({ title }),\n\t});\n}, \"addPost\");\n\nexport default function Page() {\n\tconst submission = useSubmission(addPost);\n\treturn (\n\t\t\u003cform action={addPost} method=\"post\"\u003e\n\t\t\t\u003cShow when={submission.error}\u003e\n\t\t\t\t\u003cp\u003e{submission.error.message}\u003c/p\u003e\n\t\t\t\u003c/Show\u003e\n\t\t\t\u003cinput name=\"title\" /\u003e\n\t\t\t\u003cbutton\u003eAdd Post\u003c/button\u003e\n\t\t\u003c/form\u003e\n\t);\n}\n```\n\n## Validating form fields\n\nTo validate form fields in an action:\n\n1. Add validation logic in your action and return validation errors if the data is invalid.\n2. Import [`useSubmission`](/solid-router/reference/data-apis/use-submission) from `@solidjs/router`.\n3. Call `useSubmission` with your action, and use the returned `result` property to handle the errors.\n\n```tsx tab title=\"TypeScript\" {7-11} {19} {23-25}\n// src/routes/index.tsx\nimport { Show } from \"solid-js\";\nimport { action, useSubmission } from \"@solidjs/router\";\n\nconst addPost = action(async (formData: FormData) =\u003e {\n\tconst title = formData.get(\"title\") as string;\n\tif (!title || title.length \u003c 2) {\n\t\treturn {\n\t\t\terror: \"Title must be at least 2 characters\",\n\t\t};\n\t}\n\tawait fetch(\"https://my-api.com/posts\", {\n\t\tmethod: \"POST\",\n\t\tbody: JSON.stringify({ title }),\n\t});\n}, \"addPost\");\n\nexport default function Page() {\n\tconst submission = useSubmission(addPost);\n\treturn (\n\t\t\u003cform action={addPost} method=\"post\"\u003e\n\t\t\t\u003cinput name=\"title\" /\u003e\n\t\t\t\u003cShow when={submission.result?.error}\u003e\n\t\t\t\t\u003cp\u003e{submission.result?.error}\u003c/p\u003e\n\t\t\t\u003c/Show\u003e\n\t\t\t\u003cbutton\u003eAdd Post\u003c/button\u003e\n\t\t\u003c/form\u003e\n\t);\n}\n```\n\n```jsx tab title=\"JavaScript\" {7-11} {19} {23-25}\n// src/routes/index.jsx\nimport { Show } from \"solid-js\";\nimport { action, useSubmission } from \"@solidjs/router\";\n\nconst addPost = action(async (formData) =\u003e {\n\tconst title = formData.get(\"title\");\n\tif (!title || title.length \u003c 2) {\n\t\treturn {\n\t\t\terror: \"Title must be at least 2 characters\",\n\t\t};\n\t}\n\tawait fetch(\"https://my-api.com/posts\", {\n\t\tmethod: \"POST\",\n\t\tbody: JSON.stringify({ title }),\n\t});\n}, \"addPost\");\n\nexport default function Page() {\n\tconst submission = useSubmission(addPost);\n\treturn (\n\t\t\u003cform action={addPost} method=\"post\"\u003e\n\t\t\t\u003cinput name=\"title\" /\u003e\n\t\t\t\u003cShow when={submission.result?.error}\u003e\n\t\t\t\t\u003cp\u003e{submission.result?.error}\u003c/p\u003e\n\t\t\t\u003c/Show\u003e\n\t\t\t\u003cbutton\u003eAdd Post\u003c/button\u003e\n\t\t\u003c/form\u003e\n\t);\n}\n```\n\n## Showing optimistic UI\n\nTo update the UI before the server responds:\n\n1. Import [`useSubmission`](/solid-router/reference/data-apis/use-submission) from `@solidjs/router`.\n2. Call `useSubmission` with your action, and use the returned `pending` and `input` properties to display optimistic UI.\n\n```tsx tab title=\"TypeScript\" {20} {29-31}\n// src/routes/index.tsx\nimport { For, Show } from \"solid-js\";\nimport { action, useSubmission, query, createAsync } from \"@solidjs/router\";\n\nconst getPosts = query(async () =\u003e {\n\tconst posts = await fetch(\"https://my-api.com/blog\");\n\treturn await posts.json();\n}, \"posts\");\n\nconst addPost = action(async (formData: FormData) =\u003e {\n\tconst title = formData.get(\"title\");\n\tawait fetch(\"https://my-api.com/posts\", {\n\t\tmethod: \"POST\",\n\t\tbody: JSON.stringify({ title }),\n\t});\n}, \"addPost\");\n\nexport default function Page() {\n\tconst posts = createAsync(() =\u003e getPosts());\n\tconst submission = useSubmission(addPost);\n\treturn (\n\t\t\u003cmain\u003e\n\t\t\t\u003cform action={addPost} method=\"post\"\u003e\n\t\t\t\t\u003cinput name=\"title\" /\u003e\n\t\t\t\t\u003cbutton\u003eAdd Post\u003c/button\u003e\n\t\t\t\u003c/form\u003e\n\t\t\t\u003cul\u003e\n\t\t\t\t\u003cFor each={posts()}\u003e{(post) =\u003e \u003cli\u003e{post.title}\u003c/li\u003e}\u003c/For\u003e\n\t\t\t\t\u003cShow when={submission.pending}\u003e\n\t\t\t\t\t{submission.input?.[0]?.get(\"title\")?.toString()}\n\t\t\t\t\u003c/Show\u003e\n\t\t\t\u003c/ul\u003e\n\t\t\u003c/main\u003e\n\t);\n}\n```\n\n```jsx tab title=\"JavaScript\" {20} {29-31}\n// src/routes/index.jsx\nimport { For, Show } from \"solid-js\";\nimport { action, useSubmission, query, createAsync } from \"@solidjs/router\";\n\nconst getPosts = query(async () =\u003e {\n\tconst posts = await fetch(\"https://my-api.com/blog\");\n\treturn await posts.json();\n}, \"posts\");\n\nconst addPost = action(async (formData) =\u003e {\n\tconst title = formData.get(\"title\");\n\tawait fetch(\"https://my-api.com/posts\", {\n\t\tmethod: \"POST\",\n\t\tbody: JSON.stringify({ title }),\n\t});\n}, \"addPost\");\n\nexport default function Page() {\n\tconst posts = createAsync(() =\u003e getPosts());\n\tconst submission = useSubmission(addPost);\n\treturn (\n\t\t\u003cmain\u003e\n\t\t\t\u003cform action={addPost} method=\"post\"\u003e\n\t\t\t\t\u003cinput name=\"title\" /\u003e\n\t\t\t\t\u003cbutton\u003eAdd Post\u003c/button\u003e\n\t\t\t\u003c/form\u003e\n\t\t\t\u003cul\u003e\n\t\t\t\t\u003cFor each={posts()}\u003e{(post) =\u003e \u003cli\u003e{post.title}\u003c/li\u003e}\u003c/For\u003e\n\t\t\t\t\u003cShow when={submission.pending}\u003e\n\t\t\t\t\t{submission.input?.[0]?.get(\"title\")?.toString()}\n\t\t\t\t\u003c/Show\u003e\n\t\t\t\u003c/ul\u003e\n\t\t\u003c/main\u003e\n\t);\n}\n```\n\n:::note[Multiple Submissions]\nIf you want to display optimistic UI for multiple concurrent submissions, you can use the [`useSubmissions`](/solid-router/reference/data-apis/use-submissions) primitive.\n:::\n\n## Redirecting\n\nTo redirect users to a different route within an action:\n\n1. Import [`redirect`](/solid-router/reference/response-helpers/redirect) from `@solidjs/router`.\n2. Call `redirect` with the route you want to navigate to, and throw its response.\n\n```tsx tab title=\"TypeScript\" {11}\n// src/routes/index.tsx\nimport { action, redirect } from \"@solidjs/router\";\n\nconst addPost = action(async (formData: FormData) =\u003e {\n\tconst title = formData.get(\"title\") as string;\n\tconst response = await fetch(\"https://my-api.com/posts\", {\n\t\tmethod: \"POST\",\n\t\tbody: JSON.stringify({ title }),\n\t});\n\tconst post = await response.json();\n\tthrow redirect(`/posts/${post.id}`);\n}, \"addPost\");\n\nexport default function Page() {\n\treturn (\n\t\t\u003cform action={addPost} method=\"post\"\u003e\n\t\t\t\u003cinput name=\"title\" /\u003e\n\t\t\t\u003cbutton\u003eAdd Post\u003c/button\u003e\n\t\t\u003c/form\u003e\n\t);\n}\n```\n\n```jsx tab title=\"JavaScript\" {11}\n// src/routes/index.jsx\nimport { action, redirect } from \"@solidjs/router\";\n\nconst addPost = action(async (formData) =\u003e {\n\tconst title = formData.get(\"title\");\n\tconst response = await fetch(\"https://my-api.com/posts\", {\n\t\tmethod: \"POST\",\n\t\tbody: JSON.stringify({ title }),\n\t});\n\tconst post = await response.json();\n\tthrow redirect(`/posts/${post.id}`);\n}, \"addPost\");\n\nexport default function Page() {\n\treturn (\n\t\t\u003cform action={addPost} method=\"post\"\u003e\n\t\t\t\u003cinput name=\"title\" /\u003e\n\t\t\t\u003cbutton\u003eAdd Post\u003c/button\u003e\n\t\t\u003c/form\u003e\n\t);\n}\n```\n\n## Using a database or an ORM\n\nTo safely interact with your database or ORM in an action, ensure it's server-only by adding [`\"use server\"`](/solid-start/reference/server/use-server) as the first line of your action:\n\n```tsx tab title=\"TypeScript\" {6}\n// src/routes/index.tsx\nimport { action } from \"@solidjs/router\";\nimport { db } from \"~/lib/db\";\n\nconst addPost = action(async (formData: FormData) =\u003e {\n\t\"use server\";\n\tconst title = formData.get(\"title\") as string;\n\tawait db.insert(\"posts\").values({ title });\n}, \"addPost\");\n\nexport default function Page() {\n\treturn (\n\t\t\u003cform action={addPost} method=\"post\"\u003e\n\t\t\t\u003cinput name=\"title\" /\u003e\n\t\t\t\u003cbutton\u003eAdd Post\u003c/button\u003e\n\t\t\u003c/form\u003e\n\t);\n}\n```\n\n```jsx tab title=\"JavaScript\" {6}\n// src/routes/index.jsx\nimport { action } from \"@solidjs/router\";\nimport { db } from \"~/lib/db\";\n\nconst addPost = action(async (formData) =\u003e {\n\t\"use server\";\n\tconst title = formData.get(\"title\");\n\tawait db.insert(\"posts\").values({ title });\n}, \"addPost\");\n\nexport default function Page() {\n\treturn (\n\t\t\u003cform action={addPost} method=\"post\"\u003e\n\t\t\t\u003cinput name=\"title\" /\u003e\n\t\t\t\u003cbutton\u003eAdd Post\u003c/button\u003e\n\t\t\u003c/form\u003e\n\t);\n}\n```\n\n## Triggering an action programmatically\n\nTo programmatically trigger an action:\n\n1. Import [`useAction`](/solid-router/reference/data-apis/use-action) from `@solidjs/router`.\n2. Call `useAction` with your action, and use the returned function to trigger the action.\n\n```tsx tab title=\"TypeScript\" {14} {18}\n// src/routes/index.tsx\nimport { createSignal } from \"solid-js\";\nimport { action, useAction } from \"@solidjs/router\";\n\nconst addPost = action(async (title: string) =\u003e {\n\tawait fetch(\"https://my-api.com/posts\", {\n\t\tmethod: \"POST\",\n\t\tbody: JSON.stringify({ title }),\n\t});\n}, \"addPost\");\n\nexport default function Page() {\n\tconst [title, setTitle] = createSignal(\"\");\n\tconst addPostAction = useAction(addPost);\n\treturn (\n\t\t\u003cdiv\u003e\n\t\t\t\u003cinput value={title()} onInput={(e) =\u003e setTitle(e.target.value)} /\u003e\n\t\t\t\u003cbutton onClick={() =\u003e addPostAction(title())}\u003eAdd Post\u003c/button\u003e\n\t\t\u003c/div\u003e\n\t);\n}\n```\n\n```jsx tab title=\"JavaScript\" {14} {18}\n// src/routes/index.jsx\nimport { createSignal } from \"solid-js\";\nimport { action, useAction } from \"@solidjs/router\";\n\nconst addPost = action(async (title) =\u003e {\n\tawait fetch(\"https://my-api.com/posts\", {\n\t\tmethod: \"POST\",\n\t\tbody: JSON.stringify({ title }),\n\t});\n}, \"addPost\");\n\nexport default function Page() {\n\tconst [title, setTitle] = createSignal(\"\");\n\tconst addPostAction = useAction(addPost);\n\treturn (\n\t\t\u003cdiv\u003e\n\t\t\t\u003cinput value={title()} onInput={(e) =\u003e setTitle(e.target.value)} /\u003e\n\t\t\t\u003cbutton onClick={() =\u003e addPostAction(title())}\u003eAdd Post\u003c/button\u003e\n\t\t\u003c/div\u003e\n\t);\n}\n```",
"url": "https://github.com/solidjs/solid-docs/blob/HEAD/src/routes/solid-start/guides/data-mutation.mdx",
"metadata": {
"path": "src/routes/solid-start/guides/data-mutation.mdx",
"repo": "solidjs/solid-docs",
"repo_url": "https://github.com/solidjs/solid-docs.git",
"size": 14759,
"source_type": "github"
},
"hash": "41ce7b0949f37a5ea2d67472b0a765fb261415d44f0be5548cfc8709556093cb",
"timestamp": "2026-02-23T11:43:00.194203864+01:00"
}