Typesafe input component

In the guide on scoping, we showed how to create a simple, typesafe text input component. However, there are many different types of inputs and those can support different types of values.

This recipe shows how to create a typesafe input component that supports all the different types of inputs.

Features

Our input component will have the following features:

  • Accepts a scope prop that is typesafe for the input type.
  • Automatically displays its error message if there is one.
  • Sets the correct aria-* attributes
  • Can accept all the props normally accepted by the native input element.

Preview

One component for different input types

Recipe

import {
  useField,
  FormScope,
  ValueOfInputType,
} from "@rvf/react";
import {
  ComponentPropsWithRef,
  forwardRef,
  useId,
} from "react";

// For our props, we'll take everything from the native input element except for `type`.
// You can make futher changes here to suite your needs.
type BaseInputProps = Omit<
  ComponentPropsWithRef<"input">,
  "type"
>;

interface MyInputProps<Type extends string>
  extends BaseInputProps {
  label: string;
  type?: Type;
  scope: FormScope<ValueOfInputType<Type>>;
}

// We need to do this in order to get a generic type out of `forwardRef`.
// In React 19, you won't need this anymore.
type InputType = <Type extends string>(
  props: MyInputProps<Type>,
) => React.ReactNode;

const MyInputImpl = forwardRef<
  HTMLInputElement,
  MyInputProps<string>
>(({ label, scope, type, ...rest }, ref) => {
  const field = useField(scope);
  const inputId = useId();
  const errorId = useId();

  return (
    <div className="myInputStyles">
      <label htmlFor={inputId}>{label}</label>
      <input
        {...field.getInputProps({
          type,
          id: inputId,
          ref,
          "aria-describedby": errorId,
          "aria-invalid": !!field.error(),
          ...rest,
        })}
      />
      {field.error() && <p id={errorId}>{field.error()}</p>}
    </div>
  );
});
MyInputImpl.displayName = "MyInput";

export const MyInput = MyInputImpl as InputType;