diff options
author | 2024-05-22 08:24:55 -0400 | |
---|---|---|
committer | 2024-05-22 13:24:55 +0100 | |
commit | 8ca7c731dea894e77f84b314ebe3a141d5daa918 (patch) | |
tree | e7a89e1fa92436e3d56a11ace13f43ef7b8f7bfe /packages/integrations/react/server.js | |
parent | 3dd57f69e344e4bd1bd9da28a9391c4d62246f7d (diff) | |
download | astro-8ca7c731dea894e77f84b314ebe3a141d5daa918.tar.gz astro-8ca7c731dea894e77f84b314ebe3a141d5daa918.tar.zst astro-8ca7c731dea894e77f84b314ebe3a141d5daa918.zip |
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>
Diffstat (limited to 'packages/integrations/react/server.js')
-rw-r--r-- | packages/integrations/react/server.js | 55 |
1 files changed, 55 insertions, 0 deletions
diff --git a/packages/integrations/react/server.js b/packages/integrations/react/server.js index 2ff3f55fb..a0b2db744 100644 --- a/packages/integrations/react/server.js +++ b/packages/integrations/react/server.js @@ -1,6 +1,7 @@ import opts from 'astro:react:opts'; import React from 'react'; import ReactDOM from 'react-dom/server'; +import { AstroError } from 'astro/errors'; import { incrementId } from './context.js'; import StaticHtml from './static-html.js'; @@ -101,9 +102,16 @@ async function renderToStaticMarkup(Component, props, { default: children, ...sl value: newChildren, }); } + const formState = this ? await getFormState(this) : undefined; + if (formState) { + attrs['data-action-result'] = JSON.stringify(formState[0]); + attrs['data-action-key'] = formState[1]; + attrs['data-action-name'] = formState[2]; + } const vnode = React.createElement(Component, newProps); const renderOptions = { identifierPrefix: prefix, + formState, }; let html; if ('renderToReadableStream' in ReactDOM) { @@ -114,6 +122,43 @@ async function renderToStaticMarkup(Component, props, { default: children, ...sl return { html, attrs }; } +/** + * @returns {Promise<[actionResult: any, actionKey: string, actionName: string] | undefined>} + */ +async function getFormState({ result }) { + const { request, actionResult } = result; + + if (!actionResult) return undefined; + if (!isFormRequest(request.headers.get('content-type'))) return undefined; + + const formData = await request.clone().formData(); + /** + * The key generated by React to identify each `useActionState()` call. + * @example "k511f74df5a35d32e7cf266450d85cb6c" + */ + const actionKey = formData.get('$ACTION_KEY')?.toString(); + /** + * The action name returned by an action's `toString()` property. + * This matches the endpoint path. + * @example "/_actions/blog.like" + */ + const actionName = formData.get('_astroAction')?.toString(); + + if (!actionKey || !actionName) return undefined; + + const isUsingSafe = formData.has('_astroActionSafe'); + if (!isUsingSafe && actionResult.error) { + throw new AstroError( + `Unhandled error calling action ${actionName.replace(/^\/_actions\//, '')}:\n[${ + actionResult.error.code + }] ${actionResult.error.message}`, + 'use `.safe()` to handle from your React component.' + ); + } + + return [isUsingSafe ? actionResult : actionResult.data, actionKey, actionName]; +} + async function renderToPipeableStreamAsync(vnode, options) { const Writable = await getNodeWritable(); let html = ''; @@ -170,6 +215,16 @@ async function renderToReadableStreamAsync(vnode, options) { return await readResult(await ReactDOM.renderToReadableStream(vnode, options)); } +const formContentTypes = ['application/x-www-form-urlencoded', 'multipart/form-data']; + +function isFormRequest(contentType) { + // Split off parameters like charset or boundary + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type#content-type_in_html_forms + const type = contentType?.split(';')[0].toLowerCase(); + + return formContentTypes.some((t) => type === t); +} + export default { check, renderToStaticMarkup, |