Custom Components - Fixing a Form Builder Input Problem

5 replies
Last updated: Jul 19, 2022
Hi all, I'm having an issue with a very simple custom component. It's actually just a wrapper around a
FormBuilderInput
to work around the fact that the
object
field in Sanity always displays in an indented manner. This component solves this using some custom CSS.
This works, but the issue is that some fields are not working correctly anymore - an Array of Object items is no longer editable (the edit modal doesn't show). If I add the
focusPath
prop back it does work, but then I run into an infinite loop when used within a field group. Here's the code:
import React from "react";
import { FormBuilderInput } from "part:@sanity/form-builder";

const WrappedField = React.forwardRef((props, ref) => {
  const { inputComponent, ...usableType } = props.type;
  const noHeader = usableType.options?.header === false;
  // omit the focusPath to prevent infinite loop
  const { focusPath, ...properties } = props; 

  return (
    <div className={`tm-object tm-${noHeader ? "without" : "with"}-header`}>
      <FormBuilderInput {...properties} ref={ref} type={usableType} />
    </div>
  );
});

export default WrappedField;
AI Update

I can see you're working with the older Sanity Studio v2 API using part:@sanity/form-builder. I need to be upfront with you: Studio v2 is no longer supported (support ended December 7, 2023 according to this documentation), and the Parts System has been deprecated in favor of Studio v3's architecture.

The issue you're experiencing is a known limitation with wrapping FormBuilderInput in v2. The focusPath prop is essential for nested editing (like opening array item modals), but it can create infinite render loops when your custom component recursively references itself.

Why this happens:

When you register a custom component for a field type, Sanity uses that component as the inputComponent for that type. When you render <FormBuilderInput> inside your custom component with the same type, it tries to render your custom component again, creating a loop. Omitting focusPath breaks array editing; including it can trigger the loop in certain contexts like field groups.

Potential fix to try:

The key is to remove the inputComponent property from the type definition to break the recursive cycle while keeping focusPath intact:

import React from "react";
import { FormBuilderInput } from "part:@sanity/form-builder";

const WrappedField = React.forwardRef((props, ref) => {
  // Remove inputComponent to prevent recursive rendering
  const { inputComponent, ...cleanType } = props.type;
  const noHeader = cleanType.options?.header === false;

  return (
    <div className={`tm-object tm-${noHeader ? "without" : "with"}-header`}>
      {/* Pass all props INCLUDING focusPath, but with cleaned type */}
      <FormBuilderInput {...props} ref={ref} type={cleanType} />
    </div>
  );
});

export default WrappedField;

By stripping inputComponent from the type, you tell FormBuilderInput to use the default renderer instead of your custom one, which should prevent the loop while preserving focusPath for array editing.

The honest recommendation:

Since v2 is no longer supported and I cannot verify v2-specific behavior against current documentation, I'd strongly recommend migrating to Studio v3. The custom input component system in v3 completely avoids these wrapper pattern issues. You define custom components using the components.input property in your schema, and the system handles focus management and nesting automatically without the fragile FormBuilderInput wrapper pattern.

The migration guide provides detailed instructions for transitioning from v2 to v3. If you absolutely cannot migrate right now, try the fix above, but be aware that troubleshooting v2-specific edge cases will only get harder as the platform moves forward.

Looks like this is a known issue, and Sanity is aware of this. There's a workaround that pops up in several repos on GH, most managed by Sanity staff: https://github.com/nrkno/nrkno-sanity-libs/blob/8723908ddba1628f3aa3954fb264a44dc0[…]ges/sanity-plugin-nrkno-odd-utils/src/lib/NestedFormBuilder.tsx -
user T
I saw you used this elsewhere too.
This solves my issue:


import React from "react";
import { NestedFormBuilder } from "./NestedFormBuilder";

const WrappedField = React.forwardRef((props, ref) => {
  const { inputComponent, ...usableType } = props.type;
  const noHeader = usableType.options?.header === false;

  return (
    <div className={`tm-object tm-${noHeader ? "without" : "with"}-header`}>
      <NestedFormBuilder {...props} ref={ref} type={usableType} />
    </div>
  );
});

export default WrappedField;
Correct. You can get an infinite loading loop when trying to decorate a built-in field. That’s what this hook does .
Thanks for confirming this! However, doesn't
const { inputComponent, ...usableType } = props.type;
take care of this already? I'm omitting
inputComponent
, but perhaps I'm misunderstanding the hook?
(To be clear I didn’t write this hook, another of our developers did, I just know it solved my issue 🙂 )
I see ... the main problems I faced were focus/interaction related, the infinite loop you mentioned did not occur - it was only when I passed on
focusPath
that things were recursing indefinitely.

Sanity – Build the way you think, not the way your CMS thinks

Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.

Was this answer helpful?