Concepts

Actions

Edit this page

Many user interactions in an application involve changing data on the server. These mutations can be challenging to manage, as they require updates to the application's state and proper error handling. Actions simplify managing data mutations.

Actions provide several benefits:

  • Integrated state management: Solid Router automatically tracks the execution state of an action, simplifying reactive UI feedback.
  • Automatic data revalidation: After an action successfully completes, Solid Router revalidates relevant queries, ensuring the UI reflects the latest data.
  • Progressive enhancement: When used with HTML forms, actions enable functionality even if JavaScript is not yet loaded.

Defining actions

Actions are defined by wrapping the data-mutation logic with the action function.

import { action } from "@solidjs/router";
const createTicketAction = action(async (subject: string) => {
const response = await fetch("https://my-api.com/support/tickets", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ subject }),
});
if (!response.ok) {
const errorData = await response.json();
return { ok: false, message: errorData.message };
}
return { ok: true };
}, "createTicket");

In this example, an action is defined that creates a support ticket using a remote API.


Using actions

Actions can be triggered in two ways: using a HTML <form> element or programmatically using the useAction primitive.

The recommended approach is to use a <form> element. This ensures a robust user experience with progressive enhancement, since the form works even without JavaScript.

For cases where a form is not suitable, the useAction primitive can be used to trigger the action programmatically.

With the <form> element

Solid Router extends the standard HTML <form> element to work with actions. Form submissions can be handled using action by passing an action to the action prop.

Consider these points when using actions with <form>:

  1. The <form> element must have method="post".
  2. The action function will automatically receive the form's data as a FormData object as its first parameter.
  3. For SSR environments, a unique name must be provided as the second parameter to the action function. This name is used by Solid Router to identify and serialize the action across the client and server.
import { action } from "@solidjs/router";
const submitFeedbackAction = action(async (formData: FormData) => {
const message = formData.get("message")?.toString();
// ... Sends the feedback to the server.
}, "submitFeedback");
function FeedbackForm() {
return (
<form action={submitFeedbackAction} method="post">
<textarea name="message" placeholder="Message" />
<button type="submit">Send feedback</button>
</form>
);
}

In this example, when the form is submitted, submitFeedbackAction will be triggered with the FormData containing the form values.

Passing additional arguments

Sometimes, an action needs data that isn't part of the form's inputs. These additional arguments can be passed using the with method.

The with method creates a new action that wraps around the original action. When this new action is triggered, it forwards the arguments specified in the with method to the original action, followed by the FormData object.

import { action } from "@solidjs/router";
const updateProductAction = action(
async (productId: string, formData: FormData) => {
// ... Sends the updated fields to the server.
return { ok: true };
},
"updateProduct"
);
function EditProductForm(props: { productId: string }) {
return (
<form action={updateProductAction.with(props.productId)} method="post">
<input name="name" placeholder="Product name" />
<button type="submit">Save</button>
</form>
);
}

In this example, updateProductAction receives productId (passed via with), and then the formData from the form.

With the useAction primitive

For scenarios where a <form> element is not suitable, the useAction primitive provides a way to trigger an action programmatically. The useAction primitive takes an action as its parameter and returns a function that, when called, triggers the action with the provided arguments.

This approach requires client-side JavaScript and is not progressively enhanceable.

import { action, useAction } from "@solidjs/router";
const markNotificationReadAction = action(async (notificationId: string) => {
// ... Marks a notification as read on the server.
});
function NotificationItem(props: { id: string }) {
const markRead = useAction(markNotificationReadAction);
return <button onClick={() => markRead(props.id)}>Mark as read</button>;
}

In this example, markRead is a function that can be called with arguments matching markNotificationReadAction. When the button is clicked, the action is triggered with the provided arguments.


Tracking submission state

When an action is triggered, it creates a submission object. This object is a snapshot of the action's execution, containing its input, current status (pending or complete), and its final result or error. To access this state, Solid Router provides the useSubmission and useSubmissions primitives.

The useSubmission primitive tracks the state of the most recent submission for a specific action. This 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.

import { Show } from "solid-js";
import { action, useSubmission } from "@solidjs/router";
const updateSettingsAction = action(async (formData: FormData) => {
// ... Sends the settings data to the server.
}, "updateSettings");
function UserSettingsForm() {
const submission = useSubmission(updateSettingsAction);
return (
<form action={updateSettingsAction} method="post">
<input name="email" type="email" placeholder="Enter your email" />
<button disabled={submission.pending}>
{submission.pending ? "Saving..." : "Save settings"}
</button>
</form>
);
}

In this example, the form's submit button is disabled while submission.pending is true.


Handling errors

An action can fail for various reasons. A robust application must handle these failures gracefully. Solid Router provides two mechanisms for an action to signal failure: throwing an Error or returning a value.

Throwing an Error is a valid way to signal failure. Solid Router will catch the thrown error and make it available in the submission.error property. However, this approach has some drawbacks. The submission.error property is typed as any, which undermines type safety in the consuming component. It is also difficult to convey structured error information, such as validation messages for multiple form fields, using a simple Error instance.

For these reasons, the recommended practice is to always return a descriptive object from an action to represent its outcome. The returned object is available in the submission.result property, which will be fully typed. This makes handling different outcomes in the UI simple and safe.

import { Show } from "solid-js";
import { action, useSubmission } from "@solidjs/router";
const verifyTwoFactorAction = action(async (formData: FormData) => {
const code = formData.get("code")?.toString();
if (!code || code.length !== 6) {
return {
ok: false,
errors: { code: "Enter the 6-digit code from the authenticator app." },
};
}
// ... Verifies the code with the server and handles potential errors.
return { ok: true };
}, "verifyTwoFactor");
function TwoFactorForm() {
const submission = useSubmission(verifyTwoFactorAction);
const errors = () => {
const result = submission.result;
if (result && !result.ok) {
return result.errors;
}
};
return (
<form action={verifyTwoFactorAction} method="post">
<div>
<input name="code" placeholder="6-digit code" inputMode="numeric" />
<Show when={errors()?.code}>
<p>{errors().code}</p>
</Show>
</div>
<button type="submit" disabled={submission.pending}>
{submission.pending ? "Verifying..." : "Verify"}
</button>
</form>
);
}

In this example, the errors derived signal inspects submission.result to check for failures. If an errors object is found, its properties are used to conditionally render error messages next to the relevant form fields.


Automatic data revalidation

After server data changes, the application's can become stale. To solve this, Solid Router automatically revalidates all queries used in the same page after a successful action. This ensures any component using that data is automatically updated with the freshest information.

For 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.

import { For } from "solid-js";
import { query, action, createAsync } from "@solidjs/router";
const getDevicesQuery = query(async () => {
// ... Fetches the list of registered devices.
}, "devices");
const registerDeviceAction = action(async (formData: FormData) => {
// ... Registers a new device on the server.
}, "registerDevice");
function DevicesPage() {
// This query will automatically revalidate after registerDeviceAction completes.
const devices = createAsync(() => getDevicesQuery());
return (
<div>
<h2>Registered devices</h2>
<For each={devices()}>{(device) => <p>{device.name}</p>}</For>
<h3>Register new device</h3>
<form action={registerDeviceAction} method="post">
<input name="name" placeholder="Device name" />
<button type="submit">Register device</button>
</form>
</div>
);
}

While this automatic behavior is convenient for most cases, more fine-grained control may be needed. The next section explains how to customize or even disable this behavior for specific actions.


Managing navigation and revalidation

While automatic revalidation is powerful, more control is often needed. It may be desirable to redirect the user to a different page, prevent revalidation entirely, or revalidate a specific set of queries. This is where response helpers come in.

Response helpers are functions that create special Response objects. When an action returns or throws one of these responses, Solid Router intercepts it and performs a specific task.

Redirecting

To navigate the user to a new page after an action completes, the redirect helper can be used. It can also be used to revalidate specific queries upon redirection, which is useful for updating data that is displayed on the new page.

import { action, redirect } from "@solidjs/router";
import { useSession } from "vinxi/http";
const logoutAction = action(async () => {
"use server";
const session = await useSession({
password: process.env.SESSION_SECRET as string,
name: "session",
});
if (session.data.sessionId) {
await session.clear();
await db.session.delete({ id: sessionId });
}
throw redirect("/");
}, "logout");

In this example, after a successful login, the redirect helper is used to navigate to the dashboard. It also revalidates the "session" query to ensure the UI reflects the user's authenticated state.

Customizing revalidation

To override the default revalidation behavior, the reload and json helpers can be used.

  • reload is used when only revalidation needs to be customized.
  • json is used when revalidation needs to be controlled and data needs to be returned from the action.

Both helpers accept a revalidate option, which takes an array of query keys to revalidate. If an empty array ([]) is provided, revalidation is prevented altogether.

import { action, reload, json } from "@solidjs/router";
// Example 1: Revalidating a specific query
const savePreferencesAction = action(async () => {
// ... Saves the user preferences.
// Only revalidate the 'userPreferences' query
throw reload({ revalidate: ["userPreferences"] });
});
// Example 2: Disabling revalidation and returning data
const logActivityAction = action(async () => {
// ... Logs the activity to the server.
// Return without revalidating any queries
return json({ ok: true }, { revalidate: [] });
});

Optimistic UI

Optimistic UI is a pattern where the user interface is updated immediately after a user performs an operation. This is done without waiting for the server to confirm the operation's success. This approach makes an application feel faster and more responsive.

Actions can be combined with local state management to implement optimistic UI. The useSubmission primitive can be used to access the input of an action as it's being submitted. This input can be used to temporarily update the UI.

import { For, Show } from "solid-js";
import { query, action, createAsync, useSubmission } from "@solidjs/router";
const getCartQuery = query(async () => {
// ... Fetches the current shopping cart items.
}, "cart");
const addToCartAction = action(async (formData: FormData) => {
// ... Adds a product to the cart.
}, "addToCart");
function CartPage() {
const cart = createAsync(() => getCartQuery());
const submission = useSubmission(addToCartAction);
const optimisticCart = () => {
const originalItems = cart() ?? [];
if (submission.pending) {
const formData = submission.input[0] as FormData;
const productId = formData.get("productId")?.toString();
const name = formData.get("name")?.toString();
if (productId && name) {
// Add the optimistic line item with a temporary identifier.
return [...originalItems, { id: "temp", productId, name, quantity: 1 }];
}
}
return originalItems;
};
return (
<div>
<h2>Your cart</h2>
<For each={optimisticCart()}>{(item) => <p>{item.name}</p>}</For>
<h3>Add item</h3>
<form action={addToCartAction} method="post">
<input name="productId" placeholder="Product ID" />
<input name="name" placeholder="Product name" />
<button type="submit" disabled={submission.pending}>
{submission.pending ? "Adding..." : "Add to cart"}
</button>
</form>
</div>
);
}

In this example, a derived signal optimisticCart is created. When an action is pending, it checks the submission.input and adds the new cart item to the list with a temporary ID. If the action fails, submission.pending becomes false, and optimisticCart will revert to showing the original list from cart. When the action succeeds, Solid Router automatically revalidates getCartQuery and updates the UI with the confirmed cart state.

Report an issue with this page