Fixing Optional Chaining Runtime Crashes in React Components
A shared component worked fine for months in one context, then crashed immediately the moment it was reused somewhere new.
ShabelleHub's ToolCard component had a crash waiting to happen. It had worked correctly for months — because it was always used in one context, with one set of props. The moment we used it in a new context, it crashed immediately.
The bug was one missing character. The fix was one character. The lesson is about why components that work in one place can silently carry assumptions that make them fragile everywhere else.
The Component
ToolCard is the card component used throughout the site — in the tools directory, on the homepage featured tools section, and on category pages. It renders a tool's name, category, rating, pricing, and a bookmark button.
The bookmark button lets users save tools to a favorites list stored in localStorage. The onToggleFavorite prop is a callback function that the parent component passes down:
// components/ui/index.js — original
export function ToolCard({ tool, isFavorite, onToggleFavorite }) {
return (
<div>
{/* Tool content */}
<button
aria-label={isFavorite ? 'Remove from saved' : 'Save tool'}
onClick={e => {
e.preventDefault();
e.stopPropagation();
onToggleFavorite(tool.id); // crashes if onToggleFavorite is undefined
}}
>
{isFavorite ? '🔖' : '☆'}
</button>
</div>
);
}
In the tools directory and homepage, onToggleFavorite was always provided:
// pages/tools/index.js
<ToolCard
key={tool.id}
tool={tool}
isFavorite={favorites.includes(tool.id)}
onToggleFavorite={toggleFavorite} // always passed
/>
The component worked correctly in every existing usage because every existing usage passed the prop.
When the Crash Appeared
The crash appeared the moment we built the static category pages. The category page rendered ToolCard without passing onToggleFavorite:
// pages/tools/category/[category].js — initial version
{categoryTools.map(tool => (
<ToolCard
key={tool.id}
tool={tool}
isFavorite={favorites.includes(tool.id)}
// onToggleFavorite not passed — category page doesn't manage favorites state
/>
))}
When a user clicked the bookmark button on a category page:
TypeError: onToggleFavorite is not a function
The function call onToggleFavorite(tool.id) attempted to call undefined as a function. JavaScript throws immediately. The component crashes. React's error boundary catches it and shows an error state.
Why This Happens
The component made an implicit assumption: that onToggleFavorite would always be provided. This assumption was never written down, never documented, never enforced by PropTypes or TypeScript. It existed only in the fact that every existing call site happened to pass the prop.
When a new call site was added that did not share that assumption, the assumption failed.
This is one of the most common categories of React bugs: a component works correctly in every existing context but carries hidden dependencies on props being defined. The bug does not appear until someone uses the component somewhere new.
The Fix — Optional Chaining
The fix is optional chaining — the ?. operator introduced in ES2020:
// Before
onToggleFavorite(tool.id);
// After
onToggleFavorite?.(tool.id);
onToggleFavorite?.(tool.id) means: if onToggleFavorite is not null or undefined, call it with tool.id. If it is null or undefined, do nothing and return undefined instead of throwing.
One character — the ? before the .( — changes a crash into a no-op.
The Full Fix in Context
// components/ui/index.js — fixed
export function ToolCard({ tool, isFavorite, onToggleFavorite }) {
return (
<div>
{/* Tool content */}
<button
aria-label={isFavorite ? 'Remove from saved' : 'Save tool'}
onClick={e => {
e.preventDefault();
e.stopPropagation();
onToggleFavorite?.(tool.id); // safe — no crash if prop is undefined
}}
>
{isFavorite ? '🔖' : '☆'}
</button>
</div>
);
}
The behaviour when onToggleFavorite is provided is identical to before. When it is not provided, the button click does nothing — the bookmark icon does not toggle, but the page does not crash.
This is the correct behaviour for a category page where favorites state is not managed: the bookmark button exists but is inert.
Should the Button Be Hidden When the Prop Is Missing?
An alternative fix is to conditionally render the bookmark button only when onToggleFavorite is provided:
{onToggleFavorite && (
<button onClick={e => { e.preventDefault(); onToggleFavorite(tool.id); }}>
{isFavorite ? '🔖' : '☆'}
</button>
)}
This hides the button entirely on pages that do not support favorites. The trade-off:
Optional chaining (?.): Button is visible but inert. Users can see it exists. If favorites state is added to the category page later, the button works automatically.
Conditional render: Button is hidden. UI is cleaner on pages without favorites support. Requires remembering to pass the prop when adding favorites support to a new page.
We chose optional chaining because the category pages may eventually support favorites state, and hiding the button would require a separate change when that happens. The inert bookmark is a minor UX inconsistency; the crash was a hard failure.
Optional Chaining for Other Prop Patterns
The same pattern applies to any prop that is a function and might not be provided:
// Callback props — use optional chaining
onSelect?.(value);
onChange?.(event);
onClose?.();
onSuccess?.(result);
// Object props — use optional chaining for nested access
const label = item?.label ?? 'Untitled';
const count = data?.results?.length ?? 0;
Optional chaining short-circuits at the first null or undefined in the chain and returns undefined rather than throwing. Combined with the nullish coalescing operator (??), it provides safe access with a default value:
// Without optional chaining — crashes if tool.metadata is undefined
const author = tool.metadata.author;
// With optional chaining and nullish coalescing — safe
const author = tool?.metadata?.author ?? 'Shabelle Hub';
The Broader Pattern
The ShabelleHub ToolCard bug is a specific instance of a general problem: components that are only ever used in one context accumulate implicit dependencies on that context. When the component is eventually reused, those dependencies surface as crashes.
The defenses against this pattern:
TypeScript: Define prop types explicitly. Mark optional props with ?. The type checker will warn when a required prop is not passed.
interface ToolCardProps {
tool: Tool;
isFavorite?: boolean;
onToggleFavorite?: (id: number) => void; // explicitly optional
}
Optional chaining on all callback props: Any prop that is a function and could conceivably be omitted should be called with ?.. The cost is one character. The benefit is that the component is safe to use in any context.
Default prop values: For non-function props, provide defaults:
export function ToolCard({ tool, isFavorite = false, onToggleFavorite }) {
This prevents isFavorite from being undefined when the prop is not passed, which avoids a different category of bug where boolean checks on undefined behave unexpectedly.
What the Fix Cost
One character. onToggleFavorite(tool.id) became onToggleFavorite?.(tool.id).
The category pages went from crashing on bookmark click to working silently. The tools directory and homepage continued working identically. No behaviour changed in any existing context.
The character cost of the fix is disproportionately small compared to the impact of the crash it prevented. Optional chaining exists specifically for this pattern — use it on every callback prop that might not be provided.
*This article is part of the ShabelleHub Building in Public series.*
📬 Get the latest AI tool reviews
Expert picks and comparisons, weekly. No spam.