Piplup
@piplup/rhf-adaptersmui-materialHooks

useMuiInputBaseAdapter

Returns adapter props for building custom MUI InputBase fields with React Hook Form, including number handling, transforms and validation rules.

useMuiInputBaseAdapter connects a text-like field to React Hook Form and returns props you spread onto MUI's InputBase. It's what MuiInputBaseElement uses internally.

InputBase has no border, label or helper text, so the element is rarely enough on its own. Use the hook when you draw the field yourself, for example a search bar, an inline table cell or a styled input, and need the error message and value handling in your own markup.

The same input logic powers useMuiInputAdapterProps, useMuiFilledInputAdapter, useMuiOutlinedInputAdapter and useMuiTextFieldAdapter. This page describes it once; those pages cover what differs.

Import

import { useMuiInputBaseAdapter } from "@piplup/rhf-adapters/mui-material";

Usage

This SeatsField stores a number, accepts 1 to 8, and renders its own label and helper text. Submit it empty, or type 0 or 12, to see the messages.

Between 1 and 8 seats per booking
"use client";import { useId } from "react";import Stack from "@mui/material/Stack";import InputBase from "@mui/material/InputBase";import Typography from "@mui/material/Typography";import {  MuiButtonElement,  useMuiInputBaseAdapter,} from "@piplup/rhf-adapters/mui-material";import { type Control, useForm } from "react-hook-form";type FormValues = { seats: number | null };// A borderless number field. The form stores a number (or null when empty),// and the hook adds min/max rules with custom messages.function SeatsField({ control }: { control: Control<FormValues> }) {  const id = useId();  const { ref, helperText, error, ...adapter } = useMuiInputBaseAdapter({    control,    name: "seats",    type: "number",    required: true,    min: 1,    max: 8,    composeHelperText: true,    helperText: "Between 1 and 8 seats per booking",    messages: {      required: "How many seats do you need?",      min: "Book at least one seat",      max: (max) => `Groups larger than ${max} need to call us`,    },    transform: {      // An empty number field is stored as null. Give the input an empty      // string instead so it stays controlled.      input: (value) => value ?? "",    },  });  return (    <Stack spacing={0.5}>      <Typography component="label" htmlFor={id} variant="body2">        Seats      </Typography>      <InputBase        {...adapter}        id={id}        error={error}        inputRef={ref}        slotProps={{ input: { min: 1, max: 8 } }}        sx={{          px: 1.5,          py: 0.5,          borderRadius: 1,          bgcolor: "action.hover",          maxWidth: 200,          outline: error ? "2px solid" : "none",          outlineColor: "error.main",        }}      />      <Typography variant="caption" color={error ? "error" : "text.secondary"}>        {helperText}      </Typography>    </Stack>  );}export default function Page() {  const { control, handleSubmit } = useForm<FormValues>({    defaultValues: { seats: null },  });  return (    <form      noValidate      onSubmit={handleSubmit((values) =>        alert(JSON.stringify(values, null, 2)),      )}    >      <Stack direction="column" spacing={2}>        <SeatsField control={control} />        <div>          <MuiButtonElement control={control} variant="contained" type="submit">            Book          </MuiButtonElement>        </div>      </Stack>    </form>  );}

How it works

Value. The type prop decides how values move between the form and the input:

typeValue passed to the inputValue stored on change
numberThe field value, with undefined turned into ""null when the input is empty, otherwise a number (+event.target.value)
fileAlways undefined, so the input stays uncontrolledevent.target.files (a FileList)
Anything else (text, email, tel, ...)The field value, with undefined turned into ""event.target.value

There is no valueAsNumber option. Setting type: "number" is what makes the form store numbers.

An emptied number field stores null, and that null is passed back to the input as its value. React expects a string there, so map it as the demo does: transform: { input: (value) => value ?? "" }.

Transforms. transform.input and transform.output replace the defaults above one at a time. If you pass only output, the default input still runs, and the other way round. output receives the change event followed by the current (transformed) value, so (event) => event.target.value.trim() is enough for most cases.

Your own onChange and onBlur run after React Hook Form's handlers and receive the original event, not the transformed value.

Validation. The rule props become React Hook Form rules. Each has a default message, which you can replace through messages:

PropRuleDefault message
requiredValue must not be emptyPlease fill out this field.
minLengthText lengthPlease lengthen this text to {minLength} characters or more
maxLengthText lengthPlease shorten this text to {maxLength} characters or less.
patternRegular expression (a string is converted with new RegExp)Please match the requested format.
minNumeric valueValue must be {min} or later.
maxNumeric valueValue must be {max} or earlier.

Two types add a default on their own when you don't pass pattern:

  • type: "email" adds the pattern /\S+@\S+\.\S+/ with the message "Please enter an email address."
  • type: "url" only sets the pattern message to "Please enter a valid url." It does not add a pattern, so nothing is checked until you pass one.

A key in rules wins over the matching prop. For example, rules.required replaces required and messages.required. Messages other than required can be functions, e.g. maxLength: (max) => `Keep it under ${max} characters`.

The rule props are not forwarded to the DOM. Only required is returned, and it sets the native required attribute, so add noValidate to your <form>. To limit the number spinner as well, set slotProps.input ({ min: 1, max: 8 } in the demo).

Message functions receive the rule value, for example (max) => "At most " + max. A pattern message function receives the pattern and title, and the default pattern message ends with title when you pass one.

A string pattern is not anchored. pattern: "[0-9]+" accepts abc1. Write ^[0-9]+$.

Errors and helper text. error is true when the field has a validation error, unless you pass error yourself. The returned helperText is your helperText as-is. With composeHelperText: true, it becomes the error message while the field is invalid.

InputBase has no helperText prop and passes unknown props to its root <div>. Take helperText out of the result before spreading, as the demo does.

Disabled state. disabled is true when you pass disabled, the form is disabled, disableOnError is set and the field has an error, or disableOnIsSubmitting is set while the form submits. The disabled prop and a disabled form also disable the field in React Hook Form, which skips its validation and leaves it out of the submitted values. disableOnError and disableOnIsSubmitting only disable the input.

This hook does not read MUI's FormControl. Because it always returns disabled as a boolean, an InputBase inside <FormControl disabled> stays enabled unless you pass disabled to the hook. If you want required and disabled taken from a FormControl, use useMuiInputAdapterProps or its filled and outlined variants.

Ref. The returned ref combines React Hook Form's field ref with the ref you pass as the hook's second argument. InputBase puts ref on its root <div>, so React Hook Form can't focus the field when validation fails. Pass the ref to inputRef instead, as the demo does. MuiInputBaseElement and the Input, FilledInput and OutlinedInput elements already do this.

Props

Prop

Type

Return value

Prop

Type

On this page