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
- The original code used
children !== null && children !== undefined — unnecessarily explicit, but harmless
- Cursor Bugbot correctly flagged it didn't catch
false from {condition && <Comp />} patterns
- Rather than stepping back to
{children} (which React handles correctly), the fix kept building on the wrong foundation
- The reviewer suggested named guard variables for readability — the right idea
- The author interpreted this as a shared exported utility —
isReactNodeRenderable was created
- 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
Remaining work — shared package cleanup
References
Description
isReactNodeRenderablewas 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-nativefound 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:
This was unnecessary to begin with. React already handles every empty value uniformly — just rendering
{children}is sufficient:nullundefinedfalsetrue''You never need to guard or normalise a
ReactNodebefore 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 coversfalse,null, andundefined.How the complexity snowballed
children !== null && children !== undefined— unnecessarily explicit, but harmlessfalsefrom{condition && <Comp />}patterns{children}(which React handles correctly), the fix kept building on the wrong foundationisReactNodeRenderablewas createdTitleHub, and|| nullnormalisations were added throughoutThe 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 plainif (prop)) already handles every realistic input correctly:!!propisReactNodeRenderablefalse(from{cond && <Comp />})false✓false✓null/undefinedfalse✓false✓"text"/<Component />true✓true✓0(raw number)falsetrue''(empty string)falsetrue← caused a bugThe utility only diverges from
!!on0and''. 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:What the fix looks like
renderLeftSectioninHeaderRootwent from:To:
Audit — instances to investigate in
@metamask/design-system-react-nativeThe 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
TitleHub.tsx—isReactNodeRenderable(x)throughout (tracked in refactor: simplify HeaderRoot slot guards to direct conditionals #1076)BannerBase.tsx:23—const hasContent = (content) => content !== null && content !== undefinedSkeleton.tsx:21—const hasChildren = children !== null && children !== undefinedInvestigate — may have intentional reasons
Toast.tsx:277—startAccessory !== null && startAccessory !== undefined && isValidElement(startAccessory)—isValidElementcheck may be intentionalListItem.tsx:62—Children.toArray(children).filter(Boolean)— filtering may be intentionalBottomSheetFooter.tsx:26—Boolean(primaryButtonProps) && Boolean(secondaryButtonProps)HeaderBase.tsx:58—Boolean(hasAnyAccessory)/Boolean(hasStartContent)TextFieldSearch.tsx:60—Boolean(value) && clearButtonRemaining work — shared package cleanup
packages/design-system-shared/src/utils/isReactNodeRenderable.tspackages/design-system-shared/src/utils/isReactNodeRenderable.test.tspackages/design-system-shared/src/index.tsreact+@types/reactdevDependencies frompackages/design-system-shared/package.jsonReferences
TitleHub.tsx,BannerBase.tsx,Skeleton.tsx,Toast.tsx,BottomSheetFooter.tsx,HeaderBase.tsx,TextFieldSearch.tsx,ListItem.tsx