nextui: [BUG] - react-hook-form

NextUI Version

2.2.9

Describe the bug

`

<Input type=“text” placeholder=“0” classNames={{ input: “text-right”, }} radius=“none” onValueChange={(value: string) => inputChange(value, “amount”)} size=“sm” {…register(“amount”, { required: true, })} /> `

It uses React-hook-form and next ui input, and is initialized when the value is entered with setValue.

Your Example Website or App

No response

Steps to Reproduce the Bug or Issue

“”

Expected behavior

“”

Screenshots or Videos

No response

Operating System Version

masOs

Browser

Chrome

About this issue

  • Original URL
  • State: closed
  • Created 8 months ago
  • Reactions: 16
  • Comments: 34 (3 by maintainers)

Most upvoted comments

I also got it working using the Controller component. For anyone else needing an example, with working default values, values, and error messages, here you go:

import { InputProps } from "@nextui-org/input/dist/input";
import { Input } from "@nextui-org/react";
import React from "react";
import { Control, Controller, FieldValue, FieldValues } from "react-hook-form";

export type FormInputProps = {
  name: string;
  control: Control<any, any>;
} & InputProps;

export const FormInput: React.FC<FormInputProps> = ({ name, ...props }) => {
  return (
    <Controller
      name={name}
      control={props.control}
      render={({ field, fieldState, formState }) => {
        return (
          <Input
            {...props}
            isInvalid={!!formState.errors?.[name]?.message}
            errorMessage={formState.errors?.[name]?.message?.toString()}
            value={field.value}
            onChange={field.onChange}
          />
        );
      }}
    ></Controller>
  );
};

Usage example:

<FormInput
  name={"email"}
  label={"Email"}
  variant={"bordered"}
  control={control}
/>

The ref to the input is not exposed. The forward ref is passed to the top most div element.

Dear maintainers, please either fix the documentation (because it no longer reflects reality): https://nextui.org/docs/components/input#controlled

Note: NextUI Input also supports native events like onChange, useful for form libraries such as Formik and React Hook Form.

or: have it fixed so that ref is passed down again to the input.

Current issues that I’ve discovered so far, when trying to pass {...register("field")}:

  • No default filled values
  • Clear button in input doesn’t work
  • Placeholder text doesn’t change color on error
  • When clicking reset default values appear, but on hover disappear

Reproduction sandbox: https://codesandbox.io/p/devbox/nextui-v2-react-hook-form-forked-rm9dz8

This used to work in @nextui-org/react@2.1.3, as you can see in the original sandbox: https://codesandbox.io/p/devbox/nextui-v2-react-hook-form-xnwxt8 Updating to @nextui-org/react@2.1.4 breaks the integration with react-hook-form.

I couldn’t make this work in my local Next.js 14 app router project, even when downgrading to @nextui-org/react@2.1.3. Reproduction sandbox Next.js 14: https://codesandbox.io/p/devbox/nextjs-nextui-react-hook-form-ck4vdj

Did you try using this way @kamzata https://react-hook-form.com/get-started#IntegratingwithUIlibraries ?

I had the same issue after upgrading to the latest Nextui version. For me works wrapping the Nextui Input inside react-hook-form Controller.

Hey guys, thanks for sharing your issues, it seems the ref is not working properly in the Input component therefore the defaultValues are not working, as a workaround please use the Controller component from react-hook-form here’s an example:

https://stackblitz.com/edit/vitejs-vite-xdeajz?file=src%2FApp.tsx

wrapping the NextUI Input with a Controller fixes the default value problem

Did you try using this way

This does fix it but it sort of defeats the purpose of using react hook form and embracing uncontrolled components.

@christo9090

Here is my UncontrolledTextarea.tsx (tweaked from NextUI textarea.tsx):

import { TextareaHTMLAttributes, useCallback, useMemo, useState } from 'react'

import { TextAreaProps, useInput } from '@nextui-org/react'
import { dataAttr } from '@nextui-org/shared-utils'
import { forwardRef } from '@nextui-org/system'
import { mergeProps } from '@react-aria/utils'
import TextareaAutosize from 'react-textarea-autosize'

type NativeTextareaProps = TextareaHTMLAttributes<HTMLTextAreaElement>
export type TextareaHeightChangeMeta = {
  rowHeight: number
}
type TextareaAutoSizeStyle = Omit<NonNullable<NativeTextareaProps['style']>, 'maxHeight' | 'minHeight'> & {
  height?: number
}

export const UncontrolledTextarea = forwardRef<
  'textarea',
  Omit<TextAreaProps, 'onChange'> & Pick<NativeTextareaProps, 'onChange'>
>(
  (
    {
      style,
      minRows = 3,
      maxRows = 8,
      cacheMeasurements = false,
      disableAutosize = false,
      onChange,
      onHeightChange,
      value,
      ...otherProps
    },
    ref,
  ) => {
    const {
      Component,
      label,
      description,
      startContent,
      endContent,
      hasHelper,
      shouldLabelBeOutside,
      shouldLabelBeInside,
      errorMessage,
      getBaseProps,
      getLabelProps,
      getInputProps,
      getInnerWrapperProps,
      getInputWrapperProps,
      getHelperWrapperProps,
      getDescriptionProps,
      getErrorMessageProps,
    } = useInput<HTMLTextAreaElement>({ ...otherProps, ref, isMultiline: true })

    const [hasMultipleRows, setHasMultipleRows] = useState(minRows > 1)
    const [isLimitReached, setIsLimitReached] = useState(false)
    const labelContent = label ? <label {...getLabelProps()}>{label}</label> : null

    const inputProps: NativeTextareaProps = useMemo(() => {
      const { onChange: _onChange, value: _value, ...otherInputProps } = getInputProps()
      return { onChange, value, ...otherInputProps }
    }, [getInputProps, onChange, value])

    const handleHeightChange = useCallback(
      (height: number, meta: TextareaHeightChangeMeta) => {
        if (minRows === 1) {
          setHasMultipleRows(height >= meta.rowHeight * 2)
        }
        if (maxRows > minRows) {
          const limitReached = height >= maxRows * meta.rowHeight

          setIsLimitReached(limitReached)
        }

        onHeightChange?.(height, meta)
      },
      [maxRows, minRows, onHeightChange],
    )

    const content = useMemo(
      () =>
        disableAutosize ? (
          <textarea {...inputProps} style={mergeProps(inputProps.style, style ?? {})} />
        ) : (
          <TextareaAutosize
            {...inputProps}
            cacheMeasurements={cacheMeasurements}
            data-hide-scroll={dataAttr(!isLimitReached)}
            maxRows={maxRows}
            minRows={minRows}
            style={mergeProps(inputProps.style as TextareaAutoSizeStyle, style ?? {})}
            onHeightChange={handleHeightChange}
          />
        ),
      [cacheMeasurements, disableAutosize, handleHeightChange, inputProps, isLimitReached, maxRows, minRows, style],
    )

    const innerWrapper = useMemo(() => {
      if (startContent || endContent) {
        return (
          <div {...getInnerWrapperProps()}>
            {startContent}
            {content}
            {endContent}
          </div>
        )
      }

      return <div {...getInnerWrapperProps()}>{content}</div>
    }, [startContent, endContent, getInnerWrapperProps, content])

    return (
      <Component {...getBaseProps()}>
        {shouldLabelBeOutside ? labelContent : null}
        <div {...getInputWrapperProps()} data-has-multiple-rows={dataAttr(hasMultipleRows)}>
          {shouldLabelBeInside ? labelContent : null}
          {innerWrapper}
        </div>
        {hasHelper ? (
          <div {...getHelperWrapperProps()}>
            {errorMessage ? (
              <div {...getErrorMessageProps()}>{errorMessage}</div>
            ) : description ? (
              <div {...getDescriptionProps()}>{description}</div>
            ) : null}
          </div>
        ) : null}
      </Component>
    )
  },
)

UncontrolledTextarea.displayName = 'UncontrolledTextarea'

It does what I need it to do, but I didn’t test in in all the edge cases. I have an UncontrolledInput.tsx too if you want.

For me, wrapping the NextUI Input with a Controller fixes the default value problem, however, the errorMessage is not shown! The input is red, the errorMessage is passed, but it is not displayed… I am also using RHF and Input version 2.1.16. Downgrading to Input version 2.1.9 solves the issue for now.

If I replace the NextUI Input Field with a standard HTML input element it works right away.

Same here! I’m using NextUI 2.2.9 and RHF 7.48.2 and it’s like the NextUI’s input field ignores some props like onChange. Furthermore, NextUI’s input field doesn’t show anymore the default values passed on RHF. It worked before upgrading.

I tested on Chrome and Firefox on MacOS and Linux.