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):truewhile 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.