Controlled react-select input not being captured during submition
i use remix-hook-fom in my project. I struggled trying to use it with React Select. here is my controlled React Select code:
import { useId } from 'react'
import { Controller, FieldError, get } from 'react-hook-form'
import ReactSelect from 'react-select'
import { useRemixFormContext } from 'remix-hook-form'
import { useHydrated } from 'remix-utils/use-hydrated'
import { twMerge } from 'tailwind-merge'
import { CustomOption } from './custom-option'
import { SelectProps } from './type'
export const Select = <T extends Record<string, unknown>>(
props: SelectProps<T>
) => {
const isHydrated = useHydrated()
const {
name,
id,
label,
onChange,
className,
containerClassName,
errorClassName,
disabled,
labelClassName,
required,
isSearchable = false,
size = 'md',
...rest
} = props
const generatedId = useId()
const {
control,
formState: { errors }
} = useRemixFormContext()
const error: FieldError = get(errors, name)
return (
<div
className={twMerge([
'relative flex w-full flex-col gap-1.5',
containerClassName
])}
>
{label && (
<label
htmlFor={id ?? generatedId}
className={twMerge(['text-gray-700', labelClassName])}
>
{label} {required && <span className="text-rose-500">*</span>}
</label>
)}
{isHydrated ? (
<Controller
name={name}
control={control}
render={({ field }) => {
return (
<ReactSelect
instanceId={id ?? generatedId}
components={{ Option: CustomOption }}
onChange={(newValue, actionMeta) => {
if (onChange) {
onChange(newValue, actionMeta)
}
field.onChange(newValue)
}}
value={field.value}
isSearchable={isSearchable}
isDisabled={disabled}
className={className}
{...rest}
/>
)
}}
/>
) : (
<div className="h-8 w-full animate-pulse rounded-md bg-gray-300" />
)}
{error && (
<span
className={twMerge([
errorClassName,
'absolute -bottom-4 text-xs text-rose-500'
])}
>
{error?.message?.toString()}
</span>
)}
</div>
)
}
and here is my implementation:
export const createDepositSchema = z.object({
bank: z.object({
label: z.string(),
value: z.string()
})
})
const fetcher = useFetcher()
const formMethods = useRemixForm<TCreateDeposit>({
mode: 'onSubmit',
resolver: zodResolver(createDepositSchema),
stringifyAllValues: false,
fetcher
})
const {
bank: watchedBank,
} = watch()
console.log(watchedBank) // the value is captured here
<RemixFormProvider {...formMethods}>
<fetcher.Form
method="POST"
action="/actions/deposit"
>
<Select<TCreateDeposit>
required
labelClassName="text-white"
label="Bank Transfer"
name="bank"
options={[
{label: 'BNI', value: '1'},
{label: 'BCA', value: '2'},
{label: 'BRI', value: '3'},
{label: 'Mandiri', value: '4'},
]}
/>
</fetcher.Form>
</RemixFormProvider>
and here is my action:
import { zodResolver } from '@hookform/resolvers/zod'
import { ActionFunctionArgs } from '@remix-run/node'
import { getValidatedFormData } from 'remix-hook-form'
import { createDepositSchema, TCreateDeposit } from '@/schemas/deposit'
export const action = async ({ request }: ActionFunctionArgs) => {
const {
errors,
data: formData,
receivedValues: defaultValues
} = await getValidatedFormData<TCreateDeposit>(
request,
zodResolver(createDepositSchema),
true
)
if (errors) {
console.log(errors)
return Response.json(
{ success: false, errors, defaultValues },
{ status: 400 }
)
}
console.log(formData)
return null
}
when i try to console.log the value in the client, it is there as expected. but once i submit the form, it is not being captured in the action by getValidatedFormData and returns validation error. I try to debug it by adding .optional() to the schema, it turns out that the data is empty. the bank field didn't event sent to the action.
before submitting this issue, i have tried all possible options in. stringifyAllValues as well as in preserveStringified, but nothing happens.
in another remix roject where i use plain useForm with useFormContext from React Hook Form, it works just fine.
To be completely honest here I have never really looked into if Controller from react-hook-form works with this library, but what you can do is just not use it and use setValue directly in the react-select instead and that will definitely work, eg:
{isHydrated ? (
<ReactSelect
instanceId={id ?? generatedId}
components={{ Option: CustomOption }}
onChange={(newValue, actionMeta) => {
if (onChange) {
onChange(newValue, actionMeta)
// here
setValue(name, newValue)
}
field.onChange(newValue)
}}
value={field.value}
isSearchable={isSearchable}
isDisabled={disabled}
className={className}
{...rest}
/>
) : (
<div className="h-8 w-full animate-pulse rounded-md bg-gray-300" />
)}
Hi there, I am having some issues with RHF Controller as well and would like to ask if it is supported and if not, what the advized alternative would be.
The example below uses aria-components and is producing A component changed from uncontrolled to controlled. warnings and is basically making the input unusable.
Code based on https://react-spectrum.adobe.com/react-aria/forms.html#react-hook-form
<Controller
name="language.name"
control={control}
render={({ field, fieldState: { invalid, error } }) => (
<TextField {...field} isInvalid={invalid}>
<TextField.Label>Name</TextField.Label>
<TextField.Input {...register(field.name)} />
<TextField.FieldError>{error?.message} </TextField.FieldError>
</TextField>
)
}
/>
FWIW RHF Controller works as expected, as long as we do not forget to pass defaultValues 🤦♀️