How to Implement Frontend Data Fetching with useActionQuery and useActionMutation Hooks in Agent-Native
Agent-Native provides useActionQuery and useActionMutation hooks in @agent-native/core/client that replace manual REST calls with typed, action-driven data fetching, automatically handling caching, retries, and query invalidation through React Query integration.
Agent-Native replaces traditional REST API calls with a type-safe, action-driven transport layer. The framework exposes two primary React hooks—useActionQuery for idempotent read operations and useActionMutation for state-changing mutations—that abstract HTTP complexity while providing end-to-end TypeScript inference via the AgentNativeActionRegistry.
Core Architecture and Transport Layer
The implementation resides in packages/core/src/client/use-action.ts, which exports three key utilities: useActionQuery, useActionMutation, and the imperative callAction helper. These functions wrap a central transport mechanism called actionFetch (defined around lines 38-53) that constructs requests to the framework endpoint /_agent-native/actions/<action-name>.
Every request automatically receives two critical headers:
X-Agent-Native-Frontend: 1— Identifies the caller as a UI component, distinguishing browser requests from server-side or external API calls.x-user-timezone— Propagates the browser’s IANA timezone to ensure server-side "today" logic aligns with the client’s local time.
Both hooks integrate with React Query (@tanstack/react-query) to provide automatic retries (excluding authentication failures), request deduplication, and background refetching. According to the BuilderIO/agent-native source code, mutations automatically invalidate cached queries sharing the "action" query key, triggering immediate UI updates.
Fetching Data with useActionQuery
useActionQuery handles idempotent GET operations such as listing, reading, or searching data. The hook returns a standard React Query result containing data, isLoading, error, and refetch properties, with TypeScript types inferred directly from your server-side action definitions.
Basic List Query
Consume a server-side action without manual URL construction:
import { useActionQuery } from "@agent-native/core/client";
export function FormList() {
// TypeScript infers the return type (e.g., Form[]) from AgentNativeActionRegistry
const { data: forms, isLoading, error } = useActionQuery("list-forms");
if (isLoading) return <p>Loading…</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<ul>
{forms?.map((f) => (
<li key={f.id}>{f.title}</li>
))}
</ul>
);
}
Parameterized Queries
Pass filter parameters as a second argument. The hook uses serializeActionQueryParams to convert the object into a query string, handling arrays via bracket syntax through appendActionQueryParam:
import { useActionQuery } from "@agent-native/core/client";
export function PublishedForms() {
const { data, isLoading } = useActionQuery("list-forms", {
status: "published", // Becomes ?status=published
limit: 10,
});
if (isLoading) return <div>Loading forms…</div>;
return <FormGrid data={data} />;
}
Mutating Data with useActionMutation
useActionMutation manages state-changing operations (POST, PUT, DELETE). Defined around lines 126-155 in use-action.ts, this hook wraps React Query’s useMutation and automatically invalidates related queries upon success by calling queryClient.invalidateQueries({ queryKey: ["action"] }).
Creating Records
import { useActionMutation, useActionQuery } from "@agent-native/core/client";
export function NewForm() {
const { mutate: createForm, isLoading } = useActionMutation("create-form");
const { data: forms, refetch } = useActionQuery("list-forms");
const handleSubmit = async (title: string) => {
await createForm({ title }); // POST to /_agent-native/actions/create-form
// Explicit refetch (or rely on automatic invalidation)
await refetch();
};
return (
<form onSubmit={(e) => handleSubmit(e.target.title.value)}>
<input name="title" />
<button disabled={isLoading}>Create</button>
</form>
);
}
Custom HTTP Methods
Override the default POST method by passing a method option directly to actionFetch:
import { useActionMutation } from "@agent-native/core/client";
export function DeleteForm({ id }: { id: string }) {
const { mutate: deleteForm, isPending } = useActionMutation("delete-form", {
method: "DELETE",
});
return (
<button
onClick={() => deleteForm({ id })}
disabled={isPending}
>
Delete Form
</button>
);
}
Type Safety and the Action Registry
Agent-Native achieves end-to-end type safety through the AgentNativeActionRegistry. When you define actions on the server using defineAction, the Vite plugin generates declaration files (typically in packages/core/.generated/action-types.d.ts) that extend this registry with the action’s specific params and result types.
As implemented in BuilderIO/agent-native, this means:
- Parameter autocompletion: The second argument to
useActionQueryor the first argument to the mutation function knows the exact shape required by your server definition. - Return type inference: The
dataproperty is automatically typed based on the action’s declared return value. - Compile-time validation: Mismatched parameters trigger TypeScript errors before build time.
Imperative Calls Outside Components
For scenarios where React hooks are not ergonomic—such as debounced search inputs or server-side rendering—use the callAction helper:
import { callAction } from "@agent-native/core/client";
async function prefetchForm(id: string) {
// Returns Promise<Form> based on registry types
const form = await callAction("get-form", { id });
return form;
}
This low-level function bypasses React Query’s caching layer but maintains the same transport logic, headers, and type safety as the hook-based alternatives.
Summary
- File location: Core implementation lives in
packages/core/src/client/use-action.ts, with path construction handled inpackages/core/src/client/api-path.ts. - useActionQuery: Wraps React Query’s
useQueryfor idempotent GET requests to/_agent-native/actions/<action-name>, with automatic header injection and timezone propagation. - useActionMutation: Wraps
useMutationfor state changes, automatically invalidating cached"action"queries on success to keep UI synchronized. - Type safety: Full TypeScript inference via
AgentNativeActionRegistry, populated by the build-time Vite plugin based on server-side action definitions. - Transport: Central
actionFetchutility handles serialization, error parsing, and header management consistently across all hooks.
Frequently Asked Questions
What is the difference between useActionQuery and useActionMutation?
useActionQuery is designed for idempotent read operations (GET requests) and integrates with React Query’s caching, background refetching, and deduplication mechanisms. useActionMutation handles state-changing operations (POST, PUT, DELETE) and automatically invalidates cached queries after successful execution to ensure data consistency across your application.
How does Agent-Native maintain TypeScript type safety across the client-server boundary?
The framework uses the AgentNativeActionRegistry interface, which is augmented at build time by a Vite plugin that scans your server-side defineAction calls. When you import from @agent-native/core/client, the hooks use this registry to infer parameter shapes and return types, providing autocompletion and compile-time validation without manual type definitions.
What HTTP headers does Agent-Native automatically add to requests?
As implemented in the actionFetch function within packages/core/src/client/use-action.ts, every request automatically includes X-Agent-Native-Frontend: 1 to identify browser-based callers and x-user-timezone containing the browser’s IANA timezone identifier. These headers enable server-side logic to differentiate request sources and align temporal calculations with the user’s local time.
How do I manually invalidate cached queries after a mutation?
While useActionMutation automatically invalidates queries with the key ["action"], you can manually trigger invalidation using React Query’s query client. Import queryClient and call queryClient.invalidateQueries({ queryKey: ["action"] }) to force refetching of all action-based queries, or specify a more specific query key to target individual action results.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →