Skip to main content

How to Call Custom RPCs Safely (using useCall)

In Framework M, most data operations are handled by metadata-driven forms (AutoForm) and data grids (AutoTable). However, when you need to trigger a custom side-effect (like sending an email, approving a workflow, or calling an external API), you should use the useCall hook.

The useCall hook is a React hook provided by @framework-m/desk that acts as a secure wrapper around backend API calls. Crucially, it automatically handles mutation safety (Idempotency).

Basic Usage (DocType RPCs)

If you have a backend function decorated with @rpc on a DocType, you can call it easily:

import { useCall } from "@framework-m/desk";
import { useState } from "react";

export function SendEmailButton({ invoiceId }) {
const { execute, isLoading, data, error } = useCall();
const [success, setSuccess] = useState(false);

const handleSend = async () => {
try {
// Targets: POST /api/v1/rpc/Invoice/send_email
await execute({
doctype: "Invoice",
method: "send_email",
payload: { invoice_id: invoiceId }
});
setSuccess(true);
} catch (err) {
console.error("Failed to send email", err);
}
};

if (success) return <span>Email Sent!</span>;

return (
<button onClick={handleSend} disabled={isLoading}>
{isLoading ? "Sending..." : "Send Email"}
</button>
);
}

Why use useCall instead of fetch?

If you were to use standard fetch or axios, and the user double-clicks the button, or the network drops and the browser automatically retries, the backend would receive two separate requests. The customer might receive two emails!

By using useCall, Framework M automatically generates a unique Idempotency-Key header for the request. If the request is retried, the backend recognizes the duplicate key and returns the cached success response without executing the Python logic a second time.

Advanced Usage (Custom Litestar Endpoints)

If your backend engineers have built a completely custom REST endpoint using raw Litestar controllers (bypassing the DocType RPC system), you can still use useCall by providing a path instead of doctype and method.

import { useCall } from "@framework-m/desk";

export function TriggerSyncButton() {
const { execute, isLoading } = useCall();

const handleSync = async () => {
// Targets a completely custom endpoint
await execute({
path: "/api/v2/integrations/salesforce/sync",
method: "POST", // Defaults to POST if omitted
payload: { full_sync: true }
});
};

return (
<button onClick={handleSync} disabled={isLoading}>
Start Sync
</button>
);
}

Hook Return Values

The useCall hook returns an object with the following properties:

  • execute(options): An async function that triggers the request.
  • isLoading (boolean): true while the request is in flight.
  • data (any | null): The JSON response payload from the server upon success.
  • error (Error | null): An error object if the request failed.