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 useActionQuery or the first argument to the mutation function knows the exact shape required by your server definition.
  • Return type inference: The data property 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 in packages/core/src/client/api-path.ts.
  • useActionQuery: Wraps React Query’s useQuery for idempotent GET requests to /_agent-native/actions/<action-name>, with automatic header injection and timezone propagation.
  • useActionMutation: Wraps useMutation for 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 actionFetch utility 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →