Custom fields
Custom fields let you fully control how a property’s value is edited and displayed in a form. Instead of the built‑in renderer for a dataType, you supply a React component. That component receives a rich set of props (FieldProps) so it can:
- Read and update the current value (
value,setValue) - Update any other property in the same form (
setFieldValueorcontext.setFieldValue) - Access all current entity values + form utilities (
context) - Respect form state (
isSubmitting,disabled,showError,error,touched) - Adapt layout (
size,partOfArray,minimalistView,autoFocus) - Use developer defined
customProps
When should you create a custom field?
Section titled “When should you create a custom field?”Use a custom field when you need one (or more) of the following:
- A visual style not covered by built‑ins (color pickers, tag inputs, sliders, charts, AI assisted fields, etc.)
- Composite UI combining several properties (e.g. lat/lng map picker writing to two numeric fields)
- Integrations (upload to an external API, fetch suggestions, geocode, etc.)
If you only need validation or simple transformation, prefer property level validation options first to keep things simple.
If you need dynamic behavior depending on other values, consider using conditional fields instead.
Custom field example
Section titled “Custom field example”A custom text field with a background color supplied via customProps (scroll below for full prop contract and advanced techniques):
import React from "react";import { FieldHelperText, FieldProps, useModeController } from "@firecms/core";import { TextField } from "@firecms/ui";
interface CustomColorTextFieldProps { color: string;}
export default function CustomColorTextField({ property, value, setValue, customProps, includeDescription, showError, error, isSubmitting, context}: FieldProps<string, CustomColorTextFieldProps>) {
const { mode } = useModeController(); const backgroundColor = customProps?.color ?? (mode === "light" ? "#eef4ff" : "#16325f");
return ( <> <TextField inputStyle={{ backgroundColor }} error={!!error} disabled={isSubmitting} label={error ?? property.name} value={value ?? ""} onChange={(evt: any) => setValue(evt.target.value)} /> <FieldHelperText includeDescription={includeDescription} showError={showError} error={error} property={property} /> </> );}Usage in a collection:
export const blogCollection = buildCollection({ id: "blog", path: "blog", name: "Blog entry", properties: { // ... other properties gold_text: { name: "Gold text", description: "This field is using a custom component defined by the developer", dataType: "string", Field: CustomColorTextField, customProps: { color: "gold" } } }});Component contract (FieldProps)
Section titled “Component contract (FieldProps)”Your component must at minimum:
- Read the current
value(it can beundefinedornullfor empty) - Call
setValue(newValue)when the user changes it
Recommended good practices:
- Honor
disabled/isSubmitting - Show the label and error (use your own UI or
<FieldHelperText>/ built‑ins) - Avoid heavy side effects on every keystroke (debounce network calls)
Full interface: FieldProps (includes detailed comments).
Passing custom props
Section titled “Passing custom props”Provide a customProps object in the property definition. The object is strongly typed via the second generic of FieldProps<T, CustomProps>.
Accessing the rest of the entity (form context)
Section titled “Accessing the rest of the entity (form context)”context gives you live access to:
- All current values (
context.values) context.setFieldValue(key, value)to update any other fieldcontext.save(values)to trigger a save programmatically (rarely needed in fields)- Metadata:
entityId,status(new/existing/copy),collection,openEntityMode,disabled
This enables cross‑field logic (e.g. auto‑fill slug when title changes) or conditional disabling.
Rendering (or composing) other properties inside a custom field
Section titled “Rendering (or composing) other properties inside a custom field”If your custom field wants to include the UI of another property, use PropertyFieldBinding.
This keeps validation and consistency:
import { PropertyFieldBinding } from "@firecms/core";
<PropertyFieldBinding propertyKey="subtitle" property={collection.properties.subtitle} context={context} includeDescription/>This is ideal for composite widgets that orchestrate multiple underlying values.
For example, the built-in map default widget is just a wrapper around the properties defined
Handling arrays & nested data
Section titled “Handling arrays & nested data”When your custom field is inside an array:
partOfArrayistrue- You may receive an index in a parent context when building nested array editors
For nested values (e.g. editing address.street inside a composite field), call setFieldValue("address.street", value).
Validation strategies
Section titled “Validation strategies”Prefer declarative validation in the property config when possible. You can still implement client‑side guards in the field (e.g. ignore invalid keystrokes) but allow the central validation to surface errors.
Common patterns:
- Trim on blur but preserve user typing: keep raw input in local state, call
setValuewith cleaned value on blur. - Async validation (e.g. uniqueness): debounce the check, set a transient local error, do not block typing.
Performance tips
Section titled “Performance tips”- Debounce network or expensive computations (
useEffect+setTimeoutor a utility) instead of per‑keystroke. - Memo heavy child components based on relevant props.
- Avoid storing large derived objects in state; derive them on render or memoize.
Advanced example: Composite slug editor
Section titled “Advanced example: Composite slug editor”Automatically generates a slug from the title, but allows manual override.
function SlugField({ value, setValue, context, property, showError, error }: FieldProps<string>) { const title = context.values.title as string | undefined;
React.useEffect(() => { if (!value && title) { const auto = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, ""); setValue(auto); } }, [title]);
return ( <TextField label={property.name} value={value ?? ""} error={!!error} onChange={(e: any) => setValue(e.target.value)} helperText={showError ? error : "Will auto-generate from Title if left empty"} /> );}Using PropertyFieldBinding inside a composite field
Section titled “Using PropertyFieldBinding inside a composite field”function GeoPointField({ context }: FieldProps<any>) { return ( <div style={{ display: "flex", gap: 8 }}> <PropertyFieldBinding propertyKey="lat" property={context.collection?.properties.lat} context={context} minimalistView /> <PropertyFieldBinding propertyKey="lng" property={context.collection?.properties.lng} context={context} minimalistView /> {/* Could add a map picker that calls context.setFieldValue("lat", newLat) */} </div> );}Troubleshooting & gotchas
Section titled “Troubleshooting & gotchas”- Value not updating: Ensure you call
setValue(not mutatevaluedirectly) and that you don’t shadow thevaluein local state without syncing. - Error never shows: Remember
showErrorgates visual display;errorcan exist whileshowErroris false. - Cross‑field updates ignored: Use the exact property key (e.g.
address.street, array indexes likeitems[0].price). - Field re-renders too often: Wrap heavy logic in
useMemo/useCallback, avoid creating new objects every render. - Need read‑only mode: Respect
disabledfrom props orcontext.disabled.
Next steps
Section titled “Next steps”- Explore other customization: Custom previews
- Reuse logic across many properties: create a shared field component and pass different
customProps. - Open source friendly? Consider contributing a reusable field to the community.
By leveraging custom fields you can create rich authoring experiences closely aligned with your product’s domain while keeping validation, state and persistence centralized in FireCMS.