forge42dev / remix-hook-form Goto Github PK
View Code? Open in Web Editor NEWOpen source wrapper for react-hook-form aimed at Remix.run
License: MIT License
Open source wrapper for react-hook-form aimed at Remix.run
License: MIT License
const {
data,
errors,
receivedValues: defaultValues,
} = await getValidatedFormData<FormData>(request, resolver);
if (errors) {
return json({ defaultValues, errors });
}
// router.push(`/article/${data.id}/outline`);
return json(data);
};
export default () => {
const form = useRemixForm<FormData>({
mode: 'onSubmit',
resolver,
});
// toast({
// title: 'Article Project Created',
// });
My use case is that I want to trigger toast after successful action.
In the below code, you are stringifying the whole form and Files can not be stringified they do not get sent to the server.
export const createFormData = <T extends FieldValues>( data: T, key = "formData" ): FormData => { const formData = new FormData(); const finalData = JSON.stringify(data); formData.append(key, finalData); return formData; };
I'm working on the PR to fix this, and will submit it once I'm finished. I just wanted to document the issue first.
Hey folks. ๐
I'm trying to setup a new Remix app (v2 already) and it seems the new tsconfig.json
is conflicting with how remix-hook-form
is shipping types.
Now, Remix'stsconfig.json
define something like this:
{
// ... rest
"compilerOptions": {
// ... rest
// NEW REMIX SETUP
"module": "ESNext",
"moduleResolution": "Bundler",
"target": "ES2022"
}
}
And because of the module resolution got changed, I started getting 2 errors and I think they are related.
Here's what happens when I run pnpm tsc
:
$ pnpm typecheck
> remix-hook-form-error@ typecheck /Users/raulmelo/development/personal/error-repos/remix-hook-form-error
> tsc
app/routes/_index.tsx:37:5 - error TS2345: Argument of type '{ mode: string; resolver: <TFieldValues extends FieldValues, TContext>(values: TFieldValues, context: TContext | undefined, options: ResolverOptions<TFieldValues>) => Promise<...>; }' is not assignable to parameter of type 'UseRemixFormOptions<FieldValues$1>'.
Object literal may only specify known properties, and 'mode' does not exist in type 'UseRemixFormOptions<FieldValues$1>'.
37 mode: "onSubmit",
~~~~
node_modules/.pnpm/[email protected]_@[email protected][email protected][email protected][email protected]/node_modules/remix-hook-form/dist/index.d.ts:5:98 - error TS2307: Cannot find module 'react-hook-form/dist/types' or its corresponding type declarations.
5 import { FieldValues as FieldValues$1, UseFormProps, Path, RegisterOptions, UseFormReturn } from 'react-hook-form/dist/types';
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Found 2 errors in 2 files.
Errors Files
1 app/routes/_index.tsx:37
1 node_modules/.pnpm/[email protected]_@[email protected][email protected][email protected][email protected]/node_modules/remix-hook-form/dist/index.d.ts:5
After some debuging, I noticed that's because remix-hook-form
is importing the types from a path that "does not exists in the export" (which seems to be relevant when we have "bundler" mode set):
Fix I change it by removing the submodule:
-import { FieldValues as FieldValues$1, UseFormProps, Path, RegisterOptions, UseFormReturn } from 'react-hook-form/dist/types';
+import { FieldValues as FieldValues$1, UseFormProps, Path, RegisterOptions, UseFormReturn } from 'react-hook-form';
Everything seems to be working again:
Not sure about the backwards compatibility of it though. I think some tests would be necessary.
I've created a repository to help you to debug this problem: https://github.com/devraul/remix-v2-remix-hook-form-type-error
Or, you can simply:
zod
, remix-hook-form
, etc.;dev
and build
, everything will just work;pnpm typecheck
to see the errorAlso, there's 2 friendly suggestions regarding the type def:
types
field in the export:
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs",
+ "types": "./dist/index.d.ts"
}
},
types
), just for consistency: https://www.typescriptlang.org/docs/handbook/declaration-files/publishing.html#including-declarations-in-your-npm-packageLet me know if you need something else.
Cheers!
Hi,
I noticed that formState.isSubmitting
is not working as expected. I tried to setup this library with Remix, implementing some slow action.
isSubmitting
is set to false
as soon as the POST network call is fired, before it returns.
isSubmitting
should stay true
until action POST terminates.
In react-hook-form
, formState.isSubmitting
is true
as long as onSubmit
promise is not either rejected or resolved, onSubmit
being the argument of handleSubmit
.
In remix-hook-form
, users don't provide any onSubmit
function as it's using the native implementation, triggering a Remix action.
In this repo, you're using Remix's const submit = useSubmit()
hook to submit the form. But it does not seem to me that there is any way to listen to termination of that submit event (e.g. wait for the end of the POST request).
Actually, there is a debate here about returning a promise for useSubmit()
hook in react-router. They also give a workaround that could work for us : to use useActionData()
hook to know when the remix action is complete.
I guess we could do that to properly set formState.isSubmitting, submitted, ...
etc.
let actionData = useActionData();
useEffect(() => {
if (actionData.ok) {
complete(actionData)
}
}, [actionData]);
Tried implementing the first example in the readme, but there is a type error:
Object literal may only specify known properties, and 'mode' does not exist in type 'UseRemixFormOptions<{ name: string; email: string; }>'.ts(2353)
"remix-hook-form": "^4.0.0",
"zod": "^3.22.4"
"@hookform/resolvers": "^3.3.4",
All deps:
"dependencies": {
"@hookform/resolvers": "^3.3.4",
"@remix-run/css-bundle": "^2.5.0",
"@remix-run/node": "^2.5.0",
"@remix-run/react": "^2.5.0",
"@remix-run/serve": "^2.5.0",
"drizzle-orm": "^0.29.3",
"isbot": "^4.1.0",
"pg": "^8.11.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"remix-hook-form": "^4.0.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@playwright/test": "^1.40.1",
"@remix-run/dev": "^2.5.0",
"@types/node": "^20.11.0",
"@types/pg": "^8.10.9",
"@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.7.4",
"daisyui": "^4.6.0",
"drizzle-kit": "^0.20.12",
"eslint": "^8.38.0",
"eslint-config-prettier": "^9.0.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"tailwindcss": "^3.4.1",
"typescript": "^5.1.6"
},
Since 3.2.0
all of my forms that has submitData
passes strings with quotes.
Example:
const {
handleSubmit,
formState: { errors },
register,
setValue,
} = useRemixForm({
mode: "onSubmit",
defaultValues: {
title: currentCourse.title ?? "",
desc: currentCourse.desc ?? "",
},
submitData: {
submitAction: "SAVE",
},
resolver,
});
According to this line of the hook, the isSubmitting
state is true when the navigation state (or a fetcher) is not "idle"
.
This includes a moment after the response from action is received but the loaders are revalidating.
I believe the check should rather be === "submitting"
, as per Remix docs:
submitting - A route action is being called due to a form submission using POST, PUT, PATCH, or DELETE
This would make sure that formState.isSubmitting
is actually reflecting the submitting
state, and not loading
.
What do you think?
Hi there,
A bit new to this library and react-hook-form
.
How can I use the form states to show a loading and also a message when form is submitted successfully?
I saw all of formState
available properties, but didn't manage to put it to work.
Thanks!!
I'm getting a type error when attempting to pass in a fetcher to my form configuration.
Here's a minimal reproduction:
import { zodResolver } from '@hookform/resolvers/zod'
import { useFetcher } from '@remix-run/react'
import { RemixFormProvider, useRemixForm } from 'remix-hook-form'
import { z } from 'zod'
const formSchema = z.object({
username: z.string().min(3).max(20),
password: z.string().min(8).max(100),
})
const resolver = zodResolver(formSchema)
export const ExampleForm = () => {
const fetcher = useFetcher()
const form = useRemixForm({
resolver,
defaultValues: {
username: '',
password: '',
},
fetcher,
})
return (
<RemixFormProvider {...form}>
...
</RemixFormProvider>
)
}
And the type error I'm getting with the fetcher field in the hook is:
Type 'FetcherWithComponents<unknown>' is not assignable to type 'FetcherWithComponents<FieldValues> | undefined'.
Type '{ state: "idle"; formMethod: undefined; formAction: undefined; formEncType: undefined; text: undefined; formData: undefined; json: undefined; data: unknown; } & { Form: ForwardRefExoticComponent<FetcherFormProps & RefAttributes<...>>; submit: FetcherSubmitFunction; load: (href: string) => void; }' is not assignable to type 'FetcherWithComponents<FieldValues> | undefined'.
Type '{ state: "idle"; formMethod: undefined; formAction: undefined; formEncType: undefined; text: undefined; formData: undefined; json: undefined; data: unknown; } & { Form: ForwardRefExoticComponent<FetcherFormProps & RefAttributes<...>>; submit: FetcherSubmitFunction; load: (href: string) => void; }' is not assignable to type '{ state: "idle"; formMethod: undefined; formAction: undefined; formEncType: undefined; text: undefined; formData: undefined; json: undefined; data: FieldValues | undefined; } & { ...; }'.
Type '{ state: "idle"; formMethod: undefined; formAction: undefined; formEncType: undefined; text: undefined; formData: undefined; json: undefined; data: unknown; } & { Form: ForwardRefExoticComponent<FetcherFormProps & RefAttributes<...>>; submit: FetcherSubmitFunction; load: (href: string) => void; }' is not assignable to type '{ state: "idle"; formMethod: undefined; formAction: undefined; formEncType: undefined; text: undefined; formData: undefined; json: undefined; data: FieldValues | undefined; }'.
Types of property 'data' are incompatible.
Type 'unknown' is not assignable to type 'FieldValues | undefined'.ts(2322)
index.d.ts(64, 5): The expected type comes from property 'fetcher' which is declared here on type 'UseRemixFormOptions<FieldValues>'
I've tried typing my fetcher more explicitly (via useFetcher<typeof action>()
) with no luck. Any ideas or suggestions would be very much appreciated!
Once the submission is successful, I have a form that stays on the same page once the action is complete (account page in settings).
I need the form to reset (i.e. isDirty) to go back to false.
Is there a way to do this without using a useEffect? Totally happy to contribute to the docs
I am trying to migrate from react-hook-form
as it was used with https://shadow-panda.dev/, and I am missing a few elements to piece it together. This is what I have so far.
import { Label } from '@/components/ui/label';
import { css, cx } from '@/styled-system/css';
import { styled } from '@/styled-system/jsx';
import {
formControl,
formDescription,
formItem,
formLabel,
formMessage,
} from '@/styled-system/recipes';
import type * as LabelPrimitive from '@radix-ui/react-label';
import { Slot } from '@radix-ui/react-slot';
import { createContext, forwardRef, useContext, useId, useMemo } from 'react';
import {
Controller,
type ControllerProps,
type FieldPath,
type FieldValues,
useRemixFormContext,
} from 'remix-hook-form';
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName;
};
type FormItemContextValue = {
id: string;
};
const FormFieldContext = createContext<FormFieldContextValue>(
{} as FormFieldContextValue,
);
const FormItemContext = createContext<FormItemContextValue>(
{} as FormItemContextValue,
);
export const useFormField = () => {
const fieldContext = useContext(FormFieldContext);
const itemContext = useContext(FormItemContext);
const { formState, getFieldState } = useRemixFormContext();
const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) {
throw new Error('useFormField should be used within <FormField>');
}
const { id } = itemContext;
return {
formDescriptionId: `${id}-form-item-description`,
formItemId: `${id}-form-item`,
formMessageId: `${id}-form-item-message`,
id,
name: fieldContext.name,
...fieldState,
};
};
const BaseFormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
const context = useMemo(() => {
return {
name: props.name,
};
}, [props.name]);
return (
<FormFieldContext.Provider value={context}>
<Controller {...props} />
</FormFieldContext.Provider>
);
};
const BaseFormItem = forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>((props, ref) => {
const id = useId();
const context = useMemo(() => {
return {
id,
};
}, [id]);
return (
<FormItemContext.Provider value={context}>
<div
ref={ref}
{...props}
/>
</FormItemContext.Provider>
);
});
BaseFormItem.displayName = 'FormItem';
const BaseFormLabel = forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof Label>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField();
return (
<Label
className={cx(error && css({ color: 'destructive' }), className)}
htmlFor={formItemId}
ref={ref}
{...props}
/>
);
});
BaseFormLabel.displayName = 'FormLabel';
const BaseFormControl = forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formDescriptionId, formItemId, formMessageId } =
useFormField();
return (
<Slot
aria-describedby={
error ? `${formDescriptionId} ${formMessageId}` : `${formDescriptionId}`
}
aria-invalid={Boolean(error)}
id={formItemId}
ref={ref}
{...props}
/>
);
});
BaseFormControl.displayName = 'FormControl';
const BaseFormDescription = forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>((props, ref) => {
const { formDescriptionId } = useFormField();
return (
<p
id={formDescriptionId}
ref={ref}
{...props}
/>
);
});
BaseFormDescription.displayName = 'FormDescription';
const BaseFormMessage = forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ children, ...props }, ref) => {
const { error, formMessageId } = useFormField();
const body = error ? String(error?.message) : children;
if (!body) {
return null;
}
return (
<p
id={formMessageId}
ref={ref}
{...props}
>
{body}
</p>
);
});
BaseFormMessage.displayName = 'FormMessage';
export const FormField = BaseFormField;
export const FormLabel = styled(BaseFormLabel, formLabel);
export const FormItem = styled(BaseFormItem, formItem);
export const FormControl = styled(BaseFormControl, formControl);
export const FormDescription = styled(BaseFormDescription, formDescription);
export const FormMessage = styled(BaseFormMessage, formMessage);
export { Form } from '@remix-run/react';
Hey there :)
I have a form that could be submitted by two buttons, one button saves the data in server, and the other one saves and also changes the status. I send an intent inside submitData to determine which action to run, the save one has a save
intent, while the other has saveAndMoveToPending
. I wanted to know if its possible to pass the intent to the handleSubmit function based on the button I am clicking and not in the form config
Hi,
I have a single route that is a simple instance update form. The form submits to itself and the action function then saves the changes to an API. It then returns the latest copy of the instance.
I'm using instance.updated from the API as a key into the form as this changes every time the instance is updated in the backend.
What is the best way to tell remix form that is should reload the default values so that the isDirty field goes back to false and hence the buttons go back to being disabled?
export default function InstanceView() {
let instance: any = useLoaderData();
const {
handleSubmit,
formState: { errors },
register,
control,
reset,
} = useRemixForm({
mode: "onSubmit",
resolver,
defaultValues: {
...instance,
},
});
const { isDirty} = useFormState({control});
console.log({updated: instance.updated, isDirty});
return (
<div className="mx-4 my-4">
<Form onSubmit={handleSubmit} key={instance.updated}>
<CommonFormField
name={"name"}
label={"Name"}
description={"Name for the entry"}
register={register}
errors={errors}
/>
<CommonFormField
name={"description"}
label={"Description"}
description={"User-defined descriptive text"}
inputType={CommonFormInputType.Textarea}
register={register}
errors={errors}
/>
<div className={"flex"}>
<Button className="bg-red-500 w-[150px]" disabled={!isDirty} onClick={() => reset()}>
Cancel
</Button>
<Button className="ml-8 bg-green-500 w-[150px]" disabled={!isDirty}>
Save
</Button>
</div>
</Form>
</div>
)
}
Console output:
index.tsx:133 {updated: '2023-12-09T06:49:35.997Z', isDirty: true}
index.tsx:133 {updated: '2023-12-09T06:49:35.997Z', isDirty: true}
index.tsx:133 {updated: '2023-12-09T06:49:35.997Z', isDirty: true}
index.tsx:133 {updated: '2023-12-09T06:53:31.449Z', isDirty: true}
index.tsx:133 {updated: '2023-12-09T06:53:31.449Z', isDirty: true}
Hi there,
I made a form with a submit button which his handled with the default configurations.
Now, I want to add a delete button that will simply call a delete function on the server but I can't manage to do that.
Any ideas?
formData can be of type string or blob. The check here only checks for Files so you don't allow for all Blob types. This results in Blobs being validated as objects and going through the Object path, which gets JSON.stringify() and renders them as empty objects on the server.
} else if (value instanceof File) {
formData.append(key, value);
}
add these checks to fix: value instanceof Blob || toString.call(value) === "[object Blob]";
instead of value instanceof File
the proceeding change will check for Files & Blobs.
i'm trying to make a form with file upload but i can't make it work using remix hook form, i am getting an error "Could not parse content as FormData".
<Form onSubmit={handleSubmit} method="POST" encType="multipart/form-data">
<div>
<Label htmlFor="title">
Title:
<Input type="text" {...register("title")} />
{errors.title && errors.title.message && (
<ErrorMessage message={errors.title.message} />
)}
</Label>
</div>
<div>
<Label htmlFor="content">
Content:
<Input type="text" {...register("content")} />
{errors.content && errors.content.message && (
<ErrorMessage message={errors.content.message} />
)}
</Label>
</div>
<div>
<Label htmlFor="file-upload">
Choose file:
<Input
type="file"
id="file-upload"
accept="image/*"
{...register("image")}
/>
{errors.image && errors.image.message && (
<ErrorMessage message={errors.image.message} />
)}
</Label>
</div>
<Button variant={"default"} className="mt-4 w-full">
Add
</Button>
</Form>
export const action = async ({ request, params }: ActionFunctionArgs) => {
const imageFile = await unstable_parseMultipartFormData(
request,
fileUploadHandler()
);
console.log("imageFile", imageFile);
let { data, errors, receivedValues } = await getValidatedFormData<FormData>(
request,
resolver
);
console.log("receivedValues", receivedValues);
console.log("data", data);
const schema = z.object({
title: z.string().min(2).max(300),
content: z.string().min(1).max(200),
image: z.any().refine((files) => files?.length == 1, "Image is required."),
});
I just stated using the lib, following this example but im having type errors.
The complete error:
Type '{ children: Element[]; handleSubmit: (e?: BaseSyntheticEvent<object, any, any> | undefined) => Promise<void>; register: (name: "name" | "email" | "password", options?: RegisterOptions<...> | undefined) => { ...; }; ... 12 more ...; setFocus: UseFormSetFocus<...>; }' is not assignable to type 'RemixFormProviderProps<{ name: string; email: string; password: string; }>'.
Types of property 'register' are incompatible.
Type '(name: "name" | "email" | "password", options?: RegisterOptions<{ name: string; email: string; password: string; }, "name" | "email" | "password"> | undefined) => { ...; }' is not assignable to type 'UseFormRegister<{ name: string; email: string; password: string; }>'.
Types of parameters 'options' and 'options' are incompatible.
Type 'RegisterOptions<{ name: string; email: string; password: string; }, TFieldName> | undefined' is not assignable to type 'RegisterOptions<{ name: string; email: string; password: string; }, "name" | "email" | "password"> | undefined'.
Type 'Partial<{ required: string | ValidationRule<boolean>; min: ValidationRule<string | number>; max: ValidationRule<string | number>; ... 9 more ...; deps: string | string[]; }> & { ...; }' is not assignable to type 'RegisterOptions<{ name: string; email: string; password: string; }, "name" | "email" | "password"> | undefined'.
Type 'Partial<{ required: string | import("/project/sandbox/node_modules/react-hook-form/dist/types/validator").ValidationRule<boolean>; min: import("/project/sandbox/node_modules/react-hook-form/dist/types/validator").ValidationRule<string | number>; ... 10 more ...; deps: string | string[]; }> & { ...; }' is not assignable to type 'Partial<{ required: string | import("/project/sandbox/node_modules/react-hook-form/dist/types/validator").ValidationRule<boolean>; min: import("/project/sandbox/node_modules/react-hook-form/dist/types/validator").ValidationRule<string | number>; ... 10 more ...; deps: string | string[]; }> & { ...; }'. Two different types with this name exist, but they are unrelated.
Type 'Partial<{ required: string | ValidationRule<boolean>; min: ValidationRule<string | number>; max: ValidationRule<string | number>; ... 9 more ...; deps: string | string[]; }> & { ...; }' is not assignable to type 'Partial<{ required: string | ValidationRule<boolean>; min: ValidationRule<string | number>; max: ValidationRule<string | number>; ... 9 more ...; deps: string | string[]; }>'.
Types of property 'validate' are incompatible.
Type 'Validate<TFieldName extends `${infer K}.${infer R}` ? K extends "name" | "email" | "password" ? R extends Path<{ name: string; email: string; password: string; }[K]> ? PathValue<...> : never : K extends `${number}` ? never : never : TFieldName extends "name" | ... 1 more ... | "password" ? { ...; }[TFieldName] : TFi...' is not assignable to type 'Validate<string, { name: string; email: string; password: string; }> | Record<string, Validate<string, { name: string; email: string; password: string; }>> | undefined'.
Type 'Validate<TFieldName extends `${infer K}.${infer R}` ? K extends "name" | "email" | "password" ? R extends Path<{ name: string; email: string; password: string; }[K]> ? PathValue<...> : never : K extends `${number}` ? never : never : TFieldName extends "name" | ... 1 more ... | "password" ? { ...; }[TFieldName] : TFi...' is not assignable to type 'Validate<string, { name: string; email: string; password: string; }> | Record<string, Validate<string, { name: string; email: string; password: string; }>> | undefined'.
Type 'Validate<TFieldName extends `${infer K}.${infer R}` ? K extends "name" | "email" | "password" ? R extends Path<{ name: string; email: string; password: string; }[K]> ? PathValue<...> : never : K extends `${number}` ? never : never : TFieldName extends "name" | ... 1 more ... | "password" ? { ...; }[TFieldName] : TFi...' is not assignable to type 'Validate<string, { name: string; email: string; password: string; }>'.
Type 'string' is not assignable to type 'TFieldName extends `${infer K}.${infer R}` ? K extends "name" | "email" | "password" ? R extends Path<{ name: string; email: string; password: string; }[K]> ? PathValue<...> : never : K extends `${number}` ? never : never : TFieldName extends "name" | ... 1 more ... | "password" ? { ...; }[TFieldName] : TFieldName e...'
Here is a codesandbox with the issue
Hey
I noticed that formState.isSubmitSuccessful
always remains false
Likely #22 but very different..
isSubmitSuccessful
is always false
even after a successful submission
Upon submitting the form and it completing successfully isSubmitSuccessful
remains true
until a subsequent reset
is called (like react-hook-form) (https://www.react-hook-form.com/api/useformstate/ and https://www.react-hook-form.com/api/useform/reset/)
I'm unsure how this would work given the action response is handled by the developer rather than this library. But thought i'd post the issue and see if you had any ideas!
Given this schema
export const FooSchema = z.object({
title: z.string()
number: z.string(),
jsonArray: z.string()
})
which will pass validation on the client side
<input name="title" value="title" />
<input name="number" value="0" />
<input name="jsonArray" value="[]" />
If submit by form.submit() event or submit from Remix.useSubmit()
which maybe equal with submit without JS ?
It will fails when using getValidatedFormData because the types of number and jsonArray are not strings. This happens when the following line is executed:
getValidatedFormData(request, FooSchema)
This issue arises because the parseFormData function auto-parses all values due to the use of preserveStringified = false.
const data = isGet(request) ? getFormDataFromSearchParams(request) : await parseFormData(request);
var parseFormData = async (request, preserveStringified = false) => {
const formData = request instanceof Request ? await request.formData() : request;
return generateFormData(formData, preserveStringified);
};
It should pass. Perhaps we can pass preserveStringified as the third parameter of getValidatedFormData?
For now, I must use dispatchEvent(new Event("submit", { cancelable: true, bubbles: true }))
as a work around
Great job with this awesome library! Unfortunately, I haven't been able to get it to work with a FileInput. I'm using Controller from react-hook-form and useRemixForm to handle the rest. The problem I'm facing is that whenever I submit the form, the value of the "avatar" field is always an empty object. Interestingly, on the client side, I've set up a watcher for the "avatar" field, and it is not empty. Do you have any ideas on what might be causing this issue?
Relevant code:
<Controller
control={control}
name="avatar"
render={({ field: { onChange } }) => (
<FileInput
w="100%"
size="md"
icon={<IconPhotoUp />}
label="Your Photo"
placeholder="Upload your profile photo"
onChange={onChange}
accept="image/png,image/jpeg,image/jpg,image/webp"
/>
)}
/>
This is my squema
const schema = zod.object({
avatar: zod.any() as zod.ZodType<File>,
firstName: zod.string().optional(),
lastName: zod.string().optional(),
email: zod.string().email({ message: "Invalid email address" }),
role: zod.string().optional(),
country: zod.string().optional(),
timezone: zod.string().optional(),
username: zod.string().optional()
});
type FormData = zod.infer<typeof schema>;
const resolver = zodResolver(schema);
//on action
const { errors: formErrors, data: formData, receivedValues } = await getValidatedFormData<FormData>(
request,
resolver
);
I'm using multiple forms on an account settings page. Eg one for updating user details, and another form for changing the password.
So I'm defining 2 useRemixForm
variables for each form. This all works fine. However, I'm using the form.formState.isSubmitting
to show a loader. I noticed that both of my forms loading state is being triggered even though I'm only submitting one of them.
I've encountered a problem with remix-hook-form where form submissions are not behaving as expected or as I believe is documented.
My apologies in advance if this issue stems from a misunderstanding on my part. However, after several attempts and reviews of my code, I'm inclined to believe there might be an underlying issue with how remix-hook-form handles form submissions, specifically when trying to use the method="post" and action attributes without explicitly using a useFetcher.
method="post"
and an action pointing to a route, I expected the form submission to be directed to the specified route without causing a full page reload.action
prop. This behavior suggests that the form props are not being read and submission is not using the underlying fetcher as the docs state.useFetcher
and specifying a submitConfig
in the form setup. This approach seems counterintuitive if the library is supposed to handle submissions more seamlessly by default.I've made a repository that demonstrates the problem. You can find it here - https://stackblitz.com/edit/remix-run-remix-xr6vlj?file=app%2Froutes%2F_index.tsx
Key Points:
SubscribeFormThatDoesNotWork
) doesn't use useFetcher and fails to submit properly, causing a page reload. The other (SubscribeFormThatDoesWork
) uses useFetcher and works fine, submitting without reloading the page.form.formState.isSubmitSuccessful
value that comes back from remix-hook-form is completely wrong. I've setup the API endpoint to respond with a 400, and yet it doesn't reflect that at all. Also the value is set as soon as the request initiates, it doesn't wait for successAgain, I'm very sorry if I'm being dumb and missing something obvious, but I can't for the life of me figure out what is going wrong.
Looking forward to finding out more ๐
Here is another, again just copied from the README sample in a TypeScript file.
{errors.name && <p>{errors.name.message}</p>}
gives two errors:
Type 'string | FieldError | Merge<FieldError, FieldErrorsImpl<any>> | undefined' is not assignable to type 'ReactNode'. Type 'FieldError' is not assignable to type 'ReactNode'.
ะขype 'FieldError' is missing the following properties from type 'ReactPortal': key, children, props
{errors.email && <p>{errors.email.message}</p>}
gives error:
Type 'string | FieldError | Merge<FieldError, FieldErrorsImpl<any>> | undefined' is not assignable to type 'ReactNode'.
When moving from base react-hook-form to remix-hook-form the browser console (Firefox) started showing this error:
Warning: DetailsForm contains an input of type radio with both value and defaultValue props. Input elements must be either controlled or uncontrolled (specify either the value prop, or the defaultValue prop, but not both). Decide between using a controlled or uncontrolled input element and remove one of these props. More info: https://reactjs.org/link/controlled-components
This is happening with radio inputs defined with the register function from userRemixForm:
<label className="space-x-1">
<input
type="radio"
{...register('accountType')}
value="personal"
/>
<span>Personal</span>
</label>
<label className="space-x-1">
<input
type="radio"
{...register('accountType')}
value="corporate"
/>
<span>Corporate</span>
</label>
I solved this by restructuring register
to remove the defaultValue key, but that's not particularly pretty.
Thanks for the great library!
Hi,
Thanks for building this package!
I'm trying to migrate my app from remix-validated-form to this one but I've run into some issue regarding TS types.
Loos like useRemixForm
is incorrectly returning form types.
Here's the type returned from react-hook-form
:
...and here's remix-hook-form
:
You can see the values are incorrectly typed as undefined. According to the zod schema, those fields should never be undefined just like in the first example.
const schema = z.object({
name: z.string().trim().min(3).max(32),
slug: z.string().trim().min(3).max(32),
})
I'm trying to use RemixFormProvider to wrap my form, and am receiving a type error when attempting to pass the form methods to the provider. Here's a minimal reproduction, which is almost identical to the guidance in the docs:
import { zodResolver } from '@hookform/resolvers/zod'
import { RemixFormProvider, useRemixForm } from 'remix-hook-form'
import { z } from 'zod'
const formSchema = z.object({
username: z.string().min(2).max(50),
})
const resolver = zodResolver(formSchema)
export const TestForm = () => {
const methods = useRemixForm({
resolver,
defaultValues: {
username: '',
},
})
return (
<RemixFormProvider {...methods}>
<form onSubmit={methods.handleSubmit}>
<input {...methods.register('username')} />
</form>
</RemixFormProvider>
)
}
And here's the type error I'm getting on the Provider:
Type '{ children: Element; handleSubmit: (e?: BaseSyntheticEvent<object, any, any> | undefined) => Promise<void>; reset: (values?: { username: string; } | { ...; } | undefined) => void; ... 12 more ...; setFocus: UseFormSetFocus<...>; }' is not assignable to type 'RemixFormProviderProps<{ username: string; }>'.
Types of property 'formState' are incompatible.
Property 'disabled' is missing in type '{ dirtyFields: Partial<Readonly<{ username?: boolean | undefined; }>>; isDirty: boolean; isSubmitSuccessful: boolean; isSubmitted: boolean; isSubmitting: boolean; ... 5 more ...; errors: Partial<...>; }' but required in type 'FormState<{ username: string; }>'.ts(2322)
form.d.ts(89, 5): 'disabled' is declared here.
This seems similar to this recent issue (#42 ), but it's a slightly different error. Any help would be appreciated. Thank you!
Hello, thanks for the library!
Got a small issue
When i'm returning a custom error from the action as following:
export async function action({request, context, params}: ActionArgs) {
const {
data,
errors,
receivedValues ,
} = await getValidatedFormData(request, zodResolver(createSpaceInput))
try {
const result = context.api.onboarding.setUpSpace.mutate({
name: data.name
})
return json({ space: result })
} catch(e) {
return json({
errors: {
root: {
message: e.message
}
}
})
}
}
The error can be displayed just fine.
However, when i try to clear the error for example onChange
it simply doesn't clear.
const form = useRemixForm({
mode: "onSubmit",
defaultValues: {
name: "",
},
resolver: zodResolver(formSchema),
});
<RemixForm onSubmit={form.handleSubmit} method={"POST"} onChange={() => {
console.log('form on change', form.formState.errors)
form.clearErrors('root')
}}>
I have also noticed that when I return an error which has the same name as one of the fields:
export async function action({request, context, params}: ActionArgs) {
... // rest of the function
return json({
errors: {
name: {
message: 'some backend error coming from external api'
}
}
})
}
It behaves the same way, as in the message is shown just fine, but it can't be cleared up
Any tips?
If I use the register command in hidden fields the value is never updated, so when the form is submitted, all values are empty?
As of 7.51.0
react-hook-form has a property called validatingFields
in the formState
object.
Since remix-hook-form does not export this, it leads to type errors when upgrading react-hook-form's latest version
error TS2322: Type '{ children: Element; handleSubmit: (e?: BaseSyntheticEvent<object, any, any> | undefined) => Promise<void>; reset: (values?: { message: string; name: string; email: string; phoneNumber: string; } | { ...; } | undefined, options?: Partial<...> | undefined) => void; ... 12 more ...; setFocus: UseFormSetFocus<...>; }' is not assignable to type 'RemixFormProviderProps<{ message: string; name: string; email: string; phoneNumber: string; }>'.
Types of property 'formState' are incompatible.
Property 'validatingFields' is missing in type '{ disabled: boolean; dirtyFields: Partial<Readonly<{ message?: boolean | undefined; name?: boolean | undefined; email?: boolean | undefined; phoneNumber?: boolean | undefined; }>>; ... 9 more ...; errors: FieldErrors<...>; }' but required in type 'FormState<{ message: string; name: string; email: string; phoneNumber: string; }>'.
82 <FormProvider {...form}>
~~~~~~~~~~~~
../../node_modules/.pnpm/[email protected][email protected]/node_modules/react-hook-form/dist/types/form.d.ts:103:5
103 validatingFields: Partial<Readonly<FieldNamesMarkedBoolean<TFieldValues>>>;
~~~~~~~~~~~~~~~~
'validatingFields' is declared here.
I am not sure what's the problem here, but I am following the README.md.
import { z } from 'zod';
const FormDataZodSchema = z.object({
action: z.string(),
outline: z.string().min(2, {
message: 'Outline must be at least 2 characters.',
}),
});
type FormData = z.infer<typeof FormDataZodSchema>;
const resolver = zodResolver(FormDataZodSchema);
// ...
const form = useRemixForm<FormData>({
defaultValues: {
outline: articleProject.articleOutline ?? '',
},
mode: 'onSubmit',
resolver,
});
useEffect(() => {
if (generateOutlineFetcher.data?.outlineMarkdown) {
form.setValue('outline', generateOutlineFetcher.data.outlineMarkdown);
}
}, [form, generateOutlineFetcher.data?.outlineMarkdown]);
const actionIsPending = isGenerating;
return (
<RemixFormProvider {...form}>
<Form
className={css({
display: 'flex',
flexDir: 'column',
gap: '8',
})}
onSubmit={form.handleSubmit}
>
Hello,
I have a simple input that is not part of the validation form
<input type="hidden" name="redirectTo" value={redirectTo} />
I want to get this input value in the action function along with getValidatedFormData
method, but using await request.formData()
together with await getValidatedFormData<FormData>(request, resolver)
throw backend error
TypeError: body used already for: http://localhost:3000/login
You can replicate this issue by adding a simple input field into the form and try to get its value on submit the form in the action method using formData
method.
I'm testing Remix hook form for file upload, but I am having an issue where data is undefined after call validateFormData.
here is my action
export const action = async ({ request, params }: ActionFunctionArgs) => {
const uploadHandler = unstable_composeUploadHandlers(
unstable_createFileUploadHandler({
maxPartSize: 5 * 1024 * 1024,
file: ({ filename }) => filename,
directory: "./public/uploads",
}),
unstable_createMemoryUploadHandler()
);
const formData = await unstable_parseMultipartFormData(
request,
uploadHandler
);
let { errors, data } = await validateFormData<FormData>(formData, resolver);
console.log("data", data) --> data is undefined here
}
here is my render function
export default function () {
const {
...
} = useRemixForm({
mode: "all",
resolver,
defaultValues: {
title: "",
content: "",
image: undefined,
},
submitConfig: {
encType: "multipart/form-data",
method: "POST",
},
});
return (
<Form onSubmit={handleSubmit} method="POST">
<div>
<Input type="text" {...register("title")} />
</div>
<div>
<Input type="text" {...register("content")} />
</Label>
</div>
<div>
<Input
type="file"
id="file-upload"
accept="image/*"
{...register("image")}
/>
</div>
</Form>
);
}
Hello!
I am currently exploring the remix-hook-form
library and observed that when utilizing getValidatedFormData
and validateFormData
, the form schema needs to be explicitly included in the generic.
Considering that both functions require a resolver, it seems feasible to allow automatic inference of the T
type from the resolver.
I could open a PR to introduce this enhancement. But before that, I would appreciate some clarification if there is a specific reason for not implementing this feature that I might be unaware of.
npx create-remix@latest remix
cd remix
npm i remix-hook-form
remix-hook-form installs correctly
npm ERR! code ERESOLVE
npm ERR! ERESOLVE unable to resolve dependency tree
npm ERR!
npm ERR! While resolving: remix@undefined
npm ERR! Found: @remix-run/[email protected]
npm ERR! node_modules/@remix-run/react
npm ERR! @remix-run/react@"^2.0.0" from the root project
npm ERR!
npm ERR! Could not resolve dependency:
npm ERR! peer @remix-run/react@"^1.15.0" from [email protected]
npm ERR! node_modules/remix-hook-form
npm ERR! remix-hook-form@"*" from the root project
npm ERR!
npm ERR! Fix the upstream dependency conflict, or retry
npm ERR! this command with --force or --legacy-peer-deps
npm ERR! to accept an incorrect (and potentially broken) dependency resolution.
React Hook Form's reset()
method (returned by useForm()
) allows options to be submitted to a second argument. This library omits that argument.
I came across this due to another issue: After updating values in a form and submitting them, the form does not use the new values when calculating isDirty
. I found that reset()
is the documented method for React Hook Form. I saw that Remix Hook Form performs a similar update (if the action returns {defaultValues: {}}
, but only in the case of an error return. Perhaps Remix Hook Form could perform the reset or update the values automatically on success? Or maybe I've missed something about this library?
I plan to write a PR for the change to reset()
since it's blocking my current work and seems trivial. Please let me know if this is something you would accept. Any input on the larger issue is appreciated as well.
Thanks for your work on this repo!
Sorry to open this as an issue, wanted to ask this in a discussion but couldn't find the tab.
I'm trying to use react-router
and react-hook-form
together and I was looking for how to do it and I ended up here.
Since Remix is built on react-router
is it possible to use it there as well?
const FormDataZodSchema = z.object({
action: z.string(),
outline: z.string().min(2, {
message: 'Outline must be at least 2 characters.',
}),
});
type FormData = z.infer<typeof FormDataZodSchema>;
const resolver = zodResolver(FormDataZodSchema);
export const action: ActionFunction = async ({ params, request }) => {
const articleId = Number(params.id);
if (!articleId) {
throw new Error('Article ID is required.');
}
const formData = await request.formData();
if (formData.get('action') === 'generate') {
return json(await replaceOutline(articleId));
}
if (formData.get('action') === 'accept') {
console.log('formData', Object.fromEntries(formData));
const { data, errors } = await validateFormData<FormData>(
formData,
resolver,
);
console.log('errors', errors);
if (errors) {
return json(errors);
}
return await acceptOutline(articleId, data.outline);
}
throw new Error('Invalid action.');
};
formData {
action: 'accept',
outline: '* Introduction\n' +
' * Overview of Threats Affecting Polar Bears\n' +
' * Significance of the Circumpolar Action Plan\n' +
' * The Compound Effects of these Threats\n' +
'* Climate Change As A Major Threat\n' +
" * Effects of Climate Change on Polar Bear's Habitat\n" +
' * Relation of Climate Change with Other Threats\n' +
'* Human-Caused Mortality\n' +
' * Causes of Human-Induced Polar Bear Deaths\n' +
' * Connection with Other Threats and Climate Change\n' +
'* Industrial Threats: Mineral and Energy Resource Exploration and Development\n' +
" * Impacts of Industrial Activities On Polar Bears' Habitats\n" +
' * Combination with Climate Change and Other Threats\n' +
'* Contaminants and Pollution\n' +
' * effect of Pollution on Polar Bears\n' +
' * Connection with Climate Change and Other Threats\n' +
'* Shipping and Tourism\n' +
' * Impacts of Shipping and Tourism on Polar Bear Habitat\n' +
' * Connection with Climate Change and Other Threats\n' +
'* Diseases and Parasites\n' +
' * Effects of Diseases and Parasites on Polar Bears\n' +
' * connection with Climate Change and Other Threats\n' +
'* Conclusion\n' +
' * Summary of Challenges and their Impacts\n' +
' * Importance of Protecting Polar Bears for Biodiversity\n'
}
errors {
action: { message: 'Required', type: 'invalid_type', ref: undefined },
outline: { message: 'Required', type: 'invalid_type', ref: undefined }
}
I believe I am providing the expected inputs, but validator is still producing an error.
Because of this line in createFormData(data: T, stringifyAll = true)
in ./utilities/index.ts
if (stringifyAll) {
formData.append(key, JSON.stringify(value)); // <---- This double quotes everything
} else {
formData.append(
key,
typeof value === "string" ? value : JSON.stringify(value),
);
}
So when accessing FormData in backend with formData.get('email')
the result is "/"[email protected]/""
with double quoted string. What can be the workaround for this?
Currently I am setting stringifyAllValues: false
and coercing all to strings in my zod schema.
Hi there, seems like there's an issue with numbers validation on the backend side, coz if I have z.number() in the schema it will correctly pass validation on the frontend but the action will return an error:
{
message: "Expected number, received string"
type: "invalid_type"
}
I believe that's a bug as in readme you saying it should be parsed as a number
But based on what I see in the tryParseJSON
it will never parse it as a number, also in tests you expecting strings ๐คทโโ๏ธ
Hey, I'm trying to use remix-hook-form with a <fetcher.Form>
returned from useFetcher()
hook. I supplied onSubmit
props of the <fetcher.Form>
with handleSubmit()
function returned from useRemixForm()
. However, it seems that the fetcher.state
does not change when I submit the form. And useRemixForm()
does not seem to have any related options. Do you have plans on supporting it?
Hi, we're using this in a fairly simple way, I think. Our action looks something like this:
export async function DoThingAction({ request }: ActionArgs) {
const { errors, data } = await getValidatedFormData<DoThingForm>(
request,
zodResolver(DoThingForm)
);
if (errors) {
return badRequest(
result: 'error',
error: errors.toString(),
});
}
return json(await doThing(data));
}
The return type of doThing
looks something like this:
export type doThingResponse = { result: 'success', someValue: string } | { result: 'error', error: string };
I don't think we're doing anything particularly unusual here but it seems that the errors
property of formState
in the component is always being populated with the returned value of the action, so it always looks like there are errors.
Is there something unusual about how we've set up our action? (We don't want to have a separate success state component because it's feasible that folks will want to submit this form multiple times with more than one set of values)
Hey I love this library! It works super well. I do, however have one conundrum I've been struggling with for a while. I have a form that has a file upload input as well as a few other text inputs, because I want to submit all of this data to the server at once. I can't seem to figure out a way to do it. Any advice? I've tried everything from sending the file as submit data in useRemixForm to messing with stringifyAllValues, and many other things. No luck.
I'm using react dropzone for my file upload component. I have gotten the file upload to work without the use of remix-hook-form, but I really want to be able to validate my other form elements.
here's what my form looks like currently, though I've played with a million variations:
<Form method="post" onSubmit={handleSubmit} encType="multipart/form-data">
<Input
placeholder="Event Title"
errorMsg={errors.title?.message}
{...register("title")}
/>
<Textarea placeholder="Event Description"
errorMsg={errors.description?.message}
{...register("description")}
/>
<ImageUpload previewUrl={imagePreview}
minImageWidth={800}
minImageHeight={500}
onDrop={file => {
setValue('upload', file)
setImageFile(file)
setImagePreview(URL.createObjectURL(file))
}} onError={err => console.log(err)} onRemove={() => {
if (imagePreview != null) {
URL.revokeObjectURL(imagePreview);
setImageFile(null);
setImagePreview(null)
}
}
}/>
{errors.upload?.message && (<p className="text-red-500">{errors.upload?.message}</p>)}
<AddressSearchInput onAddressSelection={(item) => {
const loc = item.selectedItem
if (loc != null) {
setValue('location', {
...loc,
state: loc.stateCode,
lat: loc.latitude,
long: loc.latitude,
zip: loc.postalCode,
})
}
setLocation(item.selectedItem)
}} errorMsg={errors.location?.message}
addressData={addressData}
/>
<DateTimePicker
value={dateTime}
onChange={function (value: { date: Date; hasTime: boolean; }): void {
setValue('eventDateTime', value.date)
setDateTime(value)
}}
className="mt-3 bg-pingray-100 font-light focus:ring-2 focus:outline-none focus:ring-bg-pinblue-500"
/>
{errors.eventDateTime?.message != null && (
<p className="text-red-500">{errors.eventDateTime?.message}</p>
)}
<button
type="submit"
disabled={isSubmitting}
className="bg-pinblue-500 shadow hover:shadow-md w-full text-gray-100 p-2 rounded mt-8 font-['bree'] focus:ring-2 focus:outline-none focus:ring-bg-pinblue-500"
>
{isSubmitting ? 'Submitting...' : 'Create Event'}
</button>
</Form>
note I'm registering anything using setValue in a useEffect.
on the action side I've attempted everything from removing my upload value from zod all together to using remix's Multipart form parser and many other variations.
Should I maybe use multiple actions, and somehow split up my file upload from the other form data processing? And if so... how can I do that in the same component?
Hi! ๐
Firstly, thanks for your work on this project! ๐
Today I used patch-package to patch [email protected]
for the project I'm working on.
Here is the diff that solved my problem:
diff --git a/node_modules/remix-hook-form/dist/index.js b/node_modules/remix-hook-form/dist/index.js
index 6fd94e4..cd0637a 100644
--- a/node_modules/remix-hook-form/dist/index.js
+++ b/node_modules/remix-hook-form/dist/index.js
@@ -130,8 +130,9 @@ var useRemixForm = ({
const onSubmit = useMemo(
() => (data2) => {
setIsSubmittedSuccessfully(true);
- const formData = createFormData(
- { ...data2, ...submitData },
+ const submitPayload = { ...data2, ...submitData };
+ const formData = submitConfig?.encType === 'application/json' ? submitPayload : createFormData(
+ submitPayload,
stringifyAllValues
);
submit(formData, {
I noticed that when I set the encType
value to application/json
in the submitConfig
, the data being sent still remained as FormData
.
This issue body was partially generated by patch-package.
hi looks like the signature for useRemixForm has changed compared to whats in the README.
It no longer requires mode: "onSubmit"
.
I have a route that can submit either "generate" or "accept" action and their fields are different.
getValidatedFormData is not returning the file data.
{
firstName: 'Jack',
lastName: 'Sparrow',
screenName: 'sparrow1',
profileImage: { '0': {} } // No blob/file data
}
Issue is here: https://github.com/Centerworx/remix-hook-form/blob/d082e1d0e7929451505db9ac0519952860d56bc4/src/utilities/index.ts#L49
FormData Blob/FIles are not handled. JSON cannot hold a file.
formData.set(pathString, blob) or formData.set(pathString, blob, filenameString)
you can use react-hook-form set function to do this.
import { set } from "react-hook-form";
set(
object: FieldValues, // in your case currentObject
path: string, // formData key
value?: unknown, // formData blob
)
With this set function you can eliminate all the string parsing and conversion you are doing in "generateFormData" it would just become the below:
/**
* Generates an output object from the given form data, where the keys in the output object retain
* the structure of the keys in the form data. Keys containing integer indexes are treated as arrays.
*
* @param {FormData} formData - The form data to generate an output object from.
* @param {boolean} [preserveStringified=false] - Whether to preserve stringified values or try to convert them
* @returns {Object} The output object generated from the form data.
*/
export const generateFormData = (
formData: FormData,
preserveStringified = false,
) => {
// Initialize an empty output object.
const outputObject: Record<any, any> = {};
// Iterate through each key-value pair in the form data.
for (const [key, value] of formData.entries()) {
if (value instanceof Blob || toString.call(value) === "[object Blob]") {
set(currentObject, key, value)
continue;
}
// Try to convert data to the original type, otherwise return the original value
const data = preserveStringified ? value : tryParseJSON(value.toString());
set(currentObject, key, data)
}
// Return the output object.
return outputObject;
};
The only issue with using react-hook-form set function is it will not use empty [] in keys urlSearchParams or dotKey syntax. You can use my "cleanArrayStringKeys" function to allow for empty [] or remove those test to conform to react-hook-form standards, you could even throw an error if there is an empty [].
/**
* cleanArrayStringUrl - This is a Fix for react-hook-form url empty [] brackets set conversion,
* react-hook-form will not load array values that use the empty [] keys.
*
* This utility will add number keys to empty [].
*
* Note: empty url arrays and no js forms don't seam to be very popular either.
*
* @param {string} urlString
* @returns {string}
*/
export const cleanArrayStringKeys = (urlString: URLSearchParams) => {
const cleanedFormData = new URLSearchParams();
const counter = {} as Record<string, number>;
for (const [path, value] of urlString.entries()) {
const keys = path.split(/([\D]$\[\])/g);
// early return if single key
if (keys.length === 1 && !/\[\]/g.test(keys[0])) {
cleanedFormData.set(keys.join(""), value);
continue;
}
const cleanedKey = [] as string[];
// adds indexes to empty arrays to match react-hook-form
keys.forEach((key) => {
key = key.replace(/\.\[/g, "[");
counter[key] = (counter[key] === undefined ? -1 : counter[key]) + 1;
cleanedKey.push(key.replace(/\[\]/g, () => `[${counter[key]}]`));
// merge array keys back into path
const cleanedPath = cleanedKey.join("");
cleanedFormData.set(cleanedPath, value);
});
}
If you don't want to simplify your code, you can just add this to "tryParseJSON", this should do the trick, but should be tested:
tryParseJSON= (jsonString) => {
if (jsonString === "null") {
return null;
}
if (jsonString === "undefined") {
return void 0;
}
if (jsonString instanceof File || jsonString instanceof Blob) { // new check before JSON.parse
return jsonString;
}
on a side note: value instanceof Blob
-> Some node servers (<16, It can be imported in 16+) may not have access to Blob and so it could break some projects. See ref: https://stackoverflow.com/questions/14653349/node-js-cant-create-blobs#:~:text=41-,Since%20Node.js%2016,-%2C%20Blob%20can
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.