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

17 lines
20 KiB
JSON

{
"id": "ce221c5e8e07e9e26c341923",
"source": "solid:signals",
"type": "github-document",
"title": "actions",
"content": "---\ntitle: Actions\nuse_cases: \u003e-\n form submissions, data mutations, server communication, user input handling,\n api calls, crud operations\ntags:\n - actions\n - forms\n - data\n - api\n - server\n - submission\n - mutations\nversion: '1.0'\ndescription: \u003e-\n Handle form submissions and server mutations with Solid Router actions. Build\n isomorphic data flows with progressive enhancement support.\n---\n\nMany user interactions in an application involve changing data on the server.\nThese **mutations** can be challenging to manage, as they require updates to the application's state and proper error handling.\nActions simplify managing data mutations.\n\nActions provide several benefits:\n\n- **Integrated state management:**\n Solid Router automatically tracks the execution state of an action, simplifying reactive UI feedback.\n- **Automatic data revalidation:**\n After an action successfully completes, Solid Router revalidates relevant [`queries`](/solid-router/data-fetching/queries), ensuring the UI reflects the latest data.\n- **Progressive enhancement:**\n When used with HTML forms, actions enable functionality even if JavaScript is not yet loaded.\n\n## Defining actions\n\nActions are defined by wrapping the data-mutation logic with the [`action` function](/solid-router/reference/data-apis/action).\n\n```tsx\nimport { action } from \"@solidjs/router\";\n\nconst createTicketAction = action(async (subject: string) =\u003e {\n\tconst response = await fetch(\"https://my-api.com/support/tickets\", {\n\t\tmethod: \"POST\",\n\t\theaders: { \"Content-Type\": \"application/json\" },\n\t\tbody: JSON.stringify({ subject }),\n\t});\n\n\tif (!response.ok) {\n\t\tconst errorData = await response.json();\n\t\treturn { ok: false, message: errorData.message };\n\t}\n\n\treturn { ok: true };\n}, \"createTicket\");\n```\n\nIn this example, an action is defined that creates a support ticket using a remote API.\n\n## Using actions\n\nActions can be triggered in two ways: using a HTML [`\u003cform\u003e` element](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/form) or programmatically using the [`useAction` primitive](/solid-router/reference/data-apis/use-action).\n\nThe recommended approach is to use a `\u003cform\u003e` element.\nThis ensures a robust user experience with progressive enhancement, since the form works even without JavaScript.\n\nFor cases where a form is not suitable, the [`useAction` primitive](/solid-router/reference/data-apis/use-action) can be used to trigger the action programmatically.\n\n### With the `\u003cform\u003e` element\n\nSolid Router extends the standard HTML `\u003cform\u003e` element to work with actions.\nForm submissions can be handled using action by passing an action to the `action` prop.\n\nConsider these points when using actions with `\u003cform\u003e`:\n\n1. The `\u003cform\u003e` element **must** have `method=\"post\"`.\n2. The action function will automatically receive the form's data as a [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) object as its first parameter.\n3. For SSR environments, a unique name **must** be provided as the second parameter to the `action` function.\n This name is used by Solid Router to identify and serialize the action across the client and server.\n\n```tsx\nimport { action } from \"@solidjs/router\";\n\nconst submitFeedbackAction = action(async (formData: FormData) =\u003e {\n\tconst message = formData.get(\"message\")?.toString();\n\t// ... Sends the feedback to the server.\n}, \"submitFeedback\");\n\nfunction FeedbackForm() {\n\treturn (\n\t\t\u003cform action={submitFeedbackAction} method=\"post\"\u003e\n\t\t\t\u003ctextarea name=\"message\" placeholder=\"Message\" /\u003e\n\t\t\t\u003cbutton type=\"submit\"\u003eSend feedback\u003c/button\u003e\n\t\t\u003c/form\u003e\n\t);\n}\n```\n\nIn this example, when the form is submitted, `submitFeedbackAction` will be triggered with the `FormData` containing the form values.\n\n:::tip[Uploading files]\nIf a form that includes file inputs, the `\u003cform\u003e` element must have `enctype=\"multipart/form-data\"` to correctly send the file data.\n\n```tsx\n\u003cform action={uploadFileAction} method=\"post\" enctype=\"multipart/form-data\"\u003e\n\t\u003cinput type=\"file\" name=\"myFile\" /\u003e\n\t\u003cbutton type=\"submit\"\u003eUpload\u003c/button\u003e\n\u003c/form\u003e\n```\n\n:::\n\n#### Passing additional arguments\n\nSometimes, an action needs data that isn't part of the form's inputs.\nThese additional arguments can be passed using the `with` method.\n\nThe `with` method creates a new action that wraps around the original action.\nWhen this new action is triggered, it forwards the arguments specified in the `with` method to the original action, followed by the `FormData` object.\n\n```tsx\nimport { action } from \"@solidjs/router\";\n\nconst updateProductAction = action(\n\tasync (productId: string, formData: FormData) =\u003e {\n\t\t// ... Sends the updated fields to the server.\n\n\t\treturn { ok: true };\n\t},\n\t\"updateProduct\"\n);\n\nfunction EditProductForm(props: { productId: string }) {\n\treturn (\n\t\t\u003cform action={updateProductAction.with(props.productId)} method=\"post\"\u003e\n\t\t\t\u003cinput name=\"name\" placeholder=\"Product name\" /\u003e\n\t\t\t\u003cbutton type=\"submit\"\u003eSave\u003c/button\u003e\n\t\t\u003c/form\u003e\n\t);\n}\n```\n\nIn this example, `updateProductAction` receives `productId` (passed via `with`), and then the `formData` from the form.\n\n### With the `useAction` primitive\n\nFor scenarios where a `\u003cform\u003e` element is not suitable, the `useAction` primitive provides a way to trigger an action programmatically.\nThe `useAction` primitive takes an action as its parameter and returns a function that, when called, triggers the action with the provided arguments.\n\nThis approach requires client-side JavaScript and is not progressively enhanceable.\n\n```tsx\nimport { action, useAction } from \"@solidjs/router\";\n\nconst markNotificationReadAction = action(async (notificationId: string) =\u003e {\n\t// ... Marks a notification as read on the server.\n});\n\nfunction NotificationItem(props: { id: string }) {\n\tconst markRead = useAction(markNotificationReadAction);\n\n\treturn \u003cbutton onClick={() =\u003e markRead(props.id)}\u003eMark as read\u003c/button\u003e;\n}\n```\n\nIn this example, `markRead` is a function that can be called with arguments matching `markNotificationReadAction`.\nWhen the button is clicked, the action is triggered with the provided arguments.\n\n## Tracking submission state\n\nWhen an action is triggered, it creates a **submission** object.\nThis object is a snapshot of the action's execution, containing its input, current status (pending or complete), and its final result or error.\nTo access this state, Solid Router provides the [`useSubmission`](/solid-router/reference/data-apis/use-submission) and [`useSubmissions`](/solid-router/reference/data-apis/use-submissions) primitives.\n\nThe `useSubmission` primitive tracks the state of the _most recent_ submission for a specific action.\nThis is ideal for most use cases, such as disabling a form's submit button while the action is pending or displaying a confirmation message upon success.\n\n```tsx\nimport { Show } from \"solid-js\";\nimport { action, useSubmission } from \"@solidjs/router\";\n\nconst updateSettingsAction = action(async (formData: FormData) =\u003e {\n\t// ... Sends the settings data to the server.\n}, \"updateSettings\");\n\nfunction UserSettingsForm() {\n\tconst submission = useSubmission(updateSettingsAction);\n\n\treturn (\n\t\t\u003cform action={updateSettingsAction} method=\"post\"\u003e\n\t\t\t\u003cinput name=\"email\" type=\"email\" placeholder=\"Enter your email\" /\u003e\n\n\t\t\t\u003cbutton disabled={submission.pending}\u003e\n\t\t\t\t{submission.pending ? \"Saving...\" : \"Save settings\"}\n\t\t\t\u003c/button\u003e\n\t\t\u003c/form\u003e\n\t);\n}\n```\n\nIn this example, the form's submit button is disabled while `submission.pending` is `true`.\n\n:::tip\nTo track multiple submissions for a single action, such as in a multi-file uploader interface, the [`useSubmissions` primitive](/solid-router/reference/data-apis/use-submissions) can be used.\n:::\n\n## Handling errors\n\nAn action can fail for various reasons.\nA robust application must handle these failures gracefully.\nSolid Router provides two mechanisms for an action to signal failure: throwing an `Error` or returning a value.\n\nThrowing an `Error` is a valid way to signal failure.\nSolid Router will catch the thrown error and make it available in the `submission.error` property.\nHowever, this approach has some drawbacks.\nThe `submission.error` property is typed as `any`, which undermines type safety in the consuming component.\nIt is also difficult to convey structured error information, such as validation messages for multiple form fields, using a simple `Error` instance.\n\nFor these reasons, the recommended practice is to always `return` a descriptive object from an action to represent its outcome.\nThe returned object is available in the `submission.result` property, which will be fully typed.\nThis makes handling different outcomes in the UI simple and safe.\n\n```tsx\nimport { Show } from \"solid-js\";\nimport { action, useSubmission } from \"@solidjs/router\";\n\nconst verifyTwoFactorAction = action(async (formData: FormData) =\u003e {\n\tconst code = formData.get(\"code\")?.toString();\n\n\tif (!code || code.length !== 6) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\terrors: { code: \"Enter the 6-digit code from the authenticator app.\" },\n\t\t};\n\t}\n\n\t// ... Verifies the code with the server and handles potential errors.\n\n\treturn { ok: true };\n}, \"verifyTwoFactor\");\n\nfunction TwoFactorForm() {\n\tconst submission = useSubmission(verifyTwoFactorAction);\n\n\tconst errors = () =\u003e {\n\t\tconst result = submission.result;\n\t\tif (result \u0026\u0026 !result.ok) {\n\t\t\treturn result.errors;\n\t\t}\n\t};\n\n\treturn (\n\t\t\u003cform action={verifyTwoFactorAction} method=\"post\"\u003e\n\t\t\t\u003cdiv\u003e\n\t\t\t\t\u003cinput name=\"code\" placeholder=\"6-digit code\" inputMode=\"numeric\" /\u003e\n\t\t\t\t\u003cShow when={errors()?.code}\u003e\n\t\t\t\t\t\u003cp\u003e{errors().code}\u003c/p\u003e\n\t\t\t\t\u003c/Show\u003e\n\t\t\t\u003c/div\u003e\n\n\t\t\t\u003cbutton type=\"submit\" disabled={submission.pending}\u003e\n\t\t\t\t{submission.pending ? \"Verifying...\" : \"Verify\"}\n\t\t\t\u003c/button\u003e\n\t\t\u003c/form\u003e\n\t);\n}\n```\n\nIn this example, the `errors` derived signal inspects `submission.result` to check for failures.\nIf an `errors` object is found, its properties are used to conditionally render error messages next to the relevant form fields.\n\n:::caution[Always return a value]\nIt is important that an action consistently returns a value from all of its possible code paths.\nBecause, if an action returns `undefined` or `null`, Solid Router removes that submission from its internal list upon completion.\nThis can lead to unexpected behavior.\n\nFor example, consider an action that returns an error object on failure but returns nothing on success.\nIf the action fails once, `useSubmission` will correctly report the error.\nHowever, if a subsequent submission succeeds, it will be removed from the list, and `useSubmission` will continue to report the previous stale error state.\nTo prevent this, ensure every code path in an action returns a value, such as `{ ok: true }` to indicate a successful outcome.\n:::\n\n## Automatic data revalidation\n\nAfter server data changes, the application's can become stale.\nTo solve this, Solid Router automatically revalidates all [queries](/solid-router/data-fetching/queries) used in the same page after a successful action.\nThis ensures any component using that data is automatically updated with the freshest information.\n\nFor example, if a page displays a list of registered devices and includes a form to register a new one, the list will automatically update after the form is submitted.\n\n```tsx\nimport { For } from \"solid-js\";\nimport { query, action, createAsync } from \"@solidjs/router\";\n\nconst getDevicesQuery = query(async () =\u003e {\n\t// ... Fetches the list of registered devices.\n}, \"devices\");\n\nconst registerDeviceAction = action(async (formData: FormData) =\u003e {\n\t// ... Registers a new device on the server.\n}, \"registerDevice\");\n\nfunction DevicesPage() {\n\t// This query will automatically revalidate after registerDeviceAction completes.\n\tconst devices = createAsync(() =\u003e getDevicesQuery());\n\n\treturn (\n\t\t\u003cdiv\u003e\n\t\t\t\u003ch2\u003eRegistered devices\u003c/h2\u003e\n\t\t\t\u003cFor each={devices()}\u003e{(device) =\u003e \u003cp\u003e{device.name}\u003c/p\u003e}\u003c/For\u003e\n\n\t\t\t\u003ch3\u003eRegister new device\u003c/h3\u003e\n\t\t\t\u003cform action={registerDeviceAction} method=\"post\"\u003e\n\t\t\t\t\u003cinput name=\"name\" placeholder=\"Device name\" /\u003e\n\t\t\t\t\u003cbutton type=\"submit\"\u003eRegister device\u003c/button\u003e\n\t\t\t\u003c/form\u003e\n\t\t\u003c/div\u003e\n\t);\n}\n```\n\nWhile this automatic behavior is convenient for most cases, more fine-grained control may be needed.\nThe next section explains how to customize or even disable this behavior for specific actions.\n\n## Managing navigation and revalidation\n\nWhile automatic revalidation is powerful, more control is often needed.\nIt may be desirable to redirect the user to a different page, prevent revalidation entirely, or revalidate a specific set of queries.\nThis is where response helpers come in.\n\nResponse helpers are functions that create special [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) objects.\nWhen an action returns or throws one of these responses, Solid Router intercepts it and performs a specific task.\n\n### Redirecting\n\nTo navigate the user to a new page after an action completes, the [`redirect` helper](/solid-router/reference/response-helpers/redirect) can be used.\nIt can also be used to revalidate specific queries upon redirection, which is useful for updating data that is displayed on the new page.\n\n```tsx\nimport { action, redirect } from \"@solidjs/router\";\nimport { useSession } from \"vinxi/http\";\n\nconst logoutAction = action(async () =\u003e {\n\t\"use server\";\n\tconst session = await useSession({\n\t\tpassword: process.env.SESSION_SECRET as string,\n\t\tname: \"session\",\n\t});\n\n\tif (session.data.sessionId) {\n\t\tawait session.clear();\n\t\tawait db.session.delete({ id: sessionId });\n\t}\n\n\tthrow redirect(\"/\");\n}, \"logout\");\n```\n\nIn this example, after a successful login, the `redirect` helper is used to navigate to the dashboard.\nIt also revalidates the \"session\" query to ensure the UI reflects the user's authenticated state.\n\n### Customizing revalidation\n\nTo override the default revalidation behavior, the [`reload`](/solid-router/reference/response-helpers/reload) and [`json`](/solid-router/reference/response-helpers/json) helpers can be used.\n\n- `reload` is used when only revalidation needs to be customized.\n- `json` is used when revalidation needs to be controlled _and_ data needs to be returned from the action.\n\nBoth helpers accept a `revalidate` option, which takes an array of query keys to revalidate.\nIf an empty array (`[]`) is provided, revalidation is prevented altogether.\n\n```tsx\nimport { action, reload, json } from \"@solidjs/router\";\n\n// Example 1: Revalidating a specific query\nconst savePreferencesAction = action(async () =\u003e {\n\t// ... Saves the user preferences.\n\n\t// Only revalidate the 'userPreferences' query\n\tthrow reload({ revalidate: [\"userPreferences\"] });\n});\n\n// Example 2: Disabling revalidation and returning data\nconst logActivityAction = action(async () =\u003e {\n\t// ... Logs the activity to the server.\n\n\t// Return without revalidating any queries\n\treturn json({ ok: true }, { revalidate: [] });\n});\n```\n\n:::tip[Throwing vs. Returning]\nA response helper can be either `return`ed or `throw`n.\nIn TypeScript, `throw` can be more convenient, as it avoids potential type conflicts with an action's expected return value.\n:::\n\n## Optimistic UI\n\nOptimistic UI is a pattern where the user interface is updated immediately after a user performs an operation.\nThis is done without waiting for the server to confirm the operation's success.\nThis approach makes an application feel faster and more responsive.\n\nActions can be combined with local state management to implement optimistic UI.\nThe `useSubmission` primitive can be used to access the input of an action as it's being submitted.\nThis input can be used to temporarily update the UI.\n\n```tsx\nimport { For, Show } from \"solid-js\";\nimport { query, action, createAsync, useSubmission } from \"@solidjs/router\";\n\nconst getCartQuery = query(async () =\u003e {\n\t// ... Fetches the current shopping cart items.\n}, \"cart\");\n\nconst addToCartAction = action(async (formData: FormData) =\u003e {\n\t// ... Adds a product to the cart.\n}, \"addToCart\");\n\nfunction CartPage() {\n\tconst cart = createAsync(() =\u003e getCartQuery());\n\tconst submission = useSubmission(addToCartAction);\n\n\tconst optimisticCart = () =\u003e {\n\t\tconst originalItems = cart() ?? [];\n\t\tif (submission.pending) {\n\t\t\tconst formData = submission.input[0] as FormData;\n\t\t\tconst productId = formData.get(\"productId\")?.toString();\n\t\t\tconst name = formData.get(\"name\")?.toString();\n\t\t\tif (productId \u0026\u0026 name) {\n\t\t\t\t// Add the optimistic line item with a temporary identifier.\n\t\t\t\treturn [...originalItems, { id: \"temp\", productId, name, quantity: 1 }];\n\t\t\t}\n\t\t}\n\t\treturn originalItems;\n\t};\n\n\treturn (\n\t\t\u003cdiv\u003e\n\t\t\t\u003ch2\u003eYour cart\u003c/h2\u003e\n\t\t\t\u003cFor each={optimisticCart()}\u003e{(item) =\u003e \u003cp\u003e{item.name}\u003c/p\u003e}\u003c/For\u003e\n\n\t\t\t\u003ch3\u003eAdd item\u003c/h3\u003e\n\t\t\t\u003cform action={addToCartAction} method=\"post\"\u003e\n\t\t\t\t\u003cinput name=\"productId\" placeholder=\"Product ID\" /\u003e\n\t\t\t\t\u003cinput name=\"name\" placeholder=\"Product name\" /\u003e\n\t\t\t\t\u003cbutton type=\"submit\" disabled={submission.pending}\u003e\n\t\t\t\t\t{submission.pending ? \"Adding...\" : \"Add to cart\"}\n\t\t\t\t\u003c/button\u003e\n\t\t\t\u003c/form\u003e\n\t\t\u003c/div\u003e\n\t);\n}\n```\n\nIn this example, a derived signal `optimisticCart` is created.\nWhen an action is pending, it checks the `submission.input` and adds the new cart item to the list with a temporary ID.\nIf the action fails, `submission.pending` becomes false, and `optimisticCart` will revert to showing the original list from `cart`.\nWhen the action succeeds, Solid Router automatically revalidates `getCartQuery` and updates the UI with the confirmed cart state.\n\n:::note\nFor more advanced patterns, consider using [TanStack Query](https://tanstack.com/query/latest/docs/framework/solid/guides/optimistic-updates).\nIt provides robust tools for managing server state, including cache-based optimistic updates.\n:::",
"url": "https://github.com/solidjs/solid-docs/blob/HEAD/src/routes/solid-router/concepts/actions.mdx",
"metadata": {
"path": "src/routes/solid-router/concepts/actions.mdx",
"repo": "solidjs/solid-docs",
"repo_url": "https://github.com/solidjs/solid-docs.git",
"size": 17645,
"source_type": "github"
},
"hash": "ae6256a35d5657eacfffdba199f816a1d286a0735ddeac05e0c6d5c7f5a9a4fe",
"timestamp": "2026-02-23T11:43:00.19155187+01:00"
}