Skip to content

Instantly share code, notes, and snippets.

@VaguelyOnline
Forked from mohitmamoria/README.md
Last active September 3, 2025 12:13
Show Gist options
  • Save VaguelyOnline/0dc4f3f2d014598301ce1ef7f26c84b9 to your computer and use it in GitHub Desktop.
Save VaguelyOnline/0dc4f3f2d014598301ce1ef7f26c84b9 to your computer and use it in GitHub Desktop.
Inertia.js Form Helper for Axios/API calls

Forked from: https://gist.github.com/mohitmamoria/91da6f30d9610b211248225c9e52bebe?permalink_comment_id=5155652 Why?: To add types and generics for a better DX.

Inertia.js ships with a fantastic form helper but it falls short when also using API/Axios calls in your project.

Here's a composable, built on top of the Inertia's form helper that hooks into it to replace the API calls with Axios.

To use, just replace useForm with useAPIForm.

const form = useAPIForm<{
    title: string;
}>({
    title: '',
});

Everything else just works!

<!-- Since this approach uses Sanctum Auth, we use the CSRF token. So the code in useApi.ts works, ensure this is placed in the app.blade.php file -->
<meta name="csrf" content="{{ csrf_token() }}" />
import axios, { AxiosInstance } from 'axios';
import { toast } from 'vue-sonner';
const csrf: string = document.querySelector('meta[name="csrf"]')?.getAttribute('content') || '';
/**
* Configure an axios instance that is wired up to the first-party Sanctum backend authentication.
* This is preferable to using the default Axios instance as we can load it up with the headers
* necessary to authenticate with Sanctum. For more: https://laravel.com/docs/12.x/sanctum
*/
const axiosClient = axios.create({
withCredentials: true,
withXSRFToken: true,
headers: {
'X-XSRF-TOKEN': csrf,
},
});
/**
* Some simple default handling for the common auth errors
* If we are not logged in (401), simply redirect to the
* login page. If unauthorized (403), toast a warning.
*/
axiosClient.interceptors.response.use(
(response) => {
return response;
},
(error) => {
if (error.response.status === 401) {
window.location.href = '/login';
}
if (error.response.status === 403) {
toast.warning('You do not have permission for the requested action');
}
return Promise.reject(error);
},
);
function useApi(): AxiosInstance {
return axiosClient;
}
export { useApi };
import { useApi } from '@/composables/useApi';
import { FormDataConvertible } from '@inertiajs/core/types';
import { InertiaFormProps, useForm } from '@inertiajs/vue3';
import { AxiosProgressEvent } from 'axios';
import cloneDeep from 'lodash.clonedeep';
const api = useApi();
interface FormOptions {
onBefore?: () => void;
onStart?: () => void;
onProgress?: (event: AxiosProgressEvent) => void;
onSuccess?: (data: any) => void;
onError?: (error: {
status: number;
}) => void;
onFinish?: () => void;
}
export type FormDataType = Record<string, FormDataConvertible>;
export type InertiaForm<TForm extends FormDataType> = TForm & InertiaFormProps<TForm>;
export function useApiForm<TForm extends FormDataType>(initialData?: TForm | (() => TForm)): InertiaForm<TForm> {
const form = useForm(initialData || {});
let transform = (data: TForm) => data;
let recentlySuccessfulTimeoutId: number | null = null;
const overriders = {
transform: (receiver: any) => (callback: (data: TForm) => any) => {
transform = callback;
return receiver;
},
submit:
() =>
(method: string, url: string, options: FormOptions = {}) => {
form.wasSuccessful = false;
form.recentlySuccessful = false;
form.clearErrors();
if (recentlySuccessfulTimeoutId) clearTimeout(recentlySuccessfulTimeoutId);
if (options.onBefore) {
options.onBefore();
}
form.processing = true;
if (options.onStart) {
options.onStart();
}
const data = transform(form.data() as any);
// @ts-expect-error Error in accessing method using square brackets
api[method](url, data, {
headers: {
'Content-Type': hasFiles(data) ? 'multipart/form-data' : 'application/json',
},
onUploadProgress: (event: any) => {
form.progress = event;
if (options.onProgress) {
options.onProgress(event);
}
},
})
.then((response: any) => {
form.processing = false;
form.progress = null;
form.clearErrors();
form.wasSuccessful = true;
form.recentlySuccessful = true;
recentlySuccessfulTimeoutId = setTimeout(() => (form.recentlySuccessful = false), 2000);
if (options.onSuccess) {
options.onSuccess(response.data);
}
form.defaults(cloneDeep(form.data()));
form.isDirty = false;
})
.catch((error: any) => {
form.processing = false;
form.progress = null;
form.clearErrors();
if (error.response?.status === 422) {
Object.keys(error.response.data.errors).forEach((key) => {
// @ts-expect-error types mismatch
form.setError(key, error.response.data.errors[key][0]);
});
}
if (options.onError) {
options.onError(error);
}
})
.finally(() => {
form.processing = false;
form.progress = null;
if (options.onFinish) {
options.onFinish();
}
});
},
};
return new Proxy(form, {
get: (target, prop, receiver) => {
if (Object.keys(overriders).indexOf(prop.toString()) < 0) {
// @ts-expect-error Using prop
return target[prop];
}
return overriders[prop as keyof typeof overriders](receiver);
},
}) as any;
}
function hasFiles(data: any): boolean {
return data instanceof FormData;
}
@scramatte
Copy link

Hi, I'm testing your changes.
About @inertiajs/core/types issue, I've added package @inertiajs/core and I replace
import { FormDataConvertible } from '@inertiajs/core/types';
by
import { FormDataConvertible } from '@inertiajs/core';

@scramatte
Copy link

Hi, it works as expected. One more thing, If I use it to login via api how do you suggest I can redirect to the initially called page or /dashboard ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment