Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
semet opened this issue Nov 14, 2024 · 1 comment
Open

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

semet opened this issue Nov 14, 2024 · 1 comment

Comments

@semet
Copy link

semet commented Nov 14, 2024

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.

@AlemTuzlak
Copy link
Contributor

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" />
      )}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants