From 0ccc5ff52afa926aa772cff89d9ea81fa8e0271c Mon Sep 17 00:00:00 2001 From: Andrei Fedosjeenko Date: Tue, 8 Apr 2025 18:24:25 +0200 Subject: [PATCH 1/2] fix: TS types --- src/hook/index.tsx | 44 ++++++++++++++++++++--------------------- src/middleware/index.ts | 4 ++-- src/utilities/index.ts | 26 ++++++++++++------------ 3 files changed, 37 insertions(+), 37 deletions(-) diff --git a/src/hook/index.tsx b/src/hook/index.tsx index 2f48997..116cedf 100644 --- a/src/hook/index.tsx +++ b/src/hook/index.tsx @@ -37,11 +37,11 @@ import { createFormData } from "../utilities"; export type SubmitFunctionOptions = Parameters[1]; -export interface UseRemixFormOptions - extends UseFormProps { +export interface UseRemixFormOptions + extends UseFormProps { submitHandlers?: { - onValid?: SubmitHandler; - onInvalid?: SubmitErrorHandler; + onValid?: SubmitHandler; + onInvalid?: SubmitErrorHandler; }; submitConfig?: SubmitFunctionOptions; submitData?: FieldValues; @@ -52,14 +52,14 @@ export interface UseRemixFormOptions stringifyAllValues?: boolean; } -export const useRemixForm = ({ +export const useRemixForm = ({ submitHandlers, submitConfig, submitData, fetcher, stringifyAllValues = true, ...formProps -}: UseRemixFormOptions) => { +}: UseRemixFormOptions) => { const [isSubmittedSuccessfully, setIsSubmittedSuccessfully] = useState(false); const basename = useHref("/"); const actionSubmit = useSubmit(); @@ -67,14 +67,14 @@ export const useRemixForm = ({ const submit = fetcher?.submit ?? actionSubmit; // biome-ignore lint/suspicious/noExplicitAny: const data: any = fetcher?.data ?? actionData; - const methods = useForm({ ...formProps, errors: data?.errors }); + const methods = useForm({ ...formProps, errors: data?.errors }); const navigation = useNavigation(); // Either it's submitted to an action or submitted to a fetcher (or neither) const isSubmittingForm = useMemo( () => Boolean( (navigation.state !== "idle" && navigation.formData !== undefined) || - (fetcher?.state !== "idle" && fetcher?.formData !== undefined), + (fetcher?.state !== "idle" && fetcher?.formData !== undefined), ), [navigation.state, navigation.formData, fetcher?.state, fetcher?.formData], ); @@ -92,7 +92,7 @@ export const useRemixForm = ({ const onSubmit = useMemo( () => ( - data: T, + data: TTransformedValues, // biome-ignore lint/suspicious/noExplicitAny: e: any, formEncType?: FormEncType, @@ -121,11 +121,11 @@ export const useRemixForm = ({ ); // eslint-disable-next-line @typescript-eslint/no-empty-function - const onInvalid = useMemo(() => () => {}, []); + const onInvalid = useMemo(() => () => { }, []); // React-hook-form uses lazy property getters to avoid re-rendering when properties // that aren't being used change. Using getters here preservers that lazy behavior. - const formState: FormState = useMemo( + const formState: FormState = useMemo( () => ({ get isDirty() { return methods.formState.isDirty; @@ -175,7 +175,7 @@ export const useRemixForm = ({ const reset = useMemo( () => ( - values?: T | DefaultValues | undefined, + values?: TFieldValues | DefaultValues | undefined, options?: KeepStateOptions, ) => { setIsSubmittedSuccessfully(false); @@ -187,8 +187,8 @@ export const useRemixForm = ({ const register = useMemo( () => ( - name: Path, - options?: RegisterOptions & { + name: Path, + options?: RegisterOptions & { disableProgressiveEnhancement?: boolean; }, ) => { @@ -242,14 +242,14 @@ export const useRemixForm = ({ return hookReturn; }; -export type UseRemixFormReturn = UseFormReturn & { +export type UseRemixFormReturn = UseFormReturn & { handleSubmit: ReturnType["handleSubmit"]; reset: ReturnType["reset"]; register: ReturnType["register"]; }; -interface RemixFormProviderProps - extends Omit, "handleSubmit" | "reset"> { +interface RemixFormProviderProps + extends Omit, "handleSubmit" | "reset"> { children: ReactNode; // biome-ignore lint/suspicious/noExplicitAny: handleSubmit: any; @@ -258,20 +258,20 @@ interface RemixFormProviderProps // biome-ignore lint/suspicious/noExplicitAny: reset: any; } -export const RemixFormProvider = ({ +export const RemixFormProvider = ({ children, ...props -}: RemixFormProviderProps) => { +}: RemixFormProviderProps) => { return {children}; }; -export const useRemixFormContext = () => { - const methods = useFormContext(); +export const useRemixFormContext = () => { + const methods = useFormContext(); return { ...methods, // biome-ignore lint/suspicious/noExplicitAny: handleSubmit: methods.handleSubmit as any as ReturnType< - UseFormHandleSubmit + UseFormHandleSubmit >, }; }; diff --git a/src/middleware/index.ts b/src/middleware/index.ts index fef00dd..f6ce697 100644 --- a/src/middleware/index.ts +++ b/src/middleware/index.ts @@ -36,9 +36,9 @@ export function unstable_extractFormDataMiddleware({ export const getFormData = (context: unstable_RouterContextProvider) => context.get(formDataContext); -export const getValidatedFormData = async ( +export const getValidatedFormData = async ( context: unstable_RouterContextProvider, - resolver: Resolver, + resolver: Resolver, ) => { const formData = context.get(formDataContext); const data = await validateFormData(formData, resolver); diff --git a/src/utilities/index.ts b/src/utilities/index.ts index a8f523f..ff613e8 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -90,23 +90,23 @@ export const getFormDataFromSearchParams = ( export const isGet = (request: Pick) => request.method === "GET" || request.method === "get"; -type ReturnData = - | { data: T; errors: undefined; receivedValues: Partial } - | { data: undefined; errors: FieldErrors; receivedValues: Partial }; +type ReturnData = + | { data: TTransformedValues; errors: undefined; receivedValues: Partial } + | { data: undefined; errors: FieldErrors; receivedValues: Partial }; /** * Parses the data from an HTTP request and validates it against a schema. Works in both loaders and actions, in loaders it extracts the data from the search params. * In actions it extracts it from request formData. * * @returns A Promise that resolves to an object containing the validated data or any errors that occurred during validation. */ -export const getValidatedFormData = async ( +export const getValidatedFormData = async ( request: Request | FormData, - resolver: Resolver, + resolver: Resolver, preserveStringified = false, -): Promise> => { - const { receivedValues } = await getFormData(request, preserveStringified); +): Promise> => { + const { receivedValues } = await getFormData(request, preserveStringified); - const data = await validateFormData(receivedValues, resolver); + const data = await validateFormData(receivedValues, resolver); return { ...data, receivedValues }; }; @@ -134,24 +134,24 @@ export const getFormData = async ( * @param resolver Schema to validate and cast the data with * @returns Returns the validated data if successful, otherwise returns the error object */ -export const validateFormData = async ( +export const validateFormData = async ( // biome-ignore lint/suspicious/noExplicitAny: data: any, - resolver: Resolver, + resolver: Resolver, ) => { const dataToValidate = data instanceof FormData ? Object.fromEntries(data) : data; const { errors, values } = await resolver( dataToValidate, - {}, + {} as TContext, { shouldUseNativeValidation: false, fields: {} }, ); if (Object.keys(errors).length > 0) { - return { errors: errors as FieldErrors, data: undefined }; + return { errors: errors as FieldErrors, data: undefined }; } - return { errors: undefined, data: values as T }; + return { errors: undefined, data: values as TTransformedValues }; }; /** Creates a new instance of FormData with the specified data and key. From b66a123cdfbe8540c23b60d474023895ae873ce5 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Wed, 9 Apr 2025 12:44:33 +0200 Subject: [PATCH 2/2] lint fix --- package.json | 2 +- src/hook/index.tsx | 55 ++++++++++++++++++++++++++++++----------- src/middleware/index.ts | 13 ++++++++-- src/utilities/index.ts | 44 ++++++++++++++++++++++++--------- 4 files changed, 86 insertions(+), 28 deletions(-) diff --git a/package.json b/package.json index e0ac03e..8fcafa2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "remix-hook-form", - "version": "7.0.0", + "version": "7.0.1", "description": "Utility wrapper around react-hook-form for use with react-router v7+", "type": "module", "main": "./dist/index.cjs", diff --git a/src/hook/index.tsx b/src/hook/index.tsx index 116cedf..bc5a91a 100644 --- a/src/hook/index.tsx +++ b/src/hook/index.tsx @@ -37,8 +37,12 @@ import { createFormData } from "../utilities"; export type SubmitFunctionOptions = Parameters[1]; -export interface UseRemixFormOptions - extends UseFormProps { +export interface UseRemixFormOptions< + TFieldValues extends FieldValues, + // biome-ignore lint/suspicious/noExplicitAny: defaults to any type + TContext = any, + TTransformedValues = TFieldValues, +> extends UseFormProps { submitHandlers?: { onValid?: SubmitHandler; onInvalid?: SubmitErrorHandler; @@ -51,8 +55,12 @@ export interface UseRemixFormOptions({ +export const useRemixForm = < + TFieldValues extends FieldValues, + // biome-ignore lint/suspicious/noExplicitAny: defaults to any type + TContext = any, + TTransformedValues = TFieldValues, +>({ submitHandlers, submitConfig, submitData, @@ -74,7 +82,7 @@ export const useRemixForm = Boolean( (navigation.state !== "idle" && navigation.formData !== undefined) || - (fetcher?.state !== "idle" && fetcher?.formData !== undefined), + (fetcher?.state !== "idle" && fetcher?.formData !== undefined), ), [navigation.state, navigation.formData, fetcher?.state, fetcher?.formData], ); @@ -121,7 +129,7 @@ export const useRemixForm = () => { }, []); + const onInvalid = useMemo(() => () => {}, []); // React-hook-form uses lazy property getters to avoid re-rendering when properties // that aren't being used change. Using getters here preservers that lazy behavior. @@ -241,15 +249,25 @@ export const useRemixForm = = UseFormReturn & { +export type UseRemixFormReturn< + TFieldValues extends FieldValues = FieldValues, + // biome-ignore lint/suspicious/noExplicitAny: defaults to any type + TContext = any, + TTransformedValues = TFieldValues, +> = UseFormReturn & { handleSubmit: ReturnType["handleSubmit"]; reset: ReturnType["reset"]; register: ReturnType["register"]; }; - -interface RemixFormProviderProps - extends Omit, "handleSubmit" | "reset"> { +interface RemixFormProviderProps< + TFieldValues extends FieldValues = FieldValues, + // biome-ignore lint/suspicious/noExplicitAny: defaults to any type + TContext = any, + TTransformedValues = TFieldValues, +> extends Omit< + UseFormReturn, + "handleSubmit" | "reset" + > { children: ReactNode; // biome-ignore lint/suspicious/noExplicitAny: handleSubmit: any; @@ -258,14 +276,23 @@ interface RemixFormProviderProps reset: any; } -export const RemixFormProvider = ({ +export const RemixFormProvider = < + TFieldValues extends FieldValues = FieldValues, + // biome-ignore lint/suspicious/noExplicitAny: defaults to any type + TContext = any, + TTransformedValues = TFieldValues, +>({ children, ...props }: RemixFormProviderProps) => { return {children}; }; - -export const useRemixFormContext = () => { +export const useRemixFormContext = < + TFieldValues extends FieldValues, + // biome-ignore lint/suspicious/noExplicitAny: defaults to any type + TContext = any, + TTransformedValues = TFieldValues, +>() => { const methods = useFormContext(); return { ...methods, diff --git a/src/middleware/index.ts b/src/middleware/index.ts index f6ce697..fa349d8 100644 --- a/src/middleware/index.ts +++ b/src/middleware/index.ts @@ -36,12 +36,21 @@ export function unstable_extractFormDataMiddleware({ export const getFormData = (context: unstable_RouterContextProvider) => context.get(formDataContext); -export const getValidatedFormData = async ( +export const getValidatedFormData = async < + TFieldValues extends FieldValues, + // biome-ignore lint/suspicious/noExplicitAny: defaults to any type + TContext = any, + TTransformedValues = TFieldValues, +>( context: unstable_RouterContextProvider, resolver: Resolver, ) => { const formData = context.get(formDataContext); - const data = await validateFormData(formData, resolver); + const data = await validateFormData< + TFieldValues, + TContext, + TTransformedValues + >(formData, resolver); /* if (errors) { throw dataFn( { errors, receivedValues: formData }, diff --git a/src/utilities/index.ts b/src/utilities/index.ts index ff613e8..486f71c 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -90,21 +90,40 @@ export const getFormDataFromSearchParams = ( export const isGet = (request: Pick) => request.method === "GET" || request.method === "get"; -type ReturnData = - | { data: TTransformedValues; errors: undefined; receivedValues: Partial } - | { data: undefined; errors: FieldErrors; receivedValues: Partial }; +type ReturnData< + TFieldValues extends FieldValues, + TTransformedValues = TFieldValues, +> = + | { + data: TTransformedValues; + errors: undefined; + receivedValues: Partial; + } + | { + data: undefined; + errors: FieldErrors; + receivedValues: Partial; + }; /** * Parses the data from an HTTP request and validates it against a schema. Works in both loaders and actions, in loaders it extracts the data from the search params. * In actions it extracts it from request formData. * * @returns A Promise that resolves to an object containing the validated data or any errors that occurred during validation. */ -export const getValidatedFormData = async ( +export const getValidatedFormData = async < + TFieldValues extends FieldValues, + // biome-ignore lint/suspicious/noExplicitAny: any by default + TContext = any, + TTransformedValues = TFieldValues, +>( request: Request | FormData, resolver: Resolver, preserveStringified = false, ): Promise> => { - const { receivedValues } = await getFormData(request, preserveStringified); + const { receivedValues } = await getFormData( + request, + preserveStringified, + ); const data = await validateFormData(receivedValues, resolver); @@ -134,18 +153,21 @@ export const getFormData = async ( * @param resolver Schema to validate and cast the data with * @returns Returns the validated data if successful, otherwise returns the error object */ -export const validateFormData = async ( +export const validateFormData = async < + TFieldValues extends FieldValues, + TContext, + TTransformedValues = TFieldValues, +>( // biome-ignore lint/suspicious/noExplicitAny: data: any, resolver: Resolver, ) => { const dataToValidate = data instanceof FormData ? Object.fromEntries(data) : data; - const { errors, values } = await resolver( - dataToValidate, - {} as TContext, - { shouldUseNativeValidation: false, fields: {} }, - ); + const { errors, values } = await resolver(dataToValidate, {} as TContext, { + shouldUseNativeValidation: false, + fields: {}, + }); if (Object.keys(errors).length > 0) { return { errors: errors as FieldErrors, data: undefined };