Skip to content

refactor: remove isReactNodeRenderable utility — React handles empty values natively #1075

@georgewrmarshall

Description

@georgewrmarshall

Description

isReactNodeRenderable was introduced in #1029 as a shared utility in @metamask/design-system-shared. It adds abstraction and package weight without solving a real problem — and introduced a subtle regression. A broader audit of @metamask/design-system-react-native found similar patterns across multiple components that are worth investigating. This issue tracks cleaning up all confirmed instances.

Root cause

The original code used an explicit null check to decide whether to render children:

children !== null && children !== undefined

This was unnecessary to begin with. React already handles every empty value uniformly — just rendering {children} is sufficient:

Value React renders
null nothing
undefined nothing
false nothing
true nothing
'' nothing visible

You never need to guard or normalise a ReactNode before passing it to JSX. React handles it.

The only thing that needs a guard is component logic — deciding whether to render a container or row at all. For that, a plain if (children) is sufficient since JavaScript truthiness already covers false, null, and undefined.

How the complexity snowballed

  1. The original code used children !== null && children !== undefined — unnecessarily explicit, but harmless
  2. Cursor Bugbot correctly flagged it didn't catch false from {condition && <Comp />} patterns
  3. Rather than stepping back to {children} (which React handles correctly), the fix kept building on the wrong foundation
  4. The reviewer suggested named guard variables for readability — the right idea
  5. The author interpreted this as a shared exported utility — isReactNodeRenderable was created
  6. The utility was then spread to TitleHub, and || null normalisations were added throughout

The entire chain of complexity was a solution to a problem that never existed. {children} would have worked from day one.

Why the utility is unnecessary

!!prop (and a plain if (prop)) already handles every realistic input correctly:

Value !!prop isReactNodeRenderable
false (from {cond && <Comp />}) false false
null / undefined false false
"text" / <Component /> true true
0 (raw number) false true
'' (empty string) false true ← caused a bug

The utility only diverges from !! on 0 and ''. Neither is a realistic input — consumers pass pre-formatted strings ("$0.00", "0 ETH"), never raw numbers. The '' divergence caused an immediate regression requiring a workaround:

// band-aid needed because isReactNodeRenderable('') === true
const titleNode = isReactNodeRenderable(title) && title !== '' ? title : null;

What the fix looks like

renderLeftSection in HeaderRoot went from:

const hasRenderableChildren = isReactNodeRenderable(children);
const hasTitleContent = title !== false &&
  (isReactNodeRenderable(title) || isReactNodeRenderable(titleAccessory));
const shouldRenderTitleRow = !hasRenderableChildren && hasTitleContent;

const renderLeftSection = () => {
  if (hasRenderableChildren) return children;
  if (shouldRenderTitleRow) return <BoxRow ...>{isReactNodeRenderable(title) && title !== '' ? title : null}</BoxRow>;
  return null;
};

To:

const renderLeftSection = () => {
  if (children) return children;
  if (title || titleAccessory) return <BoxRow ...>{title}</BoxRow>;
  return null;
};

Audit — instances to investigate in @metamask/design-system-react-native

The following were identified as potentially applying the same unnecessary guard pattern. Each should be investigated before changing — some may have intentional reasons.

Likely the same issue

Investigate — may have intentional reasons

  • Toast.tsx:277startAccessory !== null && startAccessory !== undefined && isValidElement(startAccessory)isValidElement check may be intentional
  • ListItem.tsx:62Children.toArray(children).filter(Boolean) — filtering may be intentional
  • BottomSheetFooter.tsx:26Boolean(primaryButtonProps) && Boolean(secondaryButtonProps)
  • HeaderBase.tsx:58Boolean(hasAnyAccessory) / Boolean(hasStartContent)
  • TextFieldSearch.tsx:60Boolean(value) && clearButton

Remaining work — shared package cleanup

  • Delete packages/design-system-shared/src/utils/isReactNodeRenderable.ts
  • Delete packages/design-system-shared/src/utils/isReactNodeRenderable.test.ts
  • Remove the export from packages/design-system-shared/src/index.ts
  • Remove react + @types/react devDependencies from packages/design-system-shared/package.json

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions