Why Would I Use An Action For Anything Other Than Form Submissions?

I’m struggling to understand why I would use an action for anything other than handling a form submission. Actions give you access to FormData, which simplifies working with forms. So, I can see a clear use case with forms. But why else would I use an action? Let me try to add some context to my confusion. The solid docs give the following example. I can use an action to do the following
import { action, useAction } from "@solidjs/router";

const echo = action(async (message: string) => {
// Imagine this is a call to fetch
await new Promise((resolve, reject) => setTimeout(resolve, 1000));
console.log(message);
});

export default function MyComponent() {
const myEcho = useAction(echo);
myEcho("Hello from solid!");
}
import { action, useAction } from "@solidjs/router";

const echo = action(async (message: string) => {
// Imagine this is a call to fetch
await new Promise((resolve, reject) => setTimeout(resolve, 1000));
console.log(message);
});

export default function MyComponent() {
const myEcho = useAction(echo);
myEcho("Hello from solid!");
}
BUT, I could also use a regular async function to achieve the same outcome in fewer lines of code.
async function echo(message: string) {
await new Promise((resolve, reject) => setTimeout(resolve, 1000));
console.log(message);
}

export default function MyComponent() {
echo("Hello from solid!");
}
async function echo(message: string) {
await new Promise((resolve, reject) => setTimeout(resolve, 1000));
console.log(message);
}

export default function MyComponent() {
echo("Hello from solid!");
}
Additionally, the docs say: "after submitting data the server sends some data back as well. Usually an error message if something failed. Anything returned from your action function can be accessed using the reactive action.result property. " BUT, I can also use a try/catch block in a regular async function to return an error message. So, what's the point of actions outside of working with forms? Am I missing something? Or are actions just an alternative way to do what regular async functions can do? Thanks, Chris
16 Replies
brenelz
brenelz3mo ago
They have router knowledge so it can revalidate the data that was changed in the action
peerreynders
peerreynders3mo ago
The beta documentation is catching up. From the solid-router README.md
import { action, revalidate, redirect } from "@solidjs/router"

// anywhere
const myAction = action(async (data) => {
await doMutation(data);
throw redirect("/", { revalidate: getUser.keyFor(data.id) }); // throw a response to do a redirect
});
import { action, revalidate, redirect } from "@solidjs/router"

// anywhere
const myAction = action(async (data) => {
await doMutation(data);
throw redirect("/", { revalidate: getUser.keyFor(data.id) }); // throw a response to do a redirect
});
GitHub
GitHub - solidjs/solid-router: A universal router for Solid inspire...
A universal router for Solid inspired by Ember and React Router - solidjs/solid-router
peerreynders
peerreynders3mo ago
And now single flight mutations are supported https://github.com/solidjs/solid-start/releases/tag/v0.6.0
GitHub
Release v0.6.0 - Take Flight · solidjs/solid-start
Listening carefully to the feedback and reflecting on the second month of beta 2 we have prepared this release which improves on a lot of the usability around Start itself. This is less about featu...
ChrisThornham
ChrisThornham3mo ago
@peerreynders and @brenelz couldn’t I also do that with createResource and an async function?
async function fetchData(source, { value, refetching }) {
// Fetch the data and return a value.
}

const [data, { mutate, refetch }] = createResource(getQuery, fetchData);

// MUTATION AND VALIDATION FUNCTION
async function updateData(data) {
// do mutation
doMuation(data);
// refetch data
refetch();
}
async function fetchData(source, { value, refetching }) {
// Fetch the data and return a value.
}

const [data, { mutate, refetch }] = createResource(getQuery, fetchData);

// MUTATION AND VALIDATION FUNCTION
async function updateData(data) {
// do mutation
doMuation(data);
// refetch data
refetch();
}
Additionally, couldn't I use useNavigate for redirection?
Brendonovich
Brendonovich3mo ago
by default when an action succeeds, it will invalidate all cache functions, which is more powerful than defining your fetchData, then wrapping it in updateData where you have to manually call refetch on each resource you care about. sure, but then you've got another need for that wrapper function that now must be defined inside a component (since useNavigate requires access to router context), whereas using an action lets you issue the redirect inline with everything else you need to do
peerreynders
peerreynders3mo ago
TL;DR: cache, createAsync, and action are largely about moving the responsibility of loading and aggregation of page data out of the individual component into the router which has more knowledge about the whole page than each individual component. As a REST-osaur I see coordinating the loading of the page-level data based on the client side URL as one of the router's responsibilities which mirrors the server preparing of “Resource's Representation of State” based on the requested URL. Long Story: In 2013 React's “Just the V in MVC” didn't want to deal with routing concerns. Shortly thereafter Redux's Flux) implementation was the first adoption of a practice of moving data loading out of the components and instead have them largely “project” application state instead. Ultimately that approach fell out of favour due to the inconvenience of maintaining the actions and selectors (while not taking advantage of distinguishing between essential state as indicated by the client-side route/URL and ephemeral state which only has short term, session specific relevance). Fast foward to 2022 When To Fetch: Remixing React Router; i.e. fetch before you render, rather than fetch because you render. Now RSCs, being dogmatically component-oriented, still downplay server waterfalls, again de-emphasizing the importance of leveraging the router's knowledge. Some of this could be explained by React Native culture. With native apps there is no notion of a URL which demarcates essential from ephemeral state because there is no SSR and it isn't possible to capture navigation state which can be bookmarked or shared with others. But in the web world URLs have significance so you may as well milk them for all they are worth. In the case of the Router the URL determines which data is going to be needed so you may as well start loading it even before you render.
Meta Developers
YouTube
Hacker Way: Rethinking Web App Development at Facebook
Delivering reliable, high-performance web experiences at Facebook's scale has required us to challenge some long-held assumptions about software development. Join us to learn how we abandoned the traditional MVC paradigm in favor of a more functional application architecture.
Real World React
YouTube
When To Fetch: Remixing React Router - Ryan Florence
Recorded live at Reactathon 2022. Learn more at https://reactathon.com When To Fetch: Remixing React Router We've learned that fetching in components is the quickest way to the worst UX. But it's not just the UX that suffers, the developer experience of fetching in components creates a lot of incidental complexity too: data fetching, data muta...
peerreynders
peerreynders3mo ago
So while createAsync makes it look like the component is loading the data, the intention is do so over a (router's) cached loader/value which gives the router just enough control to help orchestrating the page data loading. The actions in turn can invalidate the caches so that the router can trigger the necessary updates which once they settle will propagate to the dependent createAsyncs. With createResouce you have to trigger the refetch directly on the resource and unless that resource is managed by a context it is specific to a component. cache encourages you to "pull out" the fetch from the component and into the router. Now multiple components can depend via createAsync on the same cached value while any other component can invalidate the core cache value with an action; consequently all the dependent createAsync signals will update once the cache value is reloaded.
ChrisThornham
ChrisThornham3mo ago
Thank you for your very thorough response. I think I’m struggling here for the following reason: I learned about full-stack web dev the traditional way using a backend like Express or Go. I’m trying to draw parallels between traditional web dev and web dev with meta frameworks and I can’t see where Actions fit in. Let me explain using three simple examples. Then I'll explain why I'm confused. EXAMPLE 1: I’ll start with what I would consider the Web Server or UI routes. Using Express, here’s how you might handle a simple static Home Page route.
// Router
app.get('/', handleHomePage);

// Route Handlers
function handleHomePage(req, res) {
res.send('<h1>Home Page</h1>');
}
// Router
app.get('/', handleHomePage);

// Route Handlers
function handleHomePage(req, res) {
res.send('<h1>Home Page</h1>');
}
The equivalent of this with Solid and Solid Router might look like this:
// Router
<Router>
<Route path="/" component={Home} />
</Router>

// UI Component “Handler”
export default function Home() {
return (
<div>
<h1>Home Page</h1>
</div>
);
}
// Router
<Router>
<Route path="/" component={Home} />
</Router>

// UI Component “Handler”
export default function Home() {
return (
<div>
<h1>Home Page</h1>
</div>
);
}
EXAMPLE 2 If I want a dynamic page I can use the “Handler” to fetch the data. Using Express that might look like this:
// Router
app.get('/account', handleAccountPage);

// Handler function for the Account Page
function handleAccountPage(req, res) {
// Get User Data
const user = await getUserData();
// Respond with a greeting message
res.send(`<p>Hello ${user.name},</p>`);
}
// Router
app.get('/account', handleAccountPage);

// Handler function for the Account Page
function handleAccountPage(req, res) {
// Get User Data
const user = await getUserData();
// Respond with a greeting message
res.send(`<p>Hello ${user.name},</p>`);
}
In Solid that might look like this:
// Router
<Router>
<Route path="/account" component={Account} />
</Router>

// Account Page Component “Handler”
export default async function Account() {
// Get User Data
const user = await getUserData();
return (
<div>
<p>Hello {user.name},</p>
</div>
);
}
// Router
<Router>
<Route path="/account" component={Account} />
</Router>

// Account Page Component “Handler”
export default async function Account() {
// Get User Data
const user = await getUserData();
return (
<div>
<p>Hello {user.name},</p>
</div>
);
}
One of the benefits of SolidStart is it adds file-based routing with the <FileRoutes /> component. This lets me define routes in a folder rather than defining each Route inside a Routes component. SolidStart lets me define both UI routes and API routes. EXAMPLE 3: Next let’s consider API Routes. Here’s a simple example with express for adding data to a db: In Express that might look like this:
// API Route to Add Data
app.post('/api/addData’, (req, res) => {
// Step 1: Extract data from request body
// Step 2: Validate data
// Step 3: Add data to database
// Step 4: Respond with the new data

// Placeholder for actual logic
res.status(200).json({ message: 'Data added' });
});
// API Route to Add Data
app.post('/api/addData’, (req, res) => {
// Step 1: Extract data from request body
// Step 2: Validate data
// Step 3: Add data to database
// Step 4: Respond with the new data

// Placeholder for actual logic
res.status(200).json({ message: 'Data added' });
});
In SolidStart that might look like this:
// In routes/api/addData.ts

export function POST() {
// Step 1: Extract data from request body
// Step 2: Validate data
// Step 3: Add data to database
// Step 4: Respond with the new data

return new Response("Data added", { status: 200 });
}
// In routes/api/addData.ts

export function POST() {
// Step 1: Extract data from request body
// Step 2: Validate data
// Step 3: Add data to database
// Step 4: Respond with the new data

return new Response("Data added", { status: 200 });
}
With these three examples, I can do most things I need to do in web dev. I can serve static and dynamic pages, I can run async functions for data fetching, and I can run code on the server with API routes for doing “server” things like db interactions and talking to stripe. So, where do Actions fit in? The docs say that Actions are just POST requests, so why do I need them? Can’t I just use a POST request in an API route? I apologize if the answer to this seems obvious, but I’m pretty new to this stuff. There’s a good chance I’m missing a fundamental understanding of something. Both you and @brenelz mentioned moving the responsibility of loading and aggregating page data out of the individual component and into the router, but my understanding of this concept is weak, and I can’t draw a parallel to what this would look like in an Express app. You said: I see coordinating the loading of the page-level data based on the client side URL as one of the router's responsibilities... How do I do that in Solid? And, to expand upon my three examples above, how would I do the equivalent in an Express app? Wait... does the action function in a UI route represent a POST request to that route? And does moving data loading to a POST request for a route move the responsibility of loading and aggregating data out of the component and into the router? So assuming my path is routes/mycomponent...
import { action, useAction } from "@solidjs/router";

// THIS IS THE POST REQUEST HANDLER FOR THE ROUTE
const echo = action(async (message: string) => {
// HANDLING DATA HERE MOVES DATA LOADING AND AGGREGATION
// OUT OF THE COMPONENT AND INTO THE ROUTER
await new Promise((resolve, reject) => setTimeout(resolve, 1000));
console.log(message);
});

// THIS IS THE GET HANDLER FOR THE ROUTE
export function MyComponent() {
// CODE HERE IS INSIDE THE COMPONENT
// FETCHING DATA HERE WOULD BE LOADING AND AGGREGATING
// DATA INSIDE THE COMPONENT
const myEcho = useAction(echo);

// CALLING myEcho SENDS A POST REQUEST TO THIS ROUTE
myEcho("Hello from solid!");
}
import { action, useAction } from "@solidjs/router";

// THIS IS THE POST REQUEST HANDLER FOR THE ROUTE
const echo = action(async (message: string) => {
// HANDLING DATA HERE MOVES DATA LOADING AND AGGREGATION
// OUT OF THE COMPONENT AND INTO THE ROUTER
await new Promise((resolve, reject) => setTimeout(resolve, 1000));
console.log(message);
});

// THIS IS THE GET HANDLER FOR THE ROUTE
export function MyComponent() {
// CODE HERE IS INSIDE THE COMPONENT
// FETCHING DATA HERE WOULD BE LOADING AND AGGREGATING
// DATA INSIDE THE COMPONENT
const myEcho = useAction(echo);

// CALLING myEcho SENDS A POST REQUEST TO THIS ROUTE
myEcho("Hello from solid!");
}
peerreynders
peerreynders3mo ago
does the action function in a UI route represent a POST request to that route?
No. In SolidStart they are used with server functions, so they are essentially RPC calls where Start handles the serialization and fetch for you. By itself it doesn't actually interact with the router but the router's data API does give it the opportunity to do so if you choose to. What happens is that an addData API route is replaced with an addData action - no need for separately coded API routes. The action can invalidate the cached data to let the router know that it's stale. Based on the active createAsync dependencies the router will then decide to perform a fetch to refresh the cache which in turn will propagate to the createAsync signals once the fetch has settled. Preloads can similarly benefit from the route's cache management by warming the cache even before the components need it.
peerreynders
peerreynders3mo ago
// Router
app.get('/account', handleAccountPage);

// Handler function for the Account Page
function handleAccountPage(req, res) {
// Get User Data
const user = await getUserData();
// Respond with a greeting message
res.send(`<p>Hello ${user.name},</p>`);
}
// Router
app.get('/account', handleAccountPage);

// Handler function for the Account Page
function handleAccountPage(req, res) {
// Get User Data
const user = await getUserData();
// Respond with a greeting message
res.send(`<p>Hello ${user.name},</p>`);
}
I think the issue here is that you are equating handleAccountPage to a component or worse components to miniature pages. SolidJS is attempting to move "beyond components". The idea is to initiate the client side fetch as soon as we know where we are navigating to (which precisely identifies the data requirements), and which router cached data is stale (because the action should be aware of its consequences), but before even thinking about components. The goal is to have fetched data drive the components rather than having the components drive data fetching largely because the latter provides a worse UX. Think of createAsync as the component identifying it's data requirements based on the routes cached values. Once a component is rendered to the DOM the createAsync signal will keep it in sync whenever the router refreshes the cached value. Then any action anywhere in the client side app can notify the router which cached values are affected by it so that it can refresh them as necessary. Cache updates automatically trigger transitions so there will be paint-holding under the suspense boundaries which is generally preferred to spinners provided you can update within 1s.
Chrome for Developers
Paint Holding - reducing the flash of white on same-origin navigati...
A quick overview of paint holding. A Chrome feature for reducing the flash of white on same-origin navigations
peerreynders
peerreynders3mo ago
From the docs
const getUser = cache(async (id) => {
return (await fetch(`/api/users${id}`)).json()
}, "users") // used as cache key + serialized arguments
const getUser = cache(async (id) => {
return (await fetch(`/api/users${id}`)).json()
}, "users") // used as cache key + serialized arguments
In SolidStart you would use
const getUser = cache(async (id) => {
'use server';
return getUserOnServer(id)
}, "users")
const getUser = cache(async (id) => {
'use server';
return getUserOnServer(id)
}, "users")
because the is no need for an API route. The cache wrapper lets the router rerun the "fetch" whenever it needs to refresh the value. Now inside a component you consume this with createAsync
import { getUser } from './api';

const user = createAsync(() => getUser(params.id));
import { getUser } from './api';

const user = createAsync(() => getUser(params.id));
The user signal will now get the latest value whenever the router reruns that "fetch" registered with the cache. Now perhaps in an entirely different component:
import { action, revalidate, redirect } from "@solidjs/router"
import { getUser } from './api' // i.e. the router's cache wrapper

const myAction = action(async (userData) => {
// doMutation can be a server
// function that replaces an
// API route end point access
await doMutation(userData);
throw redirect("/", { revalidate: getUser.keyFor(userData.id) });
// 1. the action tells the router to navigate to '/'
// 2. the action marks the router's `getUser` cache
// as stale so it will refresh it if necessary
});
import { action, revalidate, redirect } from "@solidjs/router"
import { getUser } from './api' // i.e. the router's cache wrapper

const myAction = action(async (userData) => {
// doMutation can be a server
// function that replaces an
// API route end point access
await doMutation(userData);
throw redirect("/", { revalidate: getUser.keyFor(userData.id) });
// 1. the action tells the router to navigate to '/'
// 2. the action marks the router's `getUser` cache
// as stale so it will refresh it if necessary
});
"use server"
ChrisThornham
ChrisThornham3mo ago
@peerreynders You’ve been incredibly helpful. Thank you! I think I’m starting to get this. I also watched Ryan’s video, which helped: https://www.youtube.com/watch?v=RzL4N3ZavxU&t=7078s You said: *The goal is to have fetched data drive the components rather than having the components drive data fetching largely because the latter provides a worse UX. * That makes a lot of sense. Taking things one step at at time here, I’ll start with loading data. Here’s my understanding of how to load data OUTSIDE of the component. Does this pattern look correct?
import { cache, createAsync } from "@solidjs/router";
import { For } from "solid-js";

// define Todos Type
type Todos = {
userId: number;
id: number;
title: string;
completed: boolean;
};

// Use the cache API to load page data on the server OUTSIDE of the component
const getTodos = cache(async () => {
"use server";
const response = await fetch("https://jsonplaceholder.typicode.com/todos");
return (await response.json()) as Todos[];
}, "todos");

// Call the load function
export const route = {
load: () => getTodos(),
};

export default function About() {
// consume here with createAsync
const todos = createAsync(() => getTodos());

return (
<main>
<h1>Todos Page</h1>
<For each={todos()}>
{(todo) => (
<div>
<div>Title: {todo.title}</div>
</div>
)}
</For>
</main>
);
}
import { cache, createAsync } from "@solidjs/router";
import { For } from "solid-js";

// define Todos Type
type Todos = {
userId: number;
id: number;
title: string;
completed: boolean;
};

// Use the cache API to load page data on the server OUTSIDE of the component
const getTodos = cache(async () => {
"use server";
const response = await fetch("https://jsonplaceholder.typicode.com/todos");
return (await response.json()) as Todos[];
}, "todos");

// Call the load function
export const route = {
load: () => getTodos(),
};

export default function About() {
// consume here with createAsync
const todos = createAsync(() => getTodos());

return (
<main>
<h1>Todos Page</h1>
<For each={todos()}>
{(todo) => (
<div>
<div>Title: {todo.title}</div>
</div>
)}
</For>
</main>
);
}
Ryan Carniato
YouTube
SolidStart: The Shape of Frameworks to Come
Join me to take a look at what SolidStart is shaping up to be. I will be going through a full tour of the framework to show you what it is, what you can do with it, and how it changes things. [0:00:00] Intro [0:05:30] The Shape of Frameworks to Come [0:18:00] A Classic Client-Side Solid App [0:30:45] Simple Todo App with a Router & a Form [0:45...
ChrisThornham
ChrisThornham3mo ago
Here's the output on the todos page.
ChrisThornham
ChrisThornham3mo ago
And here's the todos data that loaded on hover from the home page:
No description
No description
peerreynders
peerreynders3mo ago
I think now the load function exists to warm the cache even before the components load. The original beta had an explicit route loading mechanism however more recently it became clear that component loading is irresistible to a significant segment of the developer audience. Consequently the cache/createAsync/action API emerged to create the opportunities for route loading by anchoring the cached values within the router, while createAsync still created the component loading feel while actually hooking into the router's data loading mechanism. A lot of the reasoning that went into the API was discussed in https://youtu.be/8ObxzMSIqKA and to some degree https://youtu.be/veKm9MDVVg8 So far you've set up the reactive graph to supply your components with up-to-date todos. The next step is to add actions into the mix to update the todos which then invalidate the cache to compel the router to reload them from the source. (In more advanced scenarios you then use the action submissions to provide an optimistic view even before the cache has updated, by replacing stale values with optimistic ones - it's not something that happens automatically but relies on you deftly orchestrating the blending of up-to-date and optimistic data - that's what is going on with the example todomvc).
MJ (@mjackson) on X
Would you rather have your data fetching associated with the current route or with a component?
Twitter
HackMD
Router Data APIs - HackMD
Potential Router APIs for data
Ryan Carniato
YouTube
Evolving Isomorphic Data-Fetching
New constraints have been leading to a complete rethinking of how we handle isomorphic data-fetching in JavaScript frameworks. Truthfully I don't have the full answer yet. Join me as we explore this topic understanding the constraints, learning from history, and attempt to design what the architecture of the future looks like. 0:00] - Preamble ...
Ryan Carniato
YouTube
Server Functions & Server Actions
What's the big deal with Server Functions/Actions. Are they the same thing? What do they do? We will explore the topic today and look at how these are shaping the future of fullstack applications. [0:00] Preamble [7:30] Remix & Why Routing is Important [17:00] The History of Remote Procedure Calls [26:30] Solid's Road to Modern Server Actions [...
GitHub
GitHub - solidjs/solid-router: A universal router for Solid inspire...
A universal router for Solid inspired by Ember and React Router - solidjs/solid-router
ChrisThornham
ChrisThornham3mo ago
Amazing! Thank you. I’ll work on actions tomorrow to see if I can figure those out. I’ll report back. Thanks again!
Want results from more Discord servers?
Add your server
More Posts