Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
4 changes: 4 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions docs/core-concepts/server-actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@ export default function ProfilePage({ data }) {
| `fields` | `Record<string, string[]> \| 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 |
Expand Down
5 changes: 5 additions & 0 deletions docs/reference/client-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 16 additions & 5 deletions pyxle/templates/scaffold/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 `<Form>` helper for form submissions:

```jsx
Expand Down Expand Up @@ -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`.
- `<Form action="name" onSuccess onError>` — submit named inputs to an `@action`.
- `<Head>` — set per-page `<title>`/`<meta>`/`<link>` (deduped + merged with layouts).
- `<Link href="/path">` — client-side navigation; `navigate('/path')` to do it imperatively.
Expand Down Expand Up @@ -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.
Expand Down
Loading