Skip to content

Instantly share code, notes, and snippets.

@devhammed
Last active November 14, 2025 06:01
Show Gist options
  • Select an option

  • Save devhammed/83cc47ef0c75167adb15370eba75a5b6 to your computer and use it in GitHub Desktop.

Select an option

Save devhammed/83cc47ef0c75167adb15370eba75a5b6 to your computer and use it in GitHub Desktop.
Laravel Inertia JSON API Form Helper Hook
import { FormDataKeys, FormDataType, Method } from '@inertiajs/core';
// @ts-expect-error TS2307
import type { Response } from '@inertiajs/core/types/response';
import type { InertiaFormProps } from '@inertiajs/react';
import { useForm } from '@inertiajs/react';
import axios, { AxiosProgressEvent, AxiosRequestConfig, AxiosResponse } from 'axios';
import { cloneDeep } from 'lodash-es';
import { useCallback, useMemo, useRef, useState } from 'react';
type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete';
interface SubmitOptions<TForm extends FormDataType<TForm>, TResponse = Response, TError = unknown> {
resetResponsesOnSubmit?: boolean;
headers?: Record<string, string>;
onBefore?: () => void;
onStart?: () => void;
onProgress?: (event: AxiosProgressEvent) => void;
onSuccess?: (response: AxiosResponse<TResponse>) => void;
onError?: (error: TError) => void;
onValidationError?: (errors: Record<FormDataKeys<TForm>, string[]>) => void;
onFinish?: (error?: TError, response?: AxiosResponse<TResponse>) => void;
}
type ApiForm<TForm extends FormDataType<TForm>, TResponse = Response, TError = unknown> = Omit<
InertiaFormProps<TForm>,
'submit' | 'transform' | 'get' | 'post' | 'patch' | 'put' | 'delete'
> & {
successResponse?: AxiosResponse<TResponse>;
errorResponse?: TError;
transform: (callback: (data: TForm) => Record<string, unknown>) => void;
submit: (
...args:
| [Method, string, SubmitOptions<TForm, TResponse, TError>?]
| [{ url: string; method: Method }, SubmitOptions<TForm, TResponse, TError>?]
) => void;
submitAsync: (
...args:
| [Method, string, Omit<SubmitOptions<TForm, TResponse, TError>, 'onSuccess' | 'onError'>?]
| [{ url: string; method: Method }, Omit<SubmitOptions<TForm, TResponse, TError>, 'onSuccess' | 'onError'>?]
) => Promise<AxiosResponse<TResponse>>;
get: (url: string, options?: SubmitOptions<TForm, TResponse, TError>) => void;
getAsync: (
url: string,
options?: Omit<SubmitOptions<TForm, TResponse, TError>, 'onSuccess' | 'onError'>,
) => Promise<AxiosResponse<TResponse>>;
patch: (url: string, options?: SubmitOptions<TForm, TResponse, TError>) => void;
patchAsync: (
url: string,
options?: Omit<SubmitOptions<TForm, TResponse, TError>, 'onSuccess' | 'onError'>,
) => Promise<AxiosResponse<TResponse>>;
post: (url: string, options?: SubmitOptions<TForm, TResponse, TError>) => void;
postAsync: (
url: string,
options?: Omit<SubmitOptions<TForm, TResponse, TError>, 'onSuccess' | 'onError'>,
) => Promise<AxiosResponse<TResponse>>;
put: (url: string, options?: SubmitOptions<TForm, TResponse, TError>) => void;
putAsync: (
url: string,
options?: Omit<SubmitOptions<TForm, TResponse, TError>, 'onSuccess' | 'onError'>,
) => Promise<AxiosResponse<TResponse>>;
delete: (url: string, options?: SubmitOptions<TForm, TResponse, TError>) => void;
deleteAsync: (
url: string,
options?: Omit<SubmitOptions<TForm, TResponse, TError>, 'onSuccess' | 'onError'>,
) => Promise<AxiosResponse<TResponse>>;
createSetter: <K extends keyof TForm>(key: K) => (value: TForm[K] | ((prev: TForm[K]) => TForm[K])) => void;
};
export function useApiForm<TForm extends FormDataType<TForm>, TResponse = Response, TError = unknown>(
rememberKeyOrInitialValues: string | TForm | (() => TForm),
initialValues?: TForm | (() => TForm),
): ApiForm<TForm, TResponse, TError> {
const form =
initialValues === undefined
? // eslint-disable-next-line react-hooks/rules-of-hooks
useForm<TForm>(rememberKeyOrInitialValues as TForm | (() => TForm))
: // eslint-disable-next-line react-hooks/rules-of-hooks
useForm<TForm>(rememberKeyOrInitialValues as string, initialValues as TForm | (() => TForm));
const [successResponse, setSuccessResponse] = useState<AxiosResponse<TResponse> | undefined>();
const [errorResponse, setErrorResponse] = useState<TError | undefined>();
const [processing, setProcessing] = useState(false);
const [progress, setProgress] = useState<AxiosProgressEvent | null>(null);
const [wasSuccessful, setWasSuccessful] = useState(false);
const [recentlySuccessful, setRecentlySuccessful] = useState(false);
const recentlySuccessfulTimeoutId = useRef<ReturnType<typeof setTimeout> | null>(null);
const transformFn = useRef((data: TForm): Record<string, unknown> => data);
const hasFilesFn = useRef(function $(data: unknown): boolean {
return (
data instanceof File ||
data instanceof Blob ||
(data instanceof FileList && data.length > 0) ||
(data instanceof FormData && Array.from(data.values()).some((value) => $(value))) ||
(typeof data === 'object' && data !== null && Object.values(data).some((value) => $(value)))
);
});
const submit = useCallback(
(method: HttpMethod, url: string, options: SubmitOptions<TForm, TResponse, TError> = {}) => {
setWasSuccessful(false);
setRecentlySuccessful(false);
form.clearErrors();
if (recentlySuccessfulTimeoutId.current) {
clearTimeout(recentlySuccessfulTimeoutId.current);
recentlySuccessfulTimeoutId.current = null;
}
if (options.resetResponsesOnSubmit) {
setErrorResponse(undefined);
setSuccessResponse(undefined);
}
options.onBefore?.();
setProcessing(true);
options.onStart?.();
const rawData = transformFn.current(form.data);
const hasFiles = hasFilesFn.current(rawData);
const config: AxiosRequestConfig = {
url,
method: hasFiles ? 'post' : method,
headers: {
...options.headers,
Accept: 'application/json',
'Content-Type': hasFiles ? 'multipart/form-data' : 'application/json',
},
onUploadProgress: (event) => {
setProgress(event);
options.onProgress?.(event);
},
};
if (method === 'get') {
config.params = rawData;
} else if (hasFiles) {
rawData._method = method;
config.data = rawData;
} else {
config.data = rawData;
}
axios
.request<TForm, AxiosResponse<TResponse>>(config)
.then((response) => {
form.clearErrors();
setProcessing(false);
setProgress(null);
setSuccessResponse(response);
setErrorResponse(undefined);
setWasSuccessful(true);
setRecentlySuccessful(true);
recentlySuccessfulTimeoutId.current = setTimeout(() => {
setRecentlySuccessful(false);
}, 2000);
options.onSuccess?.(response);
options.onFinish?.(undefined, response);
form.setDefaults(cloneDeep(form.data));
form.isDirty = false;
})
.catch((error) => {
form.clearErrors();
setProcessing(false);
setProgress(null);
if (
axios.isAxiosError(error) &&
error.response?.status === 422 &&
error.response.data.errors !== null
) {
const errors = error.response.data.errors;
(Object.keys(errors) as Array<FormDataKeys<TForm>>).forEach((key) => {
const error = errors[key];
form.setError(key, Array.isArray(error) ? error[0] : String(error));
});
options.onValidationError?.(errors);
}
options.onError?.(error);
options.onFinish?.(error, undefined);
setErrorResponse(error as TError);
setSuccessResponse(undefined);
});
},
[
form,
transformFn,
recentlySuccessfulTimeoutId,
setWasSuccessful,
setRecentlySuccessful,
setProcessing,
setProgress,
setErrorResponse,
setSuccessResponse,
],
);
const submitAsync = useCallback(
async (
method: HttpMethod,
url: string,
options: Omit<SubmitOptions<TForm, TResponse, TError>, 'onError' | 'onSuccess'> = {},
): Promise<AxiosResponse<TResponse>> => {
return new Promise((resolve, reject) => {
submit(method, url, {
...options,
onSuccess: resolve,
onError: reject,
});
});
},
[submit],
);
const overrides = useMemo(
() => ({
successResponse,
errorResponse,
processing,
progress,
wasSuccessful,
recentlySuccessful,
transform:
(receiver: (data: TForm) => Record<string, unknown>) =>
(callback: (data: TForm) => Record<string, unknown>) => {
transformFn.current = callback;
return receiver;
},
submit: () => submit,
submitAsync: () => submitAsync,
post: () => submit.bind(null, 'post'),
postAsync: () => submitAsync.bind(null, 'post'),
get: () => submit.bind(null, 'get'),
getAsync: () => submitAsync.bind(null, 'get'),
put: () => submit.bind(null, 'put'),
putAsync: () => submitAsync.bind(null, 'put'),
patch: () => submit.bind(null, 'patch'),
patchAsync: () => submitAsync.bind(null, 'patch'),
delete: () => submit.bind(null, 'delete'),
deleteAsync: () => submitAsync.bind(null, 'delete'),
createSetter:
() =>
(key: keyof TForm) =>
(value: TForm[keyof TForm] | ((prev: TForm[keyof TForm]) => TForm[keyof TForm])) => {
return form.setData((data) => ({
...data,
[key]: typeof value === 'function' ? value(data[key]) : value,
}));
},
}),
[
transformFn,
form,
successResponse,
errorResponse,
processing,
progress,
wasSuccessful,
recentlySuccessful,
submit,
submitAsync,
],
);
return new Proxy(form, {
get: (target, prop, receiver) => {
if (prop in overrides) {
const value = overrides[prop as keyof typeof overrides];
if (typeof value === 'function') {
return (value as (receiver: unknown) => unknown)(receiver);
}
return value;
}
return (target as ApiForm<TForm, TResponse, TError>)[prop as keyof ApiForm<TForm, TResponse, TError>];
},
}) as ApiForm<TForm, TResponse, TError>;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment