Home > Software design >  Custom React component doesn't show input length when used with react-hook-form
Custom React component doesn't show input length when used with react-hook-form

Time:01-04

Having the following Textarea component, it was built in order to be reusable, it is a basic textarea with has a maxlength props, where it can be specified the maximum input length, it also shows the current input length, in the format current input length/max length.

It works fine as a separate component, the problem is when it must be used with react-hook-form, the current input length isn't updating.

Here is the Textarea component:

import React, { useState, useEffect } from 'react';

import useTextareaController from './use-textarea-controller';

export interface TexareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
  maxLength?: number;
  id: string;
}

export const Textarea = React.forwardRef(
  (
    { id, maxLength = 200, ...props }: TexareaProps,
    ref: React.ForwardedRef<HTMLTextAreaElement>
  ) => {
    const { textareaRefSetter } = useTextareaController(ref);

    const [count, setCount] = useState(0);
    useEffect(() => {
      const refElement = document.getElementById(id) as HTMLTextAreaElement;
      if (refElement) {
        setCount(refElement.value.length);
      }
    }, [id]);

    return (
      <div>
        <div>
          {count}/{maxLength}
        </div>
        <textarea
          id={id}
          ref={textareaRefSetter}
          onChange={(event) => setCount(event.target.value.length)}
          maxLength={maxLength}
          {...props}
        ></textarea>
      </div>
    );
  }
);

export default Textarea;

it used useTextareaController from another hook, here it is the code:

import React, { useRef } from 'react';

/**
 * Utility function which registers and then removes event listeners for specified elements.
 * @param el Reference of element for which to register event
 * @param eventType native event type
 * @param onEventCallback callback to be bound to event
 */

/**
 * Controls the appearance of Button / Icon that is assigned to the reference,
 * using the visibility property.
 *
 * This implementation has been made in the effort of
 * keeping the input uncontrolled whenever it is possible.
 *
 * @See https://react-hook-form.com/api/useform/register/
 * @param forwardedInputRef forwarded reference to be set by this hook
 * @param disabled clear button / icon won't appear if it is false
 * @returns referenceSetter function to assign the inner input element to the forwarded reference
 * and the reference of the clear button / icon
 */
export const useTextareaController = (
  forwardedInputRef: React.ForwardedRef<HTMLTextAreaElement>
) => {
  const innerInputRef = useRef<HTMLTextAreaElement>();

  // Both the inner reference and the forwarded reference should be set
  const textareaRefSetter = (el: HTMLTextAreaElement) => {
    innerInputRef.current = el;
    if (!forwardedInputRef) return;
    if (typeof forwardedInputRef === 'function') {
      forwardedInputRef(el);
    }
    if (typeof forwardedInputRef === 'object') {
      forwardedInputRef.current = el;
    }
  };

  return { textareaRefSetter };
};

export default useTextareaController;

Here is a modal component that has Textarea inside of it and uses react-hook-form for validation:

import { yupResolver } from '@hookform/resolvers/yup';
import { useForm } from 'react-hook-form';
import * as yup from 'yup';

import { Modal, Textarea } from '../shared/ui-components';

const schema = yup.object().shape({
  description: yup.string().required()
});

export interface Task {
  description: string;
}

export interface MyModalProps {
  title: string;
  open: boolean;
  toggle: () => void;
}

export function MyModal({ title, open, toggle }: MyModalProps) {
  const emptyTask = { description: '' };

  const { handleSubmit, reset, register } = useForm({
    resolver: yupResolver(schema)
  });

  const onSubmit = (data: Task) => {
    // send a POST request
    toggle();
    reset(emptyTask);
  };

  return (
    <Modal title={title} open={open} onClose={toggle} onSubmit={handleSubmit(onSubmit)}>
      <div>
        <Textarea id='my-textarea' {...register('description')} />
      </div>
    </Modal>
  );
}

export default MyModal;

Is there a way to make the current input length to work in combination with react-hook-form?

I guess the changes must be done in Textarea component.

CodePudding user response:

react-hook-form provides its own onChange handler which it'll pass as a part of props, which is likely clobbering your custom handler when you spread props into the textarea props.

You should instead extract onChange from props and define your own onChange callback which invokes it if it were passed in, rather than spreading it into your props.

export const Textarea = React.forwardRef(
  (
    { id, maxLength = 200, onChange, ...props }: TexareaProps,
    ref: React.ForwardedRef<HTMLTextAreaElement>
  ) => {
    const { textareaRefSetter } = useTextareaController(ref);

    const [count, setCount] = useState(0);
    useEffect(() => {
      const refElement = document.getElementById(id) as HTMLTextAreaElement;
      if (refElement) {
        setCount(refElement.value.length);
      }
    }, [id]);

    const onChangeHandler = useCallback(
      (event) => {
        setCount(event.target.value.length);
        onChange?.(event);
      },
      [setCount, onChange]
    );

    return (
      <div>
        <div>
          {count}/{maxLength}
        </div>
        <textarea
          id={id}
          ref={textareaRefSetter}
          onChange={onChangeHandler}
          maxLength={maxLength}
          {...props}
        ></textarea>
      </div>
    );
  }
);
  •  Tags:  
  • Related