Skip to content

Controlled react-select input not being captured during submition #122

@semet

Description

@semet

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions