From 18f89057b3eb97bb51717c339a375f2b6e07235a Mon Sep 17 00:00:00 2001 From: Shivam Saini <51438830+shivamsn97@users.noreply.github.com> Date: Sat, 11 Jul 2026 21:49:14 +0530 Subject: [PATCH] docs(scaffold): clarify the shape of an action's client-side result MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An @action's awaited result is the flat { ok, ...yourReturn } — returned fields sit directly on it (res.title), and a successful result has no .data. Because useAction separately exposes a .data property (the last successful return), it was easy to write res.data.title, which is undefined on success and silently no-ops inside `if (res.ok)`, so a mutation appeared to do nothing until the page reloaded. Spell out the contract and warn against res.data.x in the scaffolded AGENTS.md, the Server Actions guide, and the client API reference; drop an ambiguous ...data spread in the README example; add a changelog note. No behavior change — the shipped TypeScript types already modelled this correctly. --- README.md | 2 +- docs/changelog.md | 4 ++++ docs/core-concepts/server-actions.md | 5 +++++ docs/reference/client-api.md | 5 +++++ pyxle/templates/scaffold/AGENTS.md | 21 ++++++++++++++++----- 5 files changed, 31 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 6218535..74ee5fe 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ export default function Users({ data }) { const remove = useAction('remove'); // calls the @action above — no fetch, no route async function onRemove(id) { - const res = await remove({ id }); // framework wraps the return as { ok, ...data } + const res = await remove({ id }); // framework wraps the return as { ok, ...yourReturn } if (res.ok) setUsers(res.users); // the server stays the source of truth } diff --git a/docs/changelog.md b/docs/changelog.md index 5f29bc2..644c99c 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,10 @@ Release notes for Pyxle. While we're in beta (`0.x`), minor versions may include breaking changes — those are called out explicitly here. To upgrade, run `pip install --upgrade pyxle-framework`. +## Unreleased + +- **Docs: the client-side shape of an `@action`'s result is now spelled out — no more `res.data` confusion.** An action's awaited result is the flat `{ ok, ...yourReturn }`: your returned fields sit directly on it (`res.title`), and a *successful* result has no `.data`. Because `useAction` *separately* exposes a `.data` property (the last successful return), it was easy — for a person or an AI coding agent — to write `res.data.title`, which is `undefined` on success and silently no-ops inside `if (res.ok)`, so a mutation looked like it did nothing until the page reloaded. The scaffolded `AGENTS.md`, the [Server Actions guide](core-concepts/server-actions.md), and the [client API reference](reference/client-api.md) now state the contract explicitly and warn against `res.data.x`, and the README drops an ambiguous `...data` spread. No behavior change — the shipped TypeScript types already modelled this correctly. + ## 0.7.2 — 2026-07-11 - **Fix: `pyxle init` scaffolds an installable `requirements.txt` again.** The template pinned `starlette>=0.37,<0.38` — the exact range 0.7.1's security bump moved off — so a fresh project's `requirements.txt` was unsatisfiable against the `pyxle-framework>=0.7.1` line beside it (which requires `starlette>=1.3.1`), and `pip install -r requirements.txt` failed to resolve. The scaffold now pins `starlette>=1.3.1,<2.0`, matching the framework. diff --git a/docs/core-concepts/server-actions.md b/docs/core-concepts/server-actions.md index 41d840a..188b8d1 100644 --- a/docs/core-concepts/server-actions.md +++ b/docs/core-concepts/server-actions.md @@ -104,6 +104,11 @@ export default function ProfilePage({ data }) { | `fields` | `Record \| null` | Per-field validation errors from the last failed submit (a `422`), or `null`. Cleared when a new request starts | | `data` | `object \| null` | Last successful response data | +The value you `await` is the **flat** result — `{ ok: true, ...yourReturn }` on success — so read +returned fields directly off it (`result.title`), **not** under `result.data`. A successful result +has no `.data`. The `data` property in the table above is the *hook's* convenience copy of that same +payload, for reading it outside the call site — don't confuse the two. + Options: | Option | Type | Description | diff --git a/docs/reference/client-api.md b/docs/reference/client-api.md index 5dd3065..420c8d9 100644 --- a/docs/reference/client-api.md +++ b/docs/reference/client-api.md @@ -244,6 +244,11 @@ const result = await actionFn(payload); // result.*: response data fields (when ok) ``` +The `.data` row in the properties table above is the **hook's** convenience copy of the last +successful result, for reading outside the call. The value you `await` is the flat result itself — +read returned fields directly off it (`result.title`), **not** under `result.data`. A successful +result has no `.data`. + **Behaviour:** - New calls abort previous in-flight requests - State resets on each new call diff --git a/pyxle/templates/scaffold/AGENTS.md b/pyxle/templates/scaffold/AGENTS.md index 44c0786..32c9313 100644 --- a/pyxle/templates/scaffold/AGENTS.md +++ b/pyxle/templates/scaffold/AGENTS.md @@ -15,8 +15,9 @@ frameworks — most mistakes come from not understanding the one-file Python+Rea performs a mutation. Both decorators are **available without importing them.** 3. The client calls an `@action` with **`useAction('name')`, not `fetch()`.** There is no API route to write — the `@action` *is* the endpoint. -4. `@server`/`@action` return a **plain dict.** On the client, an action's result arrives - **wrapped** as `{ ok: true, ...yourDict }` (or `{ ok: false, error }`). Always check `res.ok`. +4. `@server`/`@action` return a **plain dict.** On the client, `const res = await myAction(...)` + gives you `{ ok: true, ...yourDict }` (or `{ ok: false, error }`) — your fields sit **directly + on `res`** (`res.users`), **not** under `res.data`. Always check `res.ok`. 5. Raise a handled error with `raise LoaderError(...)` (in a loader) or `raise ActionError(...)` (in an action) — **no import needed.** Like the decorators, these runtime classes (`LoaderError`, `ActionError`, `ValidationActionError`, `invalidate_routes`) are auto-injected. @@ -116,11 +117,17 @@ async def create_post(request): Call it from React — **never `fetch`**: ```jsx -const create = useAction('create_post'); // returns a callable + .pending/.error/.data +const create = useAction('create_post'); // a callable, plus create.pending / .error / .fields const res = await create({ title }); // arg becomes request.json() on the server -if (res.ok) { /* use res.id, res.title */ } else { /* show res.error */ } +if (res.ok) { /* fields are top-level: res.id, res.title — never res.data.id */ } +else { /* show res.error */ } ``` +The value you `await` **is** the flat `{ ok, ...yourReturn }` object — read `res.title`, never +`res.data.title` (a successful result has no `.data`). The hook *also* exposes `create.data`: the +**same** fields from the last successful call, handy for rendering away from the call site. Both +reach your data, but the value you `await` is never nested under `.data`. + Or use the `
` helper for form submissions: ```jsx @@ -161,7 +168,7 @@ for durable/cross-worker jobs, hand off to Celery/ARQ/Dramatiq (see the Backgrou ## The client toolkit — `import { … } from 'pyxle/client'` -- `useAction(name)` — bind to an `@action`; returns a callable with `.pending`, `.error`, `.fields`, `.data`. +- `useAction(name)` — bind to an `@action`; returns a callable with `.pending`, `.error`, `.fields`, and `.data` (the last successful return's fields). The value you `await` is the flat `{ ok, ...return }` — read fields off it directly (`res.x`), not off `res.data`. - `` — submit named inputs to an `@action`. - `` — set per-page ``/`<meta>`/`<link>` (deduped + merged with layouts). - `<Link href="/path">` — client-side navigation; `navigate('/path')` to do it imperatively. @@ -229,6 +236,10 @@ pyxle install # (re)install Python + Node deps - **DO** keep one `export default function` per page (the component). Name the loader anything. - **DO** return JSON-safe dicts from `@server`/`@action`. **DON'T** add your own `ok` key to an action's return — the framework adds it. +- **DON'T** read an awaited action result as `res.data.x`. A successful result is the flat + `{ ok, ...yourReturn }`, so the field is `res.x` — your returned data is never nested under + `.data`. (`.data` is the hook property `useAction(...).data`; on an awaited result it shows up + only on a *failure*, as the optional error payload — never your returned fields.) - **DON'T** call your own actions with `fetch` or write a route for them — use `useAction`/`<Form>`. - **DON'T** put emoji or other non-BMP characters in **server-rendered** JSX text — SSR can fail to encode them. Use them only in client-only paths, or stick to plain text.