-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Actions: React 19 progressive enhancement support (#11071)
* deps: react 19 * feat: react progressive enhancement with useActionState * refactor: revert old action state implementation * feat(test): react 19 action with useFormStatus * fix: remove unused context arg * fix: wrote actions to wrong test fixture! * deps: revert react 19 beta to 18 for actions-blog fixture * chore: remove unused overrides * chore: remove unused actions export * chore: spaces vs tabs ugh * chore: fix conflicting fixture names * chore: changeset * chore: bump changeset to minor * Actions: support React 19 `useActionState()` with progressive enhancement (#11074) * feat(ex): Like with useActionState * feat: useActionState progressive enhancement! * feat: getActionState utility * chore: revert actions-blog fixture experimentation * fix: add back actions.ts export * feat(test): Like with use action state test * fix: stub form state client-side to avoid hydration error * fix: bad .safe chaining * fix: update actionState for client call * fix: correctly resume form state client side * refactor: unify and document reactServerActionResult * feat(test): useActionState assertions * feat(docs): explain my mess * refactor: add experimental_ prefix * refactor: move all react internals to integration * chore: remove unused getIslandProps * chore: remove unused imports * chore: undo format changes * refactor: get actionResult from middleware directly * refactor: remove bad result type * fix: like button disabled timeout * chore: changeset * refactor: remove request cloning * Update .changeset/gentle-windows-enjoy.md Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * changeset grammar tense --------- Co-authored-by: Emanuele Stoppa <my.burning@gmail.com> Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
- Loading branch information
1 parent
3dd57f6
commit 8ca7c73
Showing
34 changed files
with
1,211 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
--- | ||
"@astrojs/react": minor | ||
"astro": minor | ||
--- | ||
|
||
Adds two new functions `experimental_getActionState()` and `experimental_withState()` to support [the React 19 `useActionState()` hook](https://react.dev/reference/react/useActionState) when using Astro Actions. This introduces progressive enhancement when calling an Action with the `withState()` utility. | ||
|
||
This example calls a `like` action that accepts a `postId` and returns the number of likes. Pass this action to the `experimental_withState()` function to apply progressive enhancement info, and apply to `useActionState()` to track the result: | ||
|
||
```tsx | ||
import { actions } from 'astro:actions'; | ||
import { experimental_withState } from '@astrojs/react/actions'; | ||
|
||
export function Like({ postId }: { postId: string }) { | ||
const [state, action, pending] = useActionState( | ||
experimental_withState(actions.like), | ||
0, // initial likes | ||
); | ||
|
||
return ( | ||
<form action={action}> | ||
<input type="hidden" name="postId" value={postId} /> | ||
<button disabled={pending}>{state} ❤️</button> | ||
</form> | ||
); | ||
} | ||
``` | ||
|
||
You can also access the state stored by `useActionState()` from your action `handler`. Call `experimental_getActionState()` with the API context, and optionally apply a type to the result: | ||
|
||
```ts | ||
import { defineAction, z } from 'astro:actions'; | ||
import { experimental_getActionState } from '@astrojs/react/actions'; | ||
|
||
export const server = { | ||
like: defineAction({ | ||
input: z.object({ | ||
postId: z.string(), | ||
}), | ||
handler: async ({ postId }, ctx) => { | ||
const currentLikes = experimental_getActionState<number>(ctx); | ||
// write to database | ||
return currentLikes + 1; | ||
} | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
--- | ||
"astro": minor | ||
--- | ||
|
||
Adds compatibility for Astro Actions in the React 19 beta. Actions can be passed to a `form action` prop directly, and Astro will automatically add metadata for progressive enhancement. | ||
|
||
```tsx | ||
import { actions } from 'astro:actions'; | ||
|
||
function Like() { | ||
return ( | ||
<form action={actions.like}> | ||
{/* auto-inserts hidden input for progressive enhancement */} | ||
<button type="submit">Like</button> | ||
</form> | ||
) | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import { expect } from '@playwright/test'; | ||
import { testFactory } from './test-utils.js'; | ||
|
||
const test = testFactory({ root: './fixtures/actions-react-19/' }); | ||
|
||
let devServer; | ||
|
||
test.beforeAll(async ({ astro }) => { | ||
devServer = await astro.startDevServer(); | ||
}); | ||
|
||
test.afterEach(async ({ astro }) => { | ||
// Force database reset between tests | ||
await astro.editFile('./db/seed.ts', (original) => original); | ||
}); | ||
|
||
test.afterAll(async () => { | ||
await devServer.stop(); | ||
}); | ||
|
||
test.describe('Astro Actions - React 19', () => { | ||
test('Like action - client pending state', async ({ page, astro }) => { | ||
await page.goto(astro.resolveUrl('/blog/first-post/')); | ||
|
||
const likeButton = page.getByLabel('likes-client'); | ||
await expect(likeButton).toBeVisible(); | ||
await likeButton.click(); | ||
await expect(likeButton, 'like button should be disabled when pending').toBeDisabled(); | ||
await expect(likeButton).not.toBeDisabled({ timeout: 5000 }); | ||
}); | ||
|
||
test('Like action - server progressive enhancement', async ({ page, astro }) => { | ||
await page.goto(astro.resolveUrl('/blog/first-post/')); | ||
|
||
const likeButton = page.getByLabel('likes-server'); | ||
await expect(likeButton, 'like button starts with 10 likes').toContainText('10'); | ||
await likeButton.click(); | ||
|
||
await expect(likeButton, 'like button increments').toContainText('11'); | ||
}); | ||
|
||
test('Like action - client useActionState', async ({ page, astro }) => { | ||
await page.goto(astro.resolveUrl('/blog/first-post/')); | ||
|
||
const likeButton = page.getByLabel('likes-action-client'); | ||
await expect(likeButton).toBeVisible(); | ||
await likeButton.click(); | ||
|
||
await expect(likeButton, 'like button increments').toContainText('11'); | ||
}); | ||
|
||
test('Like action - server useActionState progressive enhancement', async ({ page, astro }) => { | ||
await page.goto(astro.resolveUrl('/blog/first-post/')); | ||
|
||
const likeButton = page.getByLabel('likes-action-server'); | ||
await expect(likeButton, 'like button starts with 10 likes').toContainText('10'); | ||
await likeButton.click(); | ||
|
||
await expect(likeButton, 'like button increments').toContainText('11'); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
17 changes: 17 additions & 0 deletions
17
packages/astro/e2e/fixtures/actions-react-19/astro.config.mjs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { defineConfig } from 'astro/config'; | ||
import db from '@astrojs/db'; | ||
import react from '@astrojs/react'; | ||
import node from '@astrojs/node'; | ||
|
||
// https://astro.build/config | ||
export default defineConfig({ | ||
site: 'https://example.com', | ||
integrations: [db(), react()], | ||
output: 'hybrid', | ||
adapter: node({ | ||
mode: 'standalone', | ||
}), | ||
experimental: { | ||
actions: true, | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { column, defineDb, defineTable } from "astro:db"; | ||
|
||
const Comment = defineTable({ | ||
columns: { | ||
postId: column.text(), | ||
author: column.text(), | ||
body: column.text(), | ||
}, | ||
}); | ||
|
||
const Likes = defineTable({ | ||
columns: { | ||
postId: column.text(), | ||
likes: column.number(), | ||
}, | ||
}); | ||
|
||
// https://astro.build/db/config | ||
export default defineDb({ | ||
tables: { Comment, Likes }, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { db, Likes, Comment } from "astro:db"; | ||
|
||
// https://astro.build/db/seed | ||
export default async function seed() { | ||
await db.insert(Likes).values({ | ||
postId: "first-post.md", | ||
likes: 10, | ||
}); | ||
|
||
await db.insert(Comment).values({ | ||
postId: "first-post.md", | ||
author: "Alice", | ||
body: "Great post!", | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
{ | ||
"name": "@e2e/actions-react-19", | ||
"type": "module", | ||
"version": "0.0.1", | ||
"scripts": { | ||
"dev": "astro dev", | ||
"start": "astro dev", | ||
"build": "astro check && astro build", | ||
"preview": "astro preview", | ||
"astro": "astro" | ||
}, | ||
"dependencies": { | ||
"@astrojs/check": "^0.6.0", | ||
"@astrojs/db": "workspace:*", | ||
"@astrojs/node": "workspace:*", | ||
"@astrojs/react": "workspace:*", | ||
"@types/react": "npm:types-react", | ||
"@types/react-dom": "npm:types-react-dom", | ||
"astro": "workspace:*", | ||
"react": "19.0.0-beta-26f2496093-20240514", | ||
"react-dom": "19.0.0-beta-26f2496093-20240514", | ||
"typescript": "^5.4.5" | ||
}, | ||
"overrides": { | ||
"@types/react": "npm:types-react", | ||
"@types/react-dom": "npm:types-react-dom" | ||
} | ||
} |
47 changes: 47 additions & 0 deletions
47
packages/astro/e2e/fixtures/actions-react-19/src/actions/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import { db, Likes, eq, sql } from 'astro:db'; | ||
import { defineAction, getApiContext, z } from 'astro:actions'; | ||
import { experimental_getActionState } from '@astrojs/react/actions'; | ||
|
||
export const server = { | ||
blog: { | ||
like: defineAction({ | ||
accept: 'form', | ||
input: z.object({ postId: z.string() }), | ||
handler: async ({ postId }) => { | ||
await new Promise((r) => setTimeout(r, 1000)); | ||
|
||
const { likes } = await db | ||
.update(Likes) | ||
.set({ | ||
likes: sql`likes + 1`, | ||
}) | ||
.where(eq(Likes.postId, postId)) | ||
.returning() | ||
.get(); | ||
|
||
return likes; | ||
}, | ||
}), | ||
likeWithActionState: defineAction({ | ||
accept: 'form', | ||
input: z.object({ postId: z.string() }), | ||
handler: async ({ postId }) => { | ||
await new Promise((r) => setTimeout(r, 200)); | ||
|
||
const context = getApiContext(); | ||
const state = await experimental_getActionState<number>(context); | ||
|
||
const { likes } = await db | ||
.update(Likes) | ||
.set({ | ||
likes: state + 1, | ||
}) | ||
.where(eq(Likes.postId, postId)) | ||
.returning() | ||
.get(); | ||
|
||
return likes; | ||
}, | ||
}), | ||
}, | ||
}; |
43 changes: 43 additions & 0 deletions
43
packages/astro/e2e/fixtures/actions-react-19/src/components/BaseHead.astro
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
--- | ||
// Import the global.css file here so that it is included on | ||
// all pages through the use of the <BaseHead /> component. | ||
import '../styles/global.css'; | ||
interface Props { | ||
title: string; | ||
description: string; | ||
image?: string; | ||
} | ||
const canonicalURL = new URL(Astro.url.pathname, Astro.site); | ||
const { title, description, image = '/blog-placeholder-1.jpg' } = Astro.props; | ||
--- | ||
|
||
<!-- Global Metadata --> | ||
<meta charset="utf-8" /> | ||
<meta name="viewport" content="width=device-width,initial-scale=1" /> | ||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> | ||
<meta name="generator" content={Astro.generator} /> | ||
|
||
<!-- Canonical URL --> | ||
<link rel="canonical" href={canonicalURL} /> | ||
|
||
<!-- Primary Meta Tags --> | ||
<title>{title}</title> | ||
<meta name="title" content={title} /> | ||
<meta name="description" content={description} /> | ||
|
||
<!-- Open Graph / Facebook --> | ||
<meta property="og:type" content="website" /> | ||
<meta property="og:url" content={Astro.url} /> | ||
<meta property="og:title" content={title} /> | ||
<meta property="og:description" content={description} /> | ||
<meta property="og:image" content={new URL(image, Astro.url)} /> | ||
|
||
<!-- Twitter --> | ||
<meta property="twitter:card" content="summary_large_image" /> | ||
<meta property="twitter:url" content={Astro.url} /> | ||
<meta property="twitter:title" content={title} /> | ||
<meta property="twitter:description" content={description} /> | ||
<meta property="twitter:image" content={new URL(image, Astro.url)} /> |
62 changes: 62 additions & 0 deletions
62
packages/astro/e2e/fixtures/actions-react-19/src/components/Footer.astro
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
--- | ||
const today = new Date(); | ||
--- | ||
|
||
<footer> | ||
© {today.getFullYear()} Your name here. All rights reserved. | ||
<div class="social-links"> | ||
<a href="https://m.webtoo.ls/@astro" target="_blank"> | ||
<span class="sr-only">Follow Astro on Mastodon</span> | ||
<svg | ||
viewBox="0 0 16 16" | ||
aria-hidden="true" | ||
width="32" | ||
height="32" | ||
astro-icon="social/mastodon" | ||
><path | ||
fill="currentColor" | ||
d="M11.19 12.195c2.016-.24 3.77-1.475 3.99-2.603.348-1.778.32-4.339.32-4.339 0-3.47-2.286-4.488-2.286-4.488C12.062.238 10.083.017 8.027 0h-.05C5.92.017 3.942.238 2.79.765c0 0-2.285 1.017-2.285 4.488l-.002.662c-.004.64-.007 1.35.011 2.091.083 3.394.626 6.74 3.78 7.57 1.454.383 2.703.463 3.709.408 1.823-.1 2.847-.647 2.847-.647l-.06-1.317s-1.303.41-2.767.36c-1.45-.05-2.98-.156-3.215-1.928a3.614 3.614 0 0 1-.033-.496s1.424.346 3.228.428c1.103.05 2.137-.064 3.188-.189zm1.613-2.47H11.13v-4.08c0-.859-.364-1.295-1.091-1.295-.804 0-1.207.517-1.207 1.541v2.233H7.168V5.89c0-1.024-.403-1.541-1.207-1.541-.727 0-1.091.436-1.091 1.296v4.079H3.197V5.522c0-.859.22-1.541.66-2.046.456-.505 1.052-.764 1.793-.764.856 0 1.504.328 1.933.983L8 4.39l.417-.695c.429-.655 1.077-.983 1.934-.983.74 0 1.336.259 1.791.764.442.505.661 1.187.661 2.046v4.203z" | ||
></path></svg | ||
> | ||
</a> | ||
<a href="https://twitter.com/astrodotbuild" target="_blank"> | ||
<span class="sr-only">Follow Astro on Twitter</span> | ||
<svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32" astro-icon="social/twitter" | ||
><path | ||
fill="currentColor" | ||
d="M5.026 15c6.038 0 9.341-5.003 9.341-9.334 0-.14 0-.282-.006-.422A6.685 6.685 0 0 0 16 3.542a6.658 6.658 0 0 1-1.889.518 3.301 3.301 0 0 0 1.447-1.817 6.533 6.533 0 0 1-2.087.793A3.286 3.286 0 0 0 7.875 6.03a9.325 9.325 0 0 1-6.767-3.429 3.289 3.289 0 0 0 1.018 4.382A3.323 3.323 0 0 1 .64 6.575v.045a3.288 3.288 0 0 0 2.632 3.218 3.203 3.203 0 0 1-.865.115 3.23 3.23 0 0 1-.614-.057 3.283 3.283 0 0 0 3.067 2.277A6.588 6.588 0 0 1 .78 13.58a6.32 6.32 0 0 1-.78-.045A9.344 9.344 0 0 0 5.026 15z" | ||
></path></svg | ||
> | ||
</a> | ||
<a href="https://github.com/withastro/astro" target="_blank"> | ||
<span class="sr-only">Go to Astro's GitHub repo</span> | ||
<svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32" astro-icon="social/github" | ||
><path | ||
fill="currentColor" | ||
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z" | ||
></path></svg | ||
> | ||
</a> | ||
</div> | ||
</footer> | ||
<style> | ||
footer { | ||
padding: 2em 1em 6em 1em; | ||
background: linear-gradient(var(--gray-gradient)) no-repeat; | ||
color: rgb(var(--gray)); | ||
text-align: center; | ||
} | ||
.social-links { | ||
display: flex; | ||
justify-content: center; | ||
gap: 1em; | ||
margin-top: 1em; | ||
} | ||
.social-links a { | ||
text-decoration: none; | ||
color: rgb(var(--gray)); | ||
} | ||
.social-links a:hover { | ||
color: rgb(var(--gray-dark)); | ||
} | ||
</style> |
Oops, something went wrong.