Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Actions #912

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open

Actions #912

wants to merge 10 commits into from

Conversation

bholmesdev
Copy link
Contributor

@bholmesdev bholmesdev commented May 2, 2024

Summary

Astro actions make it easy to define and call backend functions with type-safety.

// src/actions/index.ts
import { defineAction, z } from "astro:actions";

export const server = {
  like: defineAction({
    // accept json
    input: z.object({ postId: z.string() }),
    handler: async ({ postId }, context) => {
      // update likes in db

      return likes;
    },
  }),
};

// src/components/Like.tsx
import { actions } from "astro:actions";
import { useState } from "preact/hooks";

export function Like({ postId }: { postId: string }) {
  const [likes, setLikes] = useState(0);
  return (
    <button
      onClick={async () => {
        const newLikes = await actions.like({ postId });
        setLikes(newLikes);
      }}
    >
      {likes} likes
    </button>
  );
}

Links

@bholmesdev bholmesdev mentioned this pull request May 2, 2024
@rishi-raj-jain
Copy link

Does this require View Transitions to be enabled like for form data processing?

@Adammatthiesen
Copy link

I want every bit of this for StudioCMS....

@bholmesdev
Copy link
Contributor Author

@rishi-raj-jain Not at all! We are targeting client components to show how JS state management could work. But forms are callable from an form element with or without view transitions. Let me know if this section addresses that concern: https://github.com/withastro/roadmap/blob/actions/proposals/0046-actions.md#add-progressive-enhancement-with-fallbacks

@jdtjenkins
Copy link

@bholmesdev Will this be available for SSG at all, or is it just Hybrid/SSR?

@bholmesdev
Copy link
Contributor Author

@jdtjenkins Ah, good thing to note. Since actions allow you define backend handlers, you'll need a server adapter with output: 'hybrid'. You can keep all your existing routes static with that output

@ascorbic
Copy link

ascorbic commented May 4, 2024

My turn to bikeshed you. getNameProps is a bit of a confusing name to me. Would getInputProps work better?

@bholmesdev
Copy link
Contributor Author

@ascorbic That's fair! I agree that's a better name, happy to use it.

proposals/0046-actions.md Outdated Show resolved Hide resolved
@bholmesdev
Copy link
Contributor Author

@ascorbic Thinking it over, went with getActionProps(). Thought it paired nicely with Astro.getActionResult() to retrieve the return value.

@zadeviggers
Copy link

Hey @bholmesdev, it's me back again to make sweeping criticisms about one of your big proposals again 😁 (I'm the one who got you to vendor Zod back when you were working on content collections). Also, sorry for being so late to submit on this - I only just found out about this proposal from the 4.8 release!

Reading through the proposal, I noticed almost every example uses the onSubmit event of the form and then cancels the actual submit event. The use of JavaScript is understandable for many use cases, but it lacks a nice API for submitting actions without JS. This functionality is already very possible using an API route and regular form submissions. It might drive some people to use JS when they don't really need to since there isn't an obvious API for doing form submissions without it. In that light, adding an elegant path to JS-free forms through the actions API would be nice, even if it isn't a main goal.

Here's the single example of how to implement actions without JS:

import { actions, getActionProps } from 'astro:actions';

export function Comment({ postId }: { postId: string }) {
	return (
		<form method="POST" onSubmit={...}>
			<input {...getActionProps(actions.comment)} />
			{/* result:
			<input
				type="hidden"
				name="_astroAction"
				value="/_actions/comment" />
			*/}
		</form>
	)
}

It seems like a bit of an afterthought, to be honest, which doesn't really fit with Astro's whole 'Ship less JS' thing, and I hope we can all agree that <input {...getActionProps(actions.comment)} /> looks pretty ugly.

My understanding is that under the hood, Astro sees the _astroAction field and rewrites the request to go to an action handler. If that's the case, why not make this a first-class behaviour? Provide another function, say getActionURL(actions.whatever), that just returns the path to the action handler. Then you can pass that string into the action attribute of a <form> element. This eliminates the jankiness of <input {...getActionProps(actions.comment)} /> while still allowing full client-side JS control when wanted and helps guide people not to use JS when they don't need to.

E.g.

import { actions, getActionURL } from 'astro:actions';

export function Comment({ postId }: { postId: string }) {
	return (
		<form method="POST" action={getActionURL(actions.comment)} onSubmit={...}>
			{/* Regular form goes here */}
		</form>
	)
}

Some people wanted something similar to this that involved directly importing actions with import attributes, but it seems like that wasn't viable. This seems like a nice middle ground, where you still (kinda) pass the handler to the action prop, and it seems much more possible: it's just a simplification of getActionProps(...).

Also, the rest of the proposal looks great. I can't wait to have a play with some Actions!

@matthewp
Copy link
Contributor

matthewp commented May 9, 2024

@zadeviggers Form support was not an afterthought, it was the biggest part of the design and why the feature didn't ship sooner. An API like the one you're suggesting was considered but it's incomplete. What happens on the server-side after an action is executed? How do you redirect to a different page? How do you handle validation errors? @bholmesdev went through a few different designs to try and make all of those things possible but it came at the cost of making the action code much more complex.

The getActionProps design is nice because the form is submitted to whatever action="" you put in, and you are able to handle redirects / errors, etc all yourself in a normal Astro page. It gives you the freedom to do whatever you want, while keeping the Action API more a pure function-like interface.

That said, this RFC is still open so we can talk about other approaches.

@bholmesdev
Copy link
Contributor Author

Thanks for the overview @matthewp. Actually, I would not consider the action suggestion incomplete! An alternative may be to pass a ?queryParam from his proposed getActionUrl(). This lets us use the same middleware strategy for retrieving the action result from frontmatter. We could also allow some sort of prefix parameter if you want to reroute the POST to another page.

This isn't perfect though. The main reason for a hidden input over this was challenges with React 19. When using actions in React, they own the action property when you pass a function:

<form action={actions.like}>

But now, how can we add a fallback? React stubs action to an empty field now. I am working with the React core team to see if we can control the server-rendered action to avoid this somehow. To be honest, I prefer your API to getActionProps(), so I would like to see this.

@matthewp
Copy link
Contributor

matthewp commented May 9, 2024

@bholmesdev I'm not sure I understand. If the action attribute points to the Action then the action needs to be able to do redirects and other things. Where does that code live?

@bholmesdev
Copy link
Contributor Author

bholmesdev commented May 9, 2024

@matthewp Well I should clarify: the action would NOT be a URL. It would be a query param to re-request the current page.

<form action={actions.like.url}>
<!--output: ?_actionName=like-->

This does the same work as passing a hidden _actionName input from my understanding

@matthewp
Copy link
Contributor

matthewp commented May 9, 2024

Oh ok, I understand now. I still like getActionProps better than this idea. When I see <form action={actions.like.url}> I expect that the request goes directly to the action. That it actually posts to the current page would be unexpected. Also, I might want to direct the request to another page, with getActionProps I can do that by setting action="/other-page", but with this new idea it's not clear at all how to do that.

So I think this trades away flexibility for a subjective aesthetic gain only.

@zadeviggers
Copy link

zadeviggers commented May 9, 2024

@matthewp To address your flexibility concern, we can add an optional parameter to the function to specify a different path for it to submit to. I don't think this will come up super often, though, since people will probably just do all their handling in the action, and then rewrite to the page they want to render.

@bholmesdev I really like the actions.like.url idea over a whole extra function. To address @matthewp's concern, it could be a function instead: <form action={actions.like.url(/* optional path */)>

@mayank99
Copy link

mayank99 commented May 24, 2024

I think I agree with both Zade and Matthew here. I also like the actions.like(/* optional pathname */) suggestion (works similar to action={undefined} submitting to the current page).

The big advantage I can see over an extra hidden <input> is that developers can and will leave it out if it works without it (on their machines with high-speed internet and JS available). Progressive enhancement should ideally work by default, rather than being "opt-in".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants