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;
}
@VaguelyOnline
Copy link
Author

@scramatte - I've updated some comments and things above. See if that works for you now. It's working very well in the project now for me. It can also be very simple to hit an api endpoint - as simple as:

useApiForm().delete(route('photos.edit'));

@scramatte
Copy link

Hi, Great , I'm going to try during today.
Thank you very much

@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