Last active
August 29, 2025 14:05
-
-
Save 4lun/b91cdc4b82c1ff3b5506b47cb29dcfd4 to your computer and use it in GitHub Desktop.
Revisions
-
4lun revised this gist
Aug 29, 2025 . 1 changed file with 3 additions and 0 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -3,6 +3,9 @@ * Inertia.js, for use with general API endpoints. Any page updates or * navigation needs to be handled manually via onSuccess callback. * * Author: https://github.com/4lun * Source: https://gist.github.com/4lun/b91cdc4b82c1ff3b5506b47cb29dcfd4 * * Based on a combination of * - https://gist.github.com/mohitmamoria/91da6f30d9610b211248225c9e52bebe * - https://gist.github.com/mattiasghodsian/9b4ee07e805547aa13795dc3a28a206d -
4lun revised this gist
Aug 29, 2025 . 1 changed file with 6 additions and 6 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -7,29 +7,29 @@ * - https://gist.github.com/mohitmamoria/91da6f30d9610b211248225c9e52bebe * - https://gist.github.com/mattiasghodsian/9b4ee07e805547aa13795dc3a28a206d * * Version 0.3 * * Changes: * - v0.1 Rewritten to use React from Vue * - v0.1 Added TypeScript types * - v0.2 Fixed state updates of processing, progress, wasSuccessful, etc * - v0.3 Updated to new FormDataType<TForm> generic type */ import { useForm } from "@inertiajs/react"; import type { InertiaFormProps } from "@inertiajs/react"; import { cloneDeep } from "lodash-es"; import axios, { AxiosRequestConfig, AxiosProgressEvent } from "axios"; import { FormDataKeys, FormDataType, Method, VisitOptions, } from "@inertiajs/core"; import type { Response } from "@inertiajs/core/types/response"; import { useState } from "react"; type HttpMethod = "get" | "post" | "put" | "patch" | "delete"; type FormOptions = Omit<VisitOptions, "data">; interface SubmitOptions<TResponse = Response> { @@ -42,7 +42,7 @@ interface SubmitOptions<TResponse = Response> { onFinish?: () => void; } type APIForm<TForm extends FormDataType<TForm>> = InertiaFormProps<TForm> & { transform: (callback: (data: TForm) => object) => void; submit: ( ...args: @@ -69,7 +69,7 @@ export function hasFiles(data: any): boolean { ); } export function useAPIForm<TForm extends FormDataType<TForm>>( rememberKeyOrInitialValues: string | TForm, initialValues?: TForm, ): APIForm<TForm> { -
4lun revised this gist
Jul 24, 2025 . No changes.There are no files selected for viewing
-
4lun revised this gist
Jul 24, 2025 . 1 changed file with 3 additions and 3 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -1,7 +1,7 @@ /** * A utility hook that is a drop in replacement for the useForm hook from * Inertia.js, for use with general API endpoints. Any page updates or * navigation needs to be handled manually via onSuccess callback. * * Based on a combination of * - https://gist.github.com/mohitmamoria/91da6f30d9610b211248225c9e52bebe -
4lun revised this gist
Jul 24, 2025 . 1 changed file with 47 additions and 32 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -1,15 +1,18 @@ /** * A utility hook for that wraps the useForm hook from Inertia.js, but for use * with general API endpoints. Any page updates or navigation needs to be * handled manually via onSuccess callback. * * Based on a combination of * - https://gist.github.com/mohitmamoria/91da6f30d9610b211248225c9e52bebe * - https://gist.github.com/mattiasghodsian/9b4ee07e805547aa13795dc3a28a206d * * Version 0.2 * * Changes: * - v0.1 Rewritten to use React from Vue * - v0.1 Added TypeScript types * - v0.2 Fixed state updates of processing, progress, wasSuccessful, etc */ import { useForm } from "@inertiajs/react"; @@ -23,6 +26,7 @@ import { VisitOptions, } from "@inertiajs/core"; import type { Response } from "@inertiajs/core/types/response"; import { useState } from "react"; type HttpMethod = "get" | "post" | "put" | "patch" | "delete"; type FormDataType = Record<string, FormDataConvertible>; @@ -43,14 +47,12 @@ type APIForm<TForm extends FormDataType> = InertiaFormProps<TForm> & { submit: ( ...args: | [Method, string, FormOptions?] | [{ url: string; method: Method }, FormOptions?] ) => void; processing: boolean; progress: AxiosProgressEvent | null; wasSuccessful: boolean; recentlySuccessful: boolean; }; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -66,6 +68,7 @@ export function hasFiles(data: any): boolean { Object.values(data).some((value) => hasFiles(value))) ); } export function useAPIForm<TForm extends FormDataType>( rememberKeyOrInitialValues: string | TForm, initialValues?: TForm, @@ -78,24 +81,28 @@ export function useAPIForm<TForm extends FormDataType>( initialValues, ); const [processing, setProcessing] = useState(false); const [progress, setProgress] = useState<AxiosProgressEvent | null>(null); const [wasSuccessful, setWasSuccessful] = useState(false); const [recentlySuccessful, setRecentlySuccessful] = useState(false); let recentlySuccessfulTimeoutId: ReturnType<typeof setTimeout> | null = null; let transformFn = (data: TForm): object => data; const submit = ( method: HttpMethod, url: string, options: SubmitOptions = {}, ) => { setWasSuccessful(false); setRecentlySuccessful(false); form.clearErrors(); if (recentlySuccessfulTimeoutId) clearTimeout(recentlySuccessfulTimeoutId); options.onBefore?.(); setProcessing(true); options.onStart?.(); const rawData = transformFn(form.data); @@ -110,7 +117,7 @@ export function useAPIForm<TForm extends FormDataType>( : "application/json", }, onUploadProgress: (event) => { setProgress(event); options.onProgress?.(event); }, }; @@ -123,22 +130,22 @@ export function useAPIForm<TForm extends FormDataType>( axios(config) .then((response) => { setProcessing(false); setProgress(null); form.clearErrors(); setWasSuccessful(true); setRecentlySuccessful(true); recentlySuccessfulTimeoutId = setTimeout(() => { setRecentlySuccessful(false); }, 2000); options.onSuccess?.(response.data); form.setDefaults(cloneDeep(form.data)); form.isDirty = false; }) .catch((error) => { setProcessing(false); setProgress(null); form.clearErrors(); if ( @@ -157,8 +164,8 @@ export function useAPIForm<TForm extends FormDataType>( options.onError?.(error); }) .finally(() => { setProcessing(false); setProgress(null); options.onFinish?.(); }); }; @@ -179,9 +186,17 @@ export function useAPIForm<TForm extends FormDataType>( }; return new Proxy(form, { get: (target, prop, receiver) => { if (prop in overriders) { return overriders[prop as keyof typeof overriders](receiver); } if (prop === "processing") return processing; if (prop === "progress") return progress; if (prop === "wasSuccessful") return wasSuccessful; if (prop === "recentlySuccessful") return recentlySuccessful; return (target as APIForm<TForm>)[prop as keyof APIForm<TForm>]; }, }) as APIForm<TForm>; } -
4lun revised this gist
Jul 24, 2025 . 1 changed file with 3 additions and 3 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -1,7 +1,7 @@ /** * A utility hook that is a drop in replacement for the useForm hook from * Inertia.js, for use with general API endpoints. Any page updates or * navigation needs to be handled manually via onSuccess callback. * * Based on a combination of * - https://gist.github.com/mohitmamoria/91da6f30d9610b211248225c9e52bebe -
4lun created this gist
Jul 24, 2025 .There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,187 @@ /** * A utility hook for that wraps the useForm hook from Inertia.js, but for use * with general API endpoints. Any page updates or navigation needs to be * handled manually via onSuccess callback. * * Based on a combination of * - https://gist.github.com/mohitmamoria/91da6f30d9610b211248225c9e52bebe * - https://gist.github.com/mattiasghodsian/9b4ee07e805547aa13795dc3a28a206d * * Changes: * - Rewritten to use React from Vue * - Added TypeScript types */ import { useForm } from "@inertiajs/react"; import type { InertiaFormProps } from "@inertiajs/react"; import cloneDeep from "lodash.clonedeep"; import axios, { AxiosRequestConfig, AxiosProgressEvent } from "axios"; import { FormDataConvertible, FormDataKeys, Method, VisitOptions, } from "@inertiajs/core"; import type { Response } from "@inertiajs/core/types/response"; type HttpMethod = "get" | "post" | "put" | "patch" | "delete"; type FormDataType = Record<string, FormDataConvertible>; type FormOptions = Omit<VisitOptions, "data">; interface SubmitOptions<TResponse = Response> { headers?: Record<string, string>; onBefore?: () => void; onStart?: () => void; onProgress?: (event: AxiosProgressEvent) => void; onSuccess?: (response: TResponse) => void; onError?: (error: unknown) => void; onFinish?: () => void; } type APIForm<TForm extends FormDataType> = InertiaFormProps<TForm> & { transform: (callback: (data: TForm) => object) => void; submit: ( ...args: | [Method, string, FormOptions?] | [ { url: string; method: Method; }, FormOptions?, ] ) => void; }; // eslint-disable-next-line @typescript-eslint/no-explicit-any export function hasFiles(data: any): boolean { return ( data instanceof File || data instanceof Blob || (data instanceof FileList && data.length > 0) || (data instanceof FormData && Array.from(data.values()).some((value) => hasFiles(value))) || (typeof data === "object" && data !== null && Object.values(data).some((value) => hasFiles(value))) ); } export function useAPIForm<TForm extends FormDataType>( rememberKeyOrInitialValues: string | TForm, initialValues?: TForm, ): APIForm<TForm> { const form = initialValues === undefined ? useForm<TForm>(rememberKeyOrInitialValues as TForm) : useForm<TForm>( rememberKeyOrInitialValues as string, initialValues, ); let transformFn = (data: TForm): object => data; let recentlySuccessfulTimeoutId: ReturnType<typeof setTimeout> | null = null; const submit = ( method: HttpMethod, url: string, options: SubmitOptions = {}, ) => { form.wasSuccessful = false; form.recentlySuccessful = false; form.clearErrors(); if (recentlySuccessfulTimeoutId) { clearTimeout(recentlySuccessfulTimeoutId); } options.onBefore?.(); form.processing = true; options.onStart?.(); const rawData = transformFn(form.data); const config: AxiosRequestConfig = { method, url, headers: { ...options.headers, "Content-Type": hasFiles(rawData) ? "multipart/form-data" : "application/json", }, onUploadProgress: (event) => { form.progress = event; options.onProgress?.(event); }, }; if (method === "get") { config.params = rawData; } else { config.data = rawData; } axios(config) .then((response) => { form.processing = false; form.progress = null; form.clearErrors(); form.wasSuccessful = true; form.recentlySuccessful = true; recentlySuccessfulTimeoutId = setTimeout(() => { form.recentlySuccessful = false; }, 2000); options.onSuccess?.(response.data); form.setDefaults(cloneDeep(form.data)); form.isDirty = false; }) .catch((error) => { form.processing = false; form.progress = null; form.clearErrors(); if ( axios.isAxiosError(error) && error.response?.status === 422 && error.response.data.errors ) { const errors = error.response.data.errors; (Object.keys(errors) as Array<FormDataKeys<TForm>>).forEach( (key) => { form.setError(key, errors[key][0]); }, ); } options.onError?.(error); }) .finally(() => { form.processing = false; form.progress = null; options.onFinish?.(); }); }; const overriders = { transform: (receiver: (data: TForm) => object) => (callback: (data: TForm) => object) => { transformFn = callback; return receiver; }, submit: () => submit, post: () => submit.bind(null, "post"), get: () => submit.bind(null, "get"), put: () => submit.bind(null, "put"), patch: () => submit.bind(null, "patch"), delete: () => submit.bind(null, "delete"), }; return new Proxy(form, { get: (target, prop, receiver) => prop in overriders ? overriders[prop as keyof typeof overriders](receiver) : (target as APIForm<TForm>)[prop as keyof APIForm<TForm>], }) as APIForm<TForm>; }