docs_headless/src/content/docs/useInput.md
This hook lets you build custom inputs for ra-core. It's a wrapper around react-hook-form's useController.
Ra-core adds functionality to react-hook-form:
onChange,useInput expects at least a source, and returns an object with the following properties:
{ id, field, fieldState, formState, isRequired }
For instance, to build a custom input for a title field:
import { useInput } from 'ra-core';
const TitleInput = ({ source, label }) => {
const { id, field, fieldState } = useInput({ source });
return (
<label htmlFor={id}>
{label}
<input id={id} {...field} />
{fieldState.error && <span role="alert">{fieldState.error.message}</span>}
</label>
);
};
| Prop | Required | Type | Default | Description |
|---|---|---|---|---|
source | Required | string | - | The name of the field in the record |
defaultValue | Optional | any | - | The default value of the input |
readOnly | Optional | boolean | false | If true, the input is in read-only mode. |
disabled | Optional | boolean | false | If true, the input is disabled. |
format | Optional | Function | - | A function to format the value from the record to the input value |
helperText | Optional | string | - | Text to be displayed under the input |
label | Optional | string | - | Input label. |
parse | Optional | Function | - | A function to parse the value from the input to the record value |
validate | Optional | Function | Function[] | - | A function or an array of functions to validate the input value |
id | Optional | string | auto-generated | The id of the input |
onChange | Optional | Function | - | A function to call when the input value changes |
onBlur | Optional | Function | - | A function to call when the input is blurred |
Additional props are passed to react-hook-form's useController hook.
defaultValueValue of the input if the record has no value for the source.
<Form record={{ id: 123, title: 'Lorem ipsum' }}>
<NumberInput source="age" defaultValue={18} />
<TextInput source="title" defaultValue="Hello, World!" />
</Form>
Ra-core will ignore these default values if the Form already defines a form-wide defaultValues:
import { CreateBase, Form } from 'ra-core';
import { TextInput } from './TextInput';
import { NumberInput } from './NumberInput';
export const PostCreate = () => (
<CreateBase>
<Form defaultValues={{
title: 'My first post',
body: 'This is my first post',
nb_views: 123,
}}>
<TextInput source="title" />
<TextInput source="body" multiline />
<NumberInput source="nb_views" defaultValue={0} />
</Form>
</CreateBase>
);
Tip: defaultValue cannot use a function as value. For default values computed at render time, set the defaultValues at the form level.
import { CreateBase, Form } from 'ra-core';
import uuid from 'uuid';
import { TextInput } from './TextInput';
import { NumberInput } from './NumberInput';
const postDefaultValue = () => ({ id: uuid(), created_at: new Date(), nb_views: 0 });
export const PostCreate = () => (
<CreateBase>
<Form defaultValues={postDefaultValue}>
<TextInput source="title" />
<TextInput source="body" multiline />
<NumberInput source="nb_views" />
</Form>
</CreateBase>
);
readOnlyThe readOnly prop set to true should make the element not mutable, meaning the user can not edit the control.
<TextInput source="title" readOnly />
Contrary to disabled controls, read-only controls are still focusable and are submitted with the form.
Tip: readOnly is actually not interpreted by useInput. It's up to the input component to handle this prop.
disabledThe disabled prop set to true makes the element not mutable, focusable, or even submitted with the form.
<TextInput source="title" disabled />
Contrary to read-only controls, disabled controls can not receive focus and are not submitted with the form.
Warning: Note that disabled inputs are not included in the form values, and hence may trigger warnWhenUnsavedChanges if the input previously had a value in the record.
Tip: To include the input in the form values, you can use readOnly instead of disabled.
formatThe format prop accepts a callback taking the value from the form state, and returning the input value (which should be a string).
form state value --> format --> form input value (string)
<NumberInput
source="unit_price"
format={v => String(v * 100)}
parse={v => parseFloat(v) / 100}
/>
format often comes in pair with parse to transform the input value before storing it in the form state. See the Transforming Input Value section for more details.
Tip: By default, ra-core inputs have the following format function, which turns any null or undefined value into an empty string. This is to avoid warnings about controlled/uncontrolled input components:
const defaultFormat = (value: any) => value == null ? '' : value;
helperTextMost inputs accept a helperText prop to display a text below the input.
<BooleanInput
source="has_newsletter"
helperText="User has opted in to the newsletter"
/>
Set helperText to false to remove the empty line below the input.
Tip: helperText is actually not interpreted by useInput. It's up to the input component to handle this prop.
Tip: It's a good idea to have your component also support translation keys in helperText.
labelThe input label.
label is actually not interpreted by useInput. It's up to the input component to handle this prop.
It's a good idea to have your component support translation keys in label, and hiding label when label={false}.
When omitted, you can use the humanized source property as default label.
Tip: Use the <FieldTitle> component to help you implement this logic.
<TextInput source="title" />
<TextInput source="title" label="Post title" />
<TextInput source="title" label={false} />
parseThe parse prop accepts a callback taking the value from the input (which is a string), and returning the value to put in the form state.
form input value (string) ---> parse ---> form state value
<NumberInput
source="unit_price"
format={v => String(v * 100)}
parse={v => parseFloat(v) / 100}
/>
parse often comes in pair with format to transform the form value before passing it to the input. See the Transforming Input Value section for more details.
Tip: By default, ra-core inputs have the following parse function, which transforms any empty string into null:
const defaultParse = (value: string) => value === '' ? null : value;
sourceSpecifies the field of the record that the input should edit.
<Form record={{ id: 123, title: 'Hello, world!' }}>
<TextInput source="title" />
</Form>
If you edit a record with a complex structure, you can use a path as the source parameter. For instance, if the API returns the following 'book' record:
{
"id": 1234,
"title": "War and Peace",
"author": {
"firstName": "Leo",
"lastName": "Tolstoi"
}
}
Then you can display a text input to edit the author's first name as follows:
<TextInput source="author.firstName" />
validateA function or an array of functions to validate the input value.
Validator functions should return undefined if the value is valid, or a string describing the error if it's invalid.
const validateAge = (value: number) => {
if (value < 18) {
return 'Must be over 18';
}
return undefined;
}
<NumberInput source="age" validate={validate} />
Tip: If your admin has multi-language support, validator functions should return message identifiers rather than messages themselves. Ra-core automatically passes these identifiers to the translation function:
// in validators/required.js
const required = () => (value: any) =>
value
? undefined
: 'myroot.validation.required';
Ra-core comes with a set of built-in validators:
required(message) if the field is mandatory,minValue(min, message) to specify a minimum value for integers,maxValue(max, message) to specify a maximum value for integers,minLength(min, message) to specify a minimum length for strings,maxLength(max, message) to specify a maximum length for strings,number(message) to check that the input is a valid number,email(message) to check that the input is a valid email address,regex(pattern, message) to validate that the input matches a regex,choices(list, message) to validate that the input is within a given list,These are validator factories, so you need to call the function to get the validator.
<NumberInput source="age" validate={required()} />
You can use an array of validators to apply different validation rules to the same input.
<NumberInput source="age" validate={[required(), validateAge]} />
Note: You can’t use both input-level validation and form-level validation - this is a react-hook-form limitation.
Check the Validation chapter for details.
// in LatLongInput.js
import { useInput, required } from 'ra-core';
const BoundedInput = (props) => {
const { onChange, onBlur, label, helperText, ...rest } = props;
const {
field,
fieldState: { invalid, error },
isRequired
} = useInput({
// Pass the event handlers to the hook but not the component as the field property already has them.
// useInput will call the provided onChange and onBlur in addition to the default needed by react-hook-form.
onChange,
onBlur,
...rest,
});
return (
<div>
<label htmlFor={field.name}>
{label}
{isRequired && <span aria-hidden="true"> *</span>}
</label>
<input
id={field.name}
{...field}
aria-invalid={invalid}
aria-errormessage={`${field.name}-error`}
{...rest}
/>
{invalid && error?.message ? (
<span id={`${field.name}-error`} role="alert">
{error.message}
</span>
) : helperText !== false ? (
<span className="helper-text">
{helperText}
</span>
) : null}
</div>
);
};
const LatLngInput = props => {
const { source, ...rest } = props;
return (
<span>
<BoundedInput source="lat" label="Latitude" validate={required()} {...rest} />
<BoundedInput source="lng" label="Longitude" validate={required()} {...rest} />
</span>
);
};
// in SexInput.js
import { useInput } from 'ra-core';
const SexInput = props => {
const { field, isRequired } = useInput(props);
return (
<div>
<label htmlFor={field.name}>
Sex
{isRequired && <span aria-hidden="true"> *</span>}
</label>
<select
id={field.name}
{...field}
aria-required={isRequired}
>
<option value="M">Male</option>
<option value="F">Female</option>
</select>
</div>
);
};
export default SexInput;
Tip: useInput accepts all arguments that you can pass to useController. Besides, components using useInput accept props like format and parse, to convert values from the form to the input, and vice-versa:
const parse = value =>;
const format = value =>;
const PersonEdit = () => (
<EditBase>
<Form>
<SexInput
source="sex"
format={formValue => formValue === 0 ? 'M' : 'F'}
parse={inputValue => inputValue === 'M' ? 0 : 1}
/>
</Form>
</EditBase>
);
react-hook-form's formState is wrapped with a Proxy to improve render performance and skip extra computation if specific state is not subscribed. So, make sure you deconstruct or read the formState before render in order to enable the subscription.
const { isDirty } = useFormState(); // ✅
const formState = useFormState(); // ❌ should deconstruct the formState
This pattern should be followed when writing a custom input with useInput().
const { formState: { isSubmitted }} = useInput(props); // ✅
const { formState } = useInput(props);
const submitted = formState.isSubmitted; // ❌
The data format returned by the input component may not be what your API desires. You can use the parse and format functions to transform the input value when saving to and loading from the record.
Mnemonic for the two functions:
parse(): input -> recordformat(): record -> inputLet's look at a simple example. Say the user would like to input values of 0-100 to a percentage field but your API (hence record) expects 0-1.0. You can use simple parse() and format() functions to archive the transform:
<NumberInput
source="percent"
format={v => v * 100}
parse={v => parseFloat(v) / 100}
label="Formatted number"
/>
Another classical use-case is with handling dates. Imagine you have a <DateInput> that stores and returns a string. If you would like to store a JavaScript Date object in your record instead, you can do something like this:
const dateFormatRegex = /^\d{4}-\d{2}-\d{2}$/;
const dateParseRegex = /(\d{4})-(\d{2})-(\d{2})/;
const convertDateToString = (value: string | Date) => {
// value is a `Date` object
if (!(value instanceof Date) || isNaN(value.getDate())) return '';
const pad = '00';
const yyyy = value.getFullYear().toString();
const MM = (value.getMonth() + 1).toString();
const dd = value.getDate().toString();
return `${yyyy}-${(pad + MM).slice(-2)}-${(pad + dd).slice(-2)}`;
};
const dateFormatter = (value: string | Date) => {
// null, undefined and empty string values should not go through dateFormatter
// otherwise, it returns undefined and will make the input an uncontrolled one.
if (value == null || value === '') return '';
if (value instanceof Date) return convertDateToString(value);
// Valid dates should not be converted
if (dateFormatRegex.test(value)) return value;
return convertDateToString(new Date(value));
};
const dateParser = value => {
//value is a string of "YYYY-MM-DD" format
const match = dateParseRegex.exec(value);
if (match === null || match.length === 0) return;
const d = new Date(parseInt(match[1]), parseInt(match[2], 10) - 1, parseInt(match[3]));
if (isNaN(d.getDate())) return;
return d;
};
<DateInput source="isodate" format={dateFormatter} parse={dateParser} defaultValue={new Date()} />
Tip: A common usage for this feature is to deal with empty values. Indeed, HTML form inputs always return strings, even for numbers and booleans, however most backends expect a value like null. This is why, by default, all ra-core inputs will store the value null when the HTML input value is ''.
Tip: If you need to do this globally, including for custom input components that do not use the useInput hook, have a look at the sanitizeEmptyValues prop of the <Form> component.
Ra-core Form components initialize the input based on the current RecordContext values. If the RecordContext is empty or the matching property for this input is empty, the input will be empty.
If a record property is not undefined, it is not considered empty:
0 is a valid valuenull is a valid valueIn all those cases, the value will not be considered empty and the default value won't be applied.