React HOC and Render Props - Part C1
Advanced component patterns continued
Question 1: Layout Components Patternβ
Difficulty: π‘ Medium Frequency: βββ Time: 7 minutes Companies: Design system teams
Questionβ
What are Layout Components? How do they help with consistent UI structure?
Answerβ
Layout Components - Generic components that handle spacing, alignment, and structure without knowing content.
Key Points:
- Separation - Layout vs content
- Reusable - Consistent spacing
- Composable - Stack, Inline, Center, etc.
- Prop-driven - Spacing, alignment via props
- Common in - Design systems
Code Exampleβ
// Layout Components Pattern
// Generic layout components for consistent structure
function Stack({ spacing = 4, children }) {
return (
<div className={`flex flex-col gap-${spacing}`}>
{children}
</div>
);
}
function Inline({ spacing = 4, children }) {
return (
<div className={`flex flex-row gap-${spacing}`}>
{children}
</div>
);
}
function Center({ children }) {
return (
<div className="flex items-center justify-center h-full">
{children}
</div>
);
}
// Usage - Composable layouts
<Stack spacing={6}>
<h1>Title</h1>
<Inline spacing={2}>
<button>Save</button>
<button>Cancel</button>
</Inline>
<Center>
<p>Centered content</p>
</Center>
</Stack>
<details> <summary><strong>π Deep Dive: Layout Components Pattern</strong></summary>
Layout Components Architecture and Design System Philosophy:
Layout components represent a fundamental shift in how we think about UI composition. Rather than coupling layout concerns (spacing, alignment, stacking) with content, this pattern separates them into generic, reusable primitives. This approach originated from design systems like Theme UI, Chakra UI, and Braid Design System, where consistency and maintainability are paramount.
Core Architectural Principles:
The layout component pattern is built on several key principles. First, single responsibility - each component handles one layout concern (Stack for vertical spacing, Inline for horizontal, Center for centering, Box for general container). Second, prop-driven configuration - spacing, alignment, and other layout properties are controlled via props, not CSS classes. Third, composition over configuration - complex layouts are built by composing simple primitives rather than creating monolithic layout components.
Implementation Deep Dive:
A sophisticated layout system goes far beyond simple spacing. Consider a production-grade Stack component:
// Advanced Stack implementation
function Stack({
spacing = 4,
align = 'stretch', // 'start', 'center', 'end', 'stretch'
justify = 'start', // 'start', 'center', 'end', 'between', 'around'
dividers = false,
as = 'div',
children
}) {
const childArray = React.Children.toArray(children);
return React.createElement(
as,
{
className: `stack`,
style: {
display: 'flex',
flexDirection: 'column',
alignItems: align,
justifyContent: justify,
gap: `var(--space-${spacing})` // Uses design tokens
}
},
dividers
? childArray.flatMap((child, i) =>
i < childArray.length - 1
? [child, <Divider key={`divider-${i}`} />]
: [child]
)
: children
);
}
This implementation demonstrates several advanced features. The as prop enables polymorphism (render as different HTML elements), spacing uses design tokens for consistency, and dividers can be automatically inserted between children. The React.Children API ensures proper handling of dynamic children.
CSS-in-JS vs Utility Classes:
Layout components can be implemented with different styling approaches. CSS-in-JS libraries like styled-components provide dynamic styling:
import styled from 'styled-components';
const Stack = styled.div`
display: flex;
flex-direction: column;
gap: ${props => props.theme.space[props.spacing]};
align-items: ${props => props.align};
`;
Utility-first frameworks like Tailwind use class composition, though this can lead to issues with dynamic values (Tailwind JIT solves this). Modern approaches use CSS custom properties for maximum flexibility without runtime overhead.
Advanced Layout Primitives:
Beyond basic Stack/Inline, production design systems include sophisticated layout components. Grid handles 2D layouts with responsive columns, Cluster wraps items with configurable spacing, Sidebar creates flexible sidebar layouts, Cover centers content vertically with optional header/footer, and Switcher switches between horizontal and vertical based on available space.
Type Safety and Developer Experience:
TypeScript makes layout components more robust:
interface StackProps {
spacing?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 8 | 10 | 12 | 16;
align?: 'start' | 'center' | 'end' | 'stretch';
justify?: 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly';
dividers?: boolean;
as?: keyof JSX.IntrinsicElements;
children: React.ReactNode;
}
Restricting spacing to specific values ensures consistency with your design system. This prevents arbitrary spacing values and enforces design constraints.
React Server Components Consideration:
Layout components are perfect candidates for Server Components since they're purely presentational with no client-side interactivity. This reduces JavaScript bundle size and improves performance. Mark them with 'use server' directive in Next.js 13+ App Router.
π Real-World Scenarioβ
Production Case Study: Design System Migration at E-Commerce Platform (2023)
Context: A large e-commerce platform with 150+ developers had accumulated 2,500+ unique spacing values across their codebase (margin: 17px, padding: 23px, etc.). This inconsistency hurt visual quality and made redesigns nearly impossible. They migrated to a layout component system.
Problem Metrics:
- 2,500+ unique spacing values across CSS files
- 45 different font sizes (design system specified 8)
- Developer velocity: 30% of PR comments about spacing inconsistencies
- Design handoff time: 2-3 hours per screen mocking up spacing variations
- Bundle size: 85KB of CSS, 40% was spacing-related utilities
Implementation Timeline (8 weeks):
Week 1-2: Design Token System
// theme.js - Single source of truth
export const spacing = {
0: '0',
1: '0.25rem', // 4px
2: '0.5rem', // 8px
3: '0.75rem', // 12px
4: '1rem', // 16px
6: '1.5rem', // 24px
8: '2rem', // 32px
10: '2.5rem', // 40px
12: '3rem', // 48px
16: '4rem' // 64px
};
Week 3-4: Core Layout Components
// Stack.jsx
export function Stack({ spacing = 4, children, ...props }) {
return (
<div
className="stack"
style={{ '--stack-spacing': `var(--space-${spacing})` }}
{...props}
>
{children}
</div>
);
}
// stack.css
.stack {
display: flex;
flex-direction: column;
gap: var(--stack-spacing);
}
They created Stack, Inline, Grid, Cluster, Box, and Center - six primitives covering 95% of layout needs.
Week 5-6: Automated Migration
They wrote codemods to automatically migrate simple cases:
// Before
<div className="flex flex-col space-y-4">
<Header />
<Content />
</div>
// After (automated codemod)
<Stack spacing={4}>
<Header />
<Content />
</Stack>
The codemod migrated 1,200 components automatically (40% of codebase). Remaining 60% required manual review due to complex spacing logic.
Week 7-8: Manual Migration + Documentation
Developers manually migrated complex components with guidance from the design systems team. They created comprehensive documentation with before/after examples and migration guides.
Results After 6 Months:
Bundle Size Impact:
- CSS bundle: 85KB β 38KB (55% reduction)
- Eliminated 2,000+ duplicate spacing declarations
- Gzip compression improved from 62% to 78% (more repetition)
Developer Velocity:
- PR review time: -40% (fewer spacing discussions)
- Design handoff: 2-3 hours β 20 minutes (designers specify spacing tokens)
- New feature development: +25% faster (no spacing decisions needed)
Visual Consistency:
- Spacing values: 2,500 β 10 (design tokens only)
- Font sizes: 45 β 8 (design system enforced)
- Design QA failures: -70% (automated consistency)
Accessibility Improvements:
- Improved semantic HTML usage (using
asprop appropriately) - Better screen reader navigation (logical document structure)
- Keyboard navigation improved (focus management in Cluster components)
Unexpected Benefits:
- Responsive design simplified: Spacing tokens included responsive values
// Responsive spacing via design tokens
const spacing = {
4: 'clamp(0.75rem, 2vw, 1rem)' // Fluid between 12px-16px
};
- Dark mode support: Layout components integrated with theme context
<Stack spacing={4} dividers> {/* Dividers respect theme */}
- Component library portability: Layout components became a standalone package used across 4 internal projects
Challenges Encountered:
-
Dynamic spacing: Some components needed computed spacing (e.g., based on content length). Solution: Added
spacingMultiplierprop for edge cases. -
Legacy CSS specificity: Old CSS conflicted with layout components. Solution: Used CSS layers (
@layer) to control specificity. -
Team adoption: Some developers resisted "yet another abstraction." Solution: Showed bundle size metrics and developer velocity improvements in retrospective.
Key Debugging Scenario:
During migration, they found layout shifts (CLS) increased by 0.15. Investigation revealed:
// β Problem: Gap not supported in IE11, polyfill caused reflow
.stack { gap: var(--spacing); }
// β
Solution: Feature detection + fallback
.stack > * + * {
margin-top: var(--spacing); /* Fallback */
}
@supports (gap: 1rem) {
.stack { gap: var(--spacing); }
.stack > * + * { margin-top: 0; }
}
This reduced CLS back to baseline levels.
βοΈ Trade-offsβ
Layout Components vs Alternative Approaches:
1. Layout Components vs Utility Classes (Tailwind, Bootstrap)
Layout Components Advantages:
- Enforced consistency: Only valid spacing values via props (TypeScript-enforced)
- Semantics:
<Stack>conveys intent better thanflex flex-col gap-4 - Refactoring safety: Change Stack implementation without touching consumers
- Bundle size: Smaller with many repeated layouts (one Stack class vs many utility classes)
- Learning curve: Junior developers understand
<Stack spacing={4}>instantly
Utility Classes Advantages:
- No abstraction: Direct mapping to CSS, easier debugging
- Speed: Faster initial development (no component creation needed)
- Flexibility: Easier to break consistency when truly needed
- Tooling: Excellent editor autocomplete and IntelliSense
- Community: Huge ecosystem and examples available
Decision Matrix:
| Factor | Layout Components | Utility Classes | Winner |
|---|---|---|---|
| Consistency enforcement | Strong (prop types) | Weak (arbitrary values) | Layout Components |
| Initial development speed | Slower (create components) | Faster (apply classes) | Utilities |
| Long-term maintenance | Easier (change one place) | Harder (find/replace all) | Layout Components |
| Bundle size (large app) | Smaller (shared classes) | Larger (many utilities) | Layout Components |
| Flexibility | Lower (prop constraints) | Higher (any CSS value) | Utilities |
| Type safety | Excellent (TypeScript) | Good (class variants) | Layout Components |
Recommendation: Use layout components for design systems and large applications (50+ developers). Use utility classes for MVPs, prototypes, and small teams where speed matters more than consistency.
2. Layout Components vs CSS Grid/Flexbox Directly
Layout Components Advantages:
- Abstraction: Hide CSS complexity from product developers
- Consistency: Centralized layout logic
- Responsive defaults: Built-in responsive behavior
- Cross-browser: Abstract browser differences
- Semantic naming: Stack/Inline vs flex-direction values
Direct CSS Advantages:
- Performance: No React component overhead
- Flexibility: Full CSS power available
- Debugging: Browser DevTools show actual CSS
- Standard knowledge: CSS skills transfer across frameworks
- No lock-in: Not tied to component library
When to Use Each:
// β
Layout components: Consistent repeated patterns
<Stack spacing={4}>
<Card />
<Card />
<Card />
</Stack>
// β
Direct CSS: Unique complex layouts
<div className="product-grid">
{/* CSS Grid with named areas, custom gaps, etc. */}
</div>
// β Over-abstraction: Don't create components for one-off layouts
<ComplexCustomLayoutThatOnlyExistsOnce>...</>
3. Layout Components vs Compound Components
Layout Components:
- Simple, single-purpose primitives
- No shared state between children
- Focused on spacing/alignment only
- Example: Stack, Inline, Grid
Compound Components:
- Complex components with shared state
- Children communicate via context
- Handle both layout AND behavior
- Example: Tabs, Accordion, Menu
Hybrid Approach:
// Compound component for behavior
<Tabs>
<TabList>
<Tab>Tab 1</Tab>
<Tab>Tab 2</Tab>
</TabList>
{/* Layout components for spacing */}
<TabPanels>
<TabPanel>
<Stack spacing={4}>
<Content1 />
<Content2 />
</Stack>
</TabPanel>
</TabPanels>
</Tabs>
4. Implementation Trade-offs:
CSS-in-JS vs CSS Modules vs Utility Classes:
// CSS-in-JS (styled-components): Runtime overhead, dynamic styling
const Stack = styled.div`
gap: ${p => p.theme.space[p.spacing]};
`;
// +: Dynamic theming, scoped styles
// -: Runtime cost, larger bundle, Flash of Unstyled Content risk
// CSS Modules: Zero runtime, static
<div className={styles.stack} style={{ gap: spacing[props.spacing] }}>
// +: Zero runtime, excellent performance
// -: Less dynamic, need CSS variables for theming
// Inline styles: Maximum flexibility
<div style={{ display: 'flex', gap: spacing }}>
// +: No build step, truly dynamic
// -: No pseudo-selectors, media queries
Recommendation Decision Tree:
Need dynamic theming?
ββ Yes β CSS-in-JS (styled-components, Emotion)
ββ No
ββ Need server components?
β ββ Yes β CSS Modules + CSS Variables
ββ Small app? β Inline styles acceptable
5. Performance Trade-offs:
Component Wrapper Overhead:
Every layout component adds a DOM node. For deeply nested layouts:
// β Excessive nesting: 5 extra DOM nodes
<Stack>
<Inline>
<Stack>
<Center>
<Box><Content /></Box>
</Center>
</Stack>
</Inline>
</Stack>
// β
Flatten where possible: 2 extra nodes
<Stack>
<Inline align="center">
<Content />
</Inline>
</Stack>
Measurement: In a dashboard with 200 layout components, flattening reduced DOM nodes by 35% (1,200 β 780) and improved First Contentful Paint by 80ms.
Recommendation: Balance semantic clarity with performance. Use React DevTools Profiler to identify excessive nesting.
π¬ Explain to Juniorβ
The Restaurant Kitchen Analogy:
Imagine you're organizing a restaurant kitchen. Without layout components, every chef creates their own spacing system:
- Chef A: "I put 3 inches between cutting boards"
- Chef B: "I use 7.5 inches"
- Chef C: "I put them touching, no space"
This chaos makes the kitchen inefficient and ugly. Layout components are like standardized kitchen layouts:
-
Stack (vertical arrangement): Like a shelf system with fixed spacing between shelves. You can choose shelf height (spacing prop), but it's always one of the standard sizes (4", 8", 12", etc.). Everything lines up perfectly.
-
Inline (horizontal arrangement): Like a spice rack with evenly spaced jars. You decide spacing, but it's consistent across all jars.
-
Center: Like a plate presentation template that ensures the main dish is always centered, no matter its size.
Why This Matters:
// β Without layout components: Chaos
<div style={{ marginBottom: '17px' }}>Header</div>
<div style={{ marginBottom: '23px' }}>Content</div>
<div style={{ marginBottom: '19px' }}>Footer</div>
// Every developer picks random spacing!
// β
With layout components: Consistency
<Stack spacing={4}>
<Header />
<Content />
<Footer />
</Stack>
// Everyone uses the same spacing system!
Key Concepts for Juniors:
-
Separation of Concerns:
- Layout components handle where things go
- Content components handle what things are
- Never mix layout and content in one component
-
Composition:
- Build complex layouts from simple primitives
- Like LEGO blocks - combine simple pieces for complex structures
-
Props as Configuration:
- Props control behavior without changing code
spacing={4}vsspacing={6}- same component, different result
Common Junior Mistakes:
// β Mistake 1: Creating layout-specific components
function UserCardWithSpacing() {
return <div style={{ marginBottom: '20px' }}><UserCard /></div>;
}
// β
Better: Use layout components
<Stack spacing={5}>
<UserCard />
<UserCard />
</Stack>
// β Mistake 2: Nesting too deeply
<Stack><Stack><Stack><Content /></Stack></Stack></Stack>
// β
Better: One level usually enough
<Stack spacing={4}>
<Content />
<Content />
</Stack>
// β Mistake 3: Using arbitrary spacing
<Stack spacing={7}> {/* 7 doesn't exist in design system! */}
// β
Better: Use design tokens
<Stack spacing={6}> {/* 6 = 24px, valid token */}
Interview Answer Template:
Question: "Explain layout components and why they're useful."
Answer Structure:
"Layout components are reusable primitives that handle spacing, alignment, and structure without knowing about content. They solve three main problems:
First, consistency. Without them, developers create thousands of unique spacing values. With layout components, we enforce design system constraints through props - you can only use spacing values that exist in the design system.
Second, maintainability. If we change our spacing system from 8px base to 4px base, we update the Stack component once instead of finding and replacing thousands of CSS declarations.
Third, developer experience. Junior developers can build consistent UIs without learning Flexbox or Grid deeply. <Stack spacing={4}> is self-documenting - anyone knows it stacks items vertically with spacing 4.
Common layout primitives include:
- Stack for vertical spacing
- Inline for horizontal spacing
- Grid for 2D layouts
- Center for centering content
In production at [company], we saw a 55% CSS bundle reduction and 40% fewer PR review comments about spacing after migrating to layout components. The key is balancing abstraction with flexibility - provide escape hatches for truly custom layouts.
Trade-offs to consider: Layout components add DOM nodes (performance cost) and reduce flexibility (can't use arbitrary spacing). They're worth it for large teams and design systems but might be overkill for small projects or MVPs.
Example use case: Building a form layout:
<Stack spacing={6}>
<Heading>User Profile</Heading>
<Stack spacing={4}>
<Input label='Name' />
<Input label='Email' />
</Stack>
<Inline spacing={2}>
<Button>Save</Button>
<Button variant='outline'>Cancel</Button>
</Inline>
</Stack>
This creates a consistent, maintainable layout without writing any CSS. Different spacing for the heading (6) vs form fields (4) vs buttons (2) follows our design system hierarchy."
Difficulty Adaptation:
- Junior role: Focus on what/why, simple examples
- Mid-level role: Add trade-offs, bundle size impact, migration strategies
- Senior role: Discuss design system architecture, CSS-in-JS vs modules, performance measurements, team adoption challenges
Question 2: Props Collection Patternβ
Difficulty: π‘ Medium Frequency: βββ Time: 6 minutes Companies: Hook libraries
Questionβ
What is the Props Collection pattern? How does it simplify component APIs?
Answerβ
Props Collection - Return a collection of props from a hook that can be spread onto elements.
Key Points:
- Convenience - Bundle related props
- Consistency - All needed props together
- Simplicity - User spreads one object
- Limited flexibility - Can't override easily
- Common in - Form libraries
Code Exampleβ
// Props Collection Pattern
function useCheckbox() {
const [checked, setChecked] = useState(false);
// Collect all props needed for checkbox
const checkboxProps = {
type: 'checkbox',
checked,
onChange: (e) => setChecked(e.target.checked)
};
return { checked, checkboxProps };
}
// Usage
function MyCheckbox() {
const { checked, checkboxProps } = useCheckbox();
return (
<label>
<input {...checkboxProps} />
{checked ? 'Checked' : 'Unchecked'}
</label>
);
}
</details>
<details> <summary><strong>π Deep Dive: Props Collection Pattern</strong></summary>
Props Collection Pattern Architecture:
The Props Collection pattern emerged as custom hooks became the standard for sharing stateful logic in React. Instead of returning individual values that users must wire up manually, this pattern returns pre-configured objects of props that can be spread directly onto elements. It's a convenience pattern that reduces boilerplate and ensures correct prop wiring.
Core Concept:
The pattern bundles related props together into a single object:
// Without props collection (manual wiring)
function useCheckbox() {
const [checked, setChecked] = useState(false);
return { checked, setChecked };
}
function Manual() {
const { checked, setChecked } = useCheckbox();
return (
<input
type="checkbox"
checked={checked}
onChange={(e) => setChecked(e.target.checked)}
/>
);
}
// With props collection (automatic wiring)
function useCheckbox() {
const [checked, setChecked] = useState(false);
const checkboxProps = {
type: 'checkbox',
checked,
onChange: (e) => setChecked(e.target.checked)
};
return { checked, checkboxProps };
}
function Automatic() {
const { checkboxProps } = useCheckbox();
return <input {...checkboxProps} />; // Clean, simple!
}
Advanced Implementation:
Production-grade props collections handle multiple scenarios:
function useInput(initialValue = '') {
const [value, setValue] = useState(initialValue);
const [touched, setTouched] = useState(false);
const [focused, setFocused] = useState(false);
// Input props collection
const inputProps = {
value,
onChange: (e) => setValue(e.target.value),
onBlur: () => {
setTouched(true);
setFocused(false);
},
onFocus: () => setFocused(true)
};
// Label props collection (for a11y)
const labelProps = {
htmlFor: useId() // React 18 useId for unique IDs
};
// Container props collection
const containerProps = {
'data-focused': focused,
'data-touched': touched
};
const reset = () => {
setValue(initialValue);
setTouched(false);
setFocused(false);
};
return {
value,
touched,
focused,
inputProps,
labelProps,
containerProps,
reset
};
}
// Usage - Multiple prop collections working together
function FormField({ label }) {
const { inputProps, labelProps, containerProps, touched } = useInput('');
return (
<div {...containerProps} className="field">
<label {...labelProps}>{label}</label>
<input {...inputProps} />
{touched && <span>Field touched</span>}
</div>
);
}
Why Props Collections Over Individual Props:
- Convenience: One spread instead of many individual props
- Consistency: All required props included, impossible to forget one
- Accessibility: Can bundle ARIA attributes automatically
- Discoverability: TypeScript IntelliSense shows all available props at once
- Maintenance: Add new props to collection without breaking consumers
TypeScript Integration:
Props collections shine with TypeScript:
interface UseCheckboxReturn {
checked: boolean;
toggle: () => void;
checkboxProps: {
type: 'checkbox';
checked: boolean;
onChange: React.ChangeEventHandler<HTMLInputElement>;
'aria-checked': boolean;
};
}
function useCheckbox(initialChecked = false): UseCheckboxReturn {
const [checked, setChecked] = useState(initialChecked);
const checkboxProps = useMemo(() => ({
type: 'checkbox' as const,
checked,
onChange: (e: React.ChangeEvent<HTMLInputElement>) =>
setChecked(e.target.checked),
'aria-checked': checked
}), [checked]);
const toggle = useCallback(() => setChecked(prev => !prev), []);
return { checked, toggle, checkboxProps };
}
The useMemo optimization prevents recreating the props object on every render, important for performance in large forms.
Limitations and When NOT to Use:
The Props Collection pattern has a critical limitation: users can't easily override individual props. If someone needs to customize the onChange handler, they have to completely reconstruct the object:
// β Hard to override individual props
const { checkboxProps } = useCheckbox();
return (
<input
{...checkboxProps}
onChange={(e) => {
checkboxProps.onChange(e); // Must manually call original
console.log('Custom logic'); // Then add custom logic
}}
/>
);
This limitation led to the Props Getters pattern (next question), which solves this problem by returning functions instead of objects.
Real-World Usage:
Props collections are common in:
- React Hook Form:
register()returns props collections - Downshift:
getInputProps()(though it's technically a getter) - React Aria: Many hooks return props collections for accessibility
- Form libraries: Formik, Final Form use this pattern extensively
Performance Considerations:
Creating new object references on every render can cause unnecessary re-renders if props are compared with React.memo:
// β New object every render = re-render even if values unchanged
function useCheckbox() {
const [checked, setChecked] = useState(false);
return {
checkboxProps: { // New object reference!
type: 'checkbox',
checked,
onChange: (e) => setChecked(e.target.checked)
}
};
}
// β
Memoized object = stable reference
function useCheckbox() {
const [checked, setChecked] = useState(false);
const checkboxProps = useMemo(() => ({
type: 'checkbox',
checked,
onChange: (e) => setChecked(e.target.checked)
}), [checked]); // Only recreate when checked changes
return { checkboxProps };
}
For most cases, the performance impact is negligible, but in large forms (50+ fields), memoization matters.
π Real-World Scenarioβ
Production Case Study: Form Library Performance at SaaS Platform (2023)
Context: A B2B SaaS platform built a custom form library using props collections for 200+ forms across their application. Initially launched without optimization, they encountered performance issues as forms grew more complex.
Problem Discovery:
Engineers noticed forms with 30+ fields felt "sluggish" - typing had visible delay. React DevTools Profiler revealed the culprit:
Profiler Metrics:
- 30-field form typing in text input:
- Total render time: 180ms per keystroke (target: <16ms)
- Components re-rendered: All 30 fields (should be 1)
- Reason: Non-memoized props collections creating new object references
Root Cause Analysis:
// β Original implementation (problematic)
function useFormField(name) {
const { values, setFieldValue, errors } = useFormContext();
// NEW OBJECT EVERY RENDER - BREAKS REACT.MEMO
const inputProps = {
name,
value: values[name],
onChange: (e) => setFieldValue(name, e.target.value),
'aria-invalid': !!errors[name]
};
return { inputProps, error: errors[name] };
}
// Field component wrapped with React.memo
const TextField = React.memo(({ label, ...props }) => {
const { inputProps, error } = useFormField(props.name);
console.log('TextField render'); // Logging to track re-renders
return (
<div>
<label>{label}</label>
<input {...inputProps} />
{error && <span>{error}</span>}
</div>
);
});
The Problem: Every time ANY field changed, the form context updated, causing ALL useFormField hooks to re-run. Each created a new inputProps object reference, breaking React.memo comparison, causing all fields to re-render.
Fix Implementation (Phases):
Phase 1: Add Memoization (Week 1)
// β
Fixed with useMemo
function useFormField(name) {
const { values, setFieldValue, errors } = useFormContext();
const inputProps = useMemo(() => ({
name,
value: values[name],
onChange: (e) => setFieldValue(name, e.target.value),
'aria-invalid': !!errors[name]
}), [name, values[name], setFieldValue, errors[name]]);
// Only recreate when THIS field's value/error changes
return { inputProps, error: errors[name] };
}
Results:
- Render time: 180ms β 45ms per keystroke (75% improvement)
- Re-renders: 30 fields β 3-5 fields (fields sharing validation logic)
- Still not perfect - why are 3-5 fields re-rendering?
Phase 2: Fix Unstable Callbacks (Week 2)
Investigation revealed setFieldValue was recreated on every form render:
// β Context provider creating new functions
function FormProvider({ children }) {
const [values, setValues] = useState({});
const [errors, setErrors] = useState({});
// NEW FUNCTION EVERY RENDER!
const setFieldValue = (name, value) => {
setValues(prev => ({ ...prev, [name]: value }));
};
return (
<FormContext.Provider value={{ values, setFieldValue, errors }}>
{children}
</FormContext.Provider>
);
}
// β
Fixed with useCallback
function FormProvider({ children }) {
const [values, setValues] = useState({});
const [errors, setErrors] = useState({});
const setFieldValue = useCallback((name, value) => {
setValues(prev => ({ ...prev, [name]: value }));
}, []); // Stable function reference
const contextValue = useMemo(() => ({
values,
setFieldValue,
errors
}), [values, setFieldValue, errors]);
return (
<FormContext.Provider value={contextValue}>
{children}
</FormContext.Provider>
);
}
Results:
- Render time: 45ms β 18ms per keystroke (90% total improvement)
- Re-renders: 3-5 fields β 1 field (only the changing field)
- Form typing now felt instant and responsive
Phase 3: Advanced Optimization - Granular Context (Week 3-4)
For extremely large forms (100+ fields in their admin dashboard), even 18ms felt slow. They implemented context splitting:
// Split context into values and actions
const FormValuesContext = React.createContext();
const FormActionsContext = React.createContext();
function useFormField(name) {
const values = useContext(FormValuesContext);
const { setFieldValue } = useContext(FormActionsContext);
// Now only re-renders when FormValuesContext changes
// setFieldValue is always stable from FormActionsContext
const inputProps = useMemo(() => ({
name,
value: values[name],
onChange: (e) => setFieldValue(name, e.target.value)
}), [name, values[name], setFieldValue]);
return { inputProps };
}
Final Results After All Optimizations:
Performance Metrics (100-field admin form):
- Render time: 180ms β 12ms (93% improvement)
- Time to Interactive: 1.2s β 0.3s (form loads faster)
- Memory usage: 45MB β 38MB (fewer object allocations)
- User complaints: 15/month β 0 (typing lag eliminated)
Bundle Size Impact:
- Adding
useMemo/useCallback: +0KB (built-in React hooks) - Props collection pattern vs individual props: -2.3KB (less boilerplate)
Accessibility Wins:
By bundling ARIA attributes in props collections, they ensured consistent accessibility:
const inputProps = useMemo(() => ({
name,
value: values[name],
onChange: (e) => setFieldValue(name, e.target.value),
'aria-invalid': !!errors[name],
'aria-describedby': errors[name] ? `${name}-error` : undefined
}), [name, values[name], errors[name], setFieldValue]);
This caught 23 missing aria-invalid attributes that were causing accessibility audit failures.
Unexpected Discovery:
During optimization, they found a subtle React behavior: spreading props collections BEFORE custom props allows easy overrides:
// β Custom onChange overridden by spread
<input onChange={customHandler} {...inputProps} />
// inputProps.onChange wins - custom handler ignored!
// β
Spread first, then override
<input {...inputProps} onChange={customHandler} />
// Custom handler wins - as expected
This led to documenting prop spread order as a best practice.
βοΈ Trade-offsβ
Props Collection vs Alternative Patterns:
1. Props Collection vs Individual Return Values
Props Collection Advantages:
- Less boilerplate: One spread vs 5+ individual props
- Consistency: Impossible to forget a required prop
- Bundle props: Group related props logically (inputProps, labelProps, etc.)
- Discoverability: TypeScript shows all props in one object
- Accessibility: Can include ARIA attributes automatically
Individual Values Advantages:
- Flexibility: Easy to override or customize individual values
- Explicit: Clear which props come from hook vs component
- Composition: Can mix multiple hooks' values easily
- Debugging: Easier to trace which hook provided which value
Example Comparison:
// Props Collection
function useCheckbox() {
const [checked, setChecked] = useState(false);
return {
checkboxProps: {
type: 'checkbox',
checked,
onChange: (e) => setChecked(e.target.checked)
}
};
}
<input {...checkboxProps} /> // Simple!
// Individual Values
function useCheckbox() {
const [checked, setChecked] = useState(false);
const onChange = (e) => setChecked(e.target.checked);
return { checked, onChange };
}
<input type="checkbox" checked={checked} onChange={onChange} />
// More verbose but more flexible
When to Choose:
| Scenario | Use Props Collection | Use Individual Values |
|---|---|---|
| Simple standardized inputs | β Yes | β No |
| Need to override props often | β No | β Yes |
| Many related props (5+) | β Yes | β No |
| TypeScript discoverability | β Yes | Neutral |
| Composing multiple hooks | β No | β Yes |
2. Props Collection vs Props Getters
Props Collection (object):
const { inputProps } = useInput();
<input {...inputProps} />
Props Getters (function):
const { getInputProps } = useInput();
<input {...getInputProps()} />
Decision Matrix:
| Factor | Props Collection | Props Getters | Winner |
|---|---|---|---|
| Simplicity | Simpler (object spread) | More complex (function call) | Collection |
| Customization | Hard (must reconstruct) | Easy (pass overrides) | Getters |
| Type safety | Excellent | Excellent | Tie |
| Performance | Requires memoization | Requires memoization | Tie |
| Learning curve | Easier | Slightly harder | Collection |
Example - Customization:
// Props Collection: Hard to customize
const { inputProps } = useInput();
<input
{...inputProps}
onChange={(e) => {
inputProps.onChange(e); // Must call original
console.log('Custom'); // Then add custom logic
}}
/>
// Props Getters: Easy to customize
const { getInputProps } = useInput();
<input
{...getInputProps({
onChange: (e) => console.log('Custom') // Auto-merged!
})}
/>
Recommendation: Start with props collections for simple cases. Migrate to props getters when users need customization flexibility.
3. Implementation Complexity:
Simple Props Collection (Easy):
function useSimple() {
return {
inputProps: { type: 'text', value: 'foo' }
};
}
Memoized Props Collection (Moderate):
function useMemoized() {
const [value, setValue] = useState('');
const inputProps = useMemo(() => ({
type: 'text',
value,
onChange: (e) => setValue(e.target.value)
}), [value]);
return { inputProps };
}
Multiple Collections with Dependencies (Complex):
function useComplex() {
const [value, setValue] = useState('');
const [focused, setFocused] = useState(false);
const id = useId();
const inputProps = useMemo(() => ({
id,
type: 'text',
value,
onChange: (e) => setValue(e.target.value),
onFocus: () => setFocused(true),
onBlur: () => setFocused(false)
}), [id, value]);
const labelProps = useMemo(() => ({
htmlFor: id
}), [id]);
return { inputProps, labelProps, focused };
}
Trade-off: More collections = better organization but more complexity. Balance based on component needs.
4. Props Collection Anti-Patterns:
β Anti-Pattern 1: Too Many Props
// Bad: Mixing layout, behavior, and style in one collection
const inputProps = {
type: 'text',
value,
onChange,
className: 'input-field', // Layout concern
style: { color: 'red' }, // Style concern
placeholder: 'Enter...', // Content concern
'aria-label': 'Input' // A11y concern
};
β Better: Separate concerns
const inputProps = { type: 'text', value, onChange }; // Behavior only
const a11yProps = { 'aria-label': 'Input' }; // A11y
// Let component handle layout/style
β Anti-Pattern 2: Including Non-DOM Props
// Bad: Mixing DOM and non-DOM props
const inputProps = {
value,
onChange,
isValid: true, // Not a valid DOM attribute!
fieldId: 5 // Not a valid DOM attribute!
};
<input {...inputProps} /> // Warning: unknown props
β Better: Separate DOM and state
const inputProps = { value, onChange };
const fieldState = { isValid: true, fieldId: 5 };
return { inputProps, fieldState };
5. Performance Considerations:
Object Creation Overhead:
// Measured in production form with 100 fields:
// Without memoization: 50ms per keystroke (recreating 100 objects)
// With memoization: 12ms per keystroke (recreating 1 object)
// Memoization cost itself is negligible (~0.1ms per field)
When Memoization Isn't Worth It:
// Don't memoize if:
// 1. Component not wrapped with React.memo
// 2. Props change on every render anyway
// 3. Form has <10 fields (overhead not noticeable)
// Simple forms: Skip memoization
function useSimpleCheckbox() {
const [checked, setChecked] = useState(false);
return {
checkboxProps: {
checked,
onChange: (e) => setChecked(e.target.checked)
} // No useMemo needed for simple case
};
}
</details>
π¬ Explain to Juniorβ
The Restaurant Order Slip Analogy:
Imagine you're a waiter taking orders. Without props collections, you'd have to remember and communicate every detail separately:
Without Props Collection (Manual):
- Waiter: "Table 5 wants..."
- "...a burger"
- "...cooked medium"
- "...with fries"
- "...no pickles"
- "...extra sauce"
You have to remember and communicate 5 separate pieces of information. If you forget one, the order is wrong.
With Props Collection (Automatic):
- Waiter: "Table 5 wants [hands over complete order slip]"
The order slip has ALL the information bundled together. Impossible to forget a piece!
Code Translation:
// β Without Props Collection: Manual wiring (error-prone)
function useCheckbox() {
const [checked, setChecked] = useState(false);
const onChange = (e) => setChecked(e.target.checked);
return { checked, onChange };
}
function Manual() {
const { checked, onChange } = useCheckbox();
return (
<input
type="checkbox" // You have to remember this
checked={checked}
onChange={onChange}
// Oops, forgot aria-checked! β
/>
);
}
// β
With Props Collection: Everything included (foolproof)
function useCheckbox() {
const [checked, setChecked] = useState(false);
const checkboxProps = {
type: 'checkbox',
checked,
onChange: (e) => setChecked(e.target.checked),
'aria-checked': checked // Accessibility included!
};
return { checkboxProps };
}
function Automatic() {
const { checkboxProps } = useCheckbox();
return <input {...checkboxProps} />; // All props applied automatically!
}
Key Concepts for Juniors:
1. The Spread Operator (...):
Think of ... as "unpack this box":
const box = { color: 'red', size: 'large' };
<div {...box} />
// Becomes:
<div color="red" size="large" />
2. Why Bundle Props:
// Without bundling: Easy to make mistakes
<input
type="text"
value={value}
onChange={handleChange}
// Forgot onBlur? Forgot aria-label? β
/>
// With bundling: All or nothing (no mistakes)
<input {...inputProps} /> // β
Includes EVERYTHING
3. When NOT to Use:
Props collections are great when you DON'T need customization:
// β
Good: Standard checkbox (no customization)
const { checkboxProps } = useCheckbox();
<input {...checkboxProps} />
// β Bad: Need custom onChange
const { checkboxProps } = useCheckbox();
<input
{...checkboxProps}
onChange={(e) => {
// Hard to customize! Need to call checkboxProps.onChange manually
}}
/>
// For this case, use Props Getters pattern instead (next question)
Common Junior Mistakes:
// β Mistake 1: Forgetting the spread
const { inputProps } = useInput();
<input inputProps={inputProps} /> // WRONG! Passes object as prop
// β
Correct: Spread the object
<input {...inputProps} />
// β Mistake 2: Overriding props in wrong order
<input onChange={custom} {...inputProps} />
// inputProps.onChange overwrites custom! β
// β
Correct order: Spread first, override after
<input {...inputProps} onChange={custom} />
// Now custom overwrites inputProps.onChange β
// β Mistake 3: Not memoizing in large forms
function useFormField() {
return {
inputProps: { /* new object every render */ }
};
}
// In 50-field form, creates 50 new objects per keystroke!
// β
Better: Memoize
function useFormField() {
const inputProps = useMemo(() => ({
/* ... */
}), [dependencies]);
return { inputProps };
}
Interview Answer Template:
Question: "What is the Props Collection pattern and when would you use it?"
Answer:
"The Props Collection pattern is when a custom hook returns an object of props that can be spread directly onto an element, rather than returning individual values that need manual wiring.
Example:
// Props Collection pattern
function useCheckbox() {
const [checked, setChecked] = useState(false);
const checkboxProps = {
type: 'checkbox',
checked,
onChange: (e) => setChecked(e.target.checked),
'aria-checked': checked
};
return { checkboxProps };
}
// Clean usage
<input {...checkboxProps} />
Benefits:
First, convenience. Instead of wiring up 4-5 props manually, you spread one object. This reduces boilerplate significantly in forms with many fields.
Second, consistency. You can't forget required props - they're all bundled together. This caught 23 missing aria-invalid attributes in our accessibility audit.
Third, discoverability. With TypeScript, hovering over checkboxProps shows all included props at once, improving developer experience.
Trade-offs:
The main limitation is flexibility. If users need to customize individual props (like adding custom onChange logic), they have to manually call the original handler:
<input
{...checkboxProps}
onChange={(e) => {
checkboxProps.onChange(e); // Call original
console.log('Custom logic'); // Add custom
}}
/>
This is awkward. For cases needing customization, the Props Getters pattern is better - it returns a function that accepts overrides and merges them automatically.
When to use:
- Form libraries (React Hook Form uses this extensively)
- Standardized inputs where customization is rare
- Components with many required props (5+)
- When bundling accessibility attributes
When NOT to use:
- Components needing frequent customization
- Simple components with 1-2 props (overkill)
- Performance-critical code without memoization
In production at [company], we use props collections for our form library. After optimizing with useMemo, we saw typing latency drop from 180ms to 12ms per keystroke in a 100-field admin form."
Difficulty Adaptation:
- Junior: Focus on what/why, simple examples, spread operator explanation
- Mid: Add memoization, performance considerations, real metrics
- Senior: Discuss vs Props Getters, context optimization, bundle size, a11y benefits