Last active
November 14, 2025 06:01
-
-
Save devhammed/83cc47ef0c75167adb15370eba75a5b6 to your computer and use it in GitHub Desktop.
Laravel Inertia JSON API Form Helper Hook
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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