Comprehensive guidelines for reviewing and refactoring React Creme components Based on breadcrumb and dialog component analysis (January 2025)
This document provides a systematic approach to reviewing components for:
- React best practices and performance
- Design system consistency
- Accessibility (WCAG AA compliance)
- User experience and developer experience
- TypeScript type safety
- Test coverage
- SCSS/CSS quality
- All
useMemohooks have complete dependency arrays - No stale closures (all used variables in deps)
- Expensive computations are properly memoized
Common Issues:
// ❌ BAD: Missing dependencies
const dialogClass = useMemo(
() => classNames(styles.dialog, { [styles.dark]: isDarkMode }),
[isClosing, size] // Missing: isDarkMode, animationType
);
// ✅ GOOD: Complete dependencies
const dialogClass = useMemo(
() => classNames(styles.dialog, { [styles.dark]: isDarkMode }),
[isClosing, size, isDarkMode, animationType]
);- Event handlers are wrapped in
useCallback - Callbacks passed to child components are memoized
- Dependency arrays are complete
Common Issues:
// ❌ BAD: Not memoized
const handleClick = () => {
onClose?.();
};
// ✅ GOOD: Memoized with dependencies
const handleClick = useCallback(() => {
onClose?.();
}, [onClose]);- Component doesn't copy props to state without synchronization
- Derived state uses
useMemoinstead ofuseState -
useEffectisn't used to sync props to state
Common Issues:
// ❌ BAD: Props-to-state anti-pattern
const [items, setItems] = useState(
links.map((link) => ({ name: link }))
);
// Never updates when links prop changes!
// ✅ GOOD: Derived state
const items = useMemo(
() => links.map((link) => ({ name: link })),
[links]
);- No imperative ref mutations (use
useMemoinstead) - Refs for DOM references only
-
useRefused for stable values that don't trigger re-renders - Lazy initialization for expensive values
Common Issues:
// ❌ BAD: Imperative mutation
const focusProps = useRef({});
focusProps.current = { tabIndex: 0 }; // Mutating!
// ✅ GOOD: useMemo
const focusProps = useMemo(
() => ({ tabIndex: 0 }),
[]
);
// ❌ BAD: nanoid called every render
const id = useRef(`rc-dialog-${nanoid()}`);
// ✅ GOOD: Lazy initialization
const id = useRef<string>();
if (!id.current) {
id.current = `rc-dialog-${nanoid()}`;
}- Component wrapped in
React.memoif appropriate - Expensive computations are memoized
- Functions called once, not on every render
Common Issues:
// ❌ BAD: Called on every render
const isDarkMode = useMemo(() => isDark(), []);
// ✅ GOOD: Called once
const isDarkMode = isDark();- No unused props
- No unused refs
- No unused variables
- All imports are used
- No hard-coded spacing values (rem, px)
- Uses
tokens.$space-*variables - Consistent spacing scale
Token Reference:
$space-0: 0; // 0
$space-1: 0.25rem; // 4px
$space-2: 0.5rem; // 8px
$space-3: 0.75rem; // 12px
$space-4: 1rem; // 16px
$space-5: 1.25rem; // 20px
$space-6: 1.5rem; // 24pxCommon Issues:
// ❌ BAD: Hard-coded values
.header {
margin: 0.25rem 0;
padding-left: 1rem;
}
// ✅ GOOD: Design tokens
@use '@design/tokens.scss';
.header {
margin: tokens.$space-1 0;
padding-left: tokens.$space-4;
}- Uses font placeholders (
%font-sm,%font-md,%font-lg) - No hard-coded font sizes
- Proper heading hierarchy
Common Issues:
// ❌ BAD: Hard-coded font size
.title {
font-size: 1rem;
}
// ✅ GOOD: Design system placeholder
.title {
@extend %font-md;
}- Uses design system placeholders for common layouts
- Proper flexbox patterns (
%left,%center,%right,%col) - Uses list patterns for lists (
%list-horizontal-left-wrap)
Common Patterns:
// Horizontal list (like breadcrumbs, tabs, pin)
.wrapper {
@extend %list-horizontal-left-wrap;
}
// List items with inline flex
.item {
align-items: center;
display: flex;
flex-shrink: 0;
flex-wrap: nowrap;
&:not(:first-child) {
margin-left: $spacing;
}
}- Uses theme variables (
theme.$primary,theme.$white, etc.) - Supports dark mode with
.darkclasses - No hard-coded color values
- Uses placeholders:
%border-radius,%shadow-inset,%border - No hard-coded values
- Proper
roleattributes -
aria-labeloraria-labelledbyfor all interactive elements -
aria-describedbyfor additional descriptions -
aria-modal="true"for modals/dialogs -
aria-currentfor current/active items -
aria-hidden="true"for decorative elements
Common Issues:
// ❌ BAD: Missing ARIA attributes
<div role="dialog">
<h2>{title}</h2>
<section>{children}</section>
</div>
// ✅ GOOD: Complete ARIA
<div
role="dialog"
aria-modal="true"
aria-labelledby={titleId}
aria-describedby={bodyId}
>
<h2 id={titleId}>{title}</h2>
<section id={bodyId}>{children}</section>
</div>- Uses semantic elements (
<nav>,<button>,<header>,<footer>) - Proper heading hierarchy (h1-h6)
- Lists use
<ul>/<ol>and<li> - Interactive elements are
<button>or<a>
- All interactive elements are keyboard accessible
- Proper
tabIndexusage - Arrow key navigation for appropriate components
-
Home/Endkeys for navigation -
Escapekey to close modals/overlays - Focus trapping in modals
- No keyboard traps
Common Patterns:
// Create reusable keyboard navigation hooks
export function useHorizontalKeyboard<T extends HTMLElement>(
ref: RefObject<T | null>,
itemCount: number,
currentIndex: number,
onNavigate: (index: number) => void,
enabled = true,
rtl = false
) {
// Handle ArrowLeft, ArrowRight, Home, End
}- Visible focus indicators
- Focus order is logical
- First focusable element receives focus on open (modals)
- Focus returns to trigger on close (modals)
- Meaningful labels for all controls
- State changes are announced
- Error messages are associated with inputs
- No
aria-hiddenon focusable elements
- Component passes
axeaccessibility tests - Manual keyboard navigation tested
- Screen reader tested (if critical component)
it('should have no accessibility violations', async () => {
const { container } = render(<Component />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});- Configurable labels (no hard-coded text)
- Optional sections can be hidden
- Flexible styling options (size, variant, color)
- Optional callback props
Common Issues:
// ❌ BAD: Hard-coded, inflexible
<Button label="okay" onClick={handleSuccess} />
<Button label="cancel" onClick={handleClose} />
// ✅ GOOD: Customizable
<Button
label={primaryButtonLabel}
onClick={handlePrimaryClick}
/>
<Button
label={secondaryButtonLabel}
onClick={handleSecondaryClick}
/>- Required props are actually required
- Optional props have sensible defaults
- TypeScript types reflect requirements
Common Issues:
// ❌ BAD: Title should be required for accessibility
interface DialogProps {
title?: string;
}
// ✅ GOOD: Required with proper type
interface DialogProps {
title: string; // Required for accessibility
}- Clear, descriptive names
- Consistent with other components
- Boolean props prefixed with
is,has,show, etc. - Callback props prefixed with
on
- Breaking changes are avoided
- Deprecated props marked with
@deprecated - Deprecated props still work (with fallback)
export interface DialogProps {
// New prop
onPrimaryClick?: () => void;
// @deprecated Use onPrimaryClick instead
onSuccess?: () => void;
}
// Implementation supports both
const handlePrimaryClick = useCallback(() => {
onPrimaryClick?.();
onSuccess?.(); // Backward compatibility
onClose?.();
}, [onPrimaryClick, onSuccess, onClose]);- Component works on mobile devices
- Touch-friendly targets (min 44x44px)
- Responsive breakpoints in SCSS
- Handles overflow gracefully
- All props have explicit types
- JSDoc comments for complex props
- Union types for variants
- Optional props marked with
?
Example:
export interface ComponentProps {
// Size variant
size?: 'sm' | 'md' | 'lg';
// Callback executed on close
onClose?: () => void;
// React node content
children?: React.ReactNode;
// Required title for accessibility
title: string;
}- No
anytypes (useunknownif needed) - Proper generic types for flexible components
- Type guards for runtime checks
- Handle
nullandundefinedproperly - Optional chaining (
?.) for optional props - Nullish coalescing (
??) for defaults
- Correct ref types for forwarded refs
- Nullable refs:
RefObject<T | null>
- All major features tested
- Edge cases covered
- Error states tested
- Accessibility tested
Minimum Test Categories:
- Rendering: Basic rendering, props, variants
- Interaction: Clicks, keyboard, focus
- Accessibility: ARIA, keyboard nav, axe tests
- Edge Cases: Empty data, long content, special characters
- Callbacks: All event handlers
- Snapshots: Default and all variants
- Tests are readable and maintainable
- Tests use proper roles/labels (not test IDs)
- Tests wait for async operations
- No flaky tests
Example Test Structure:
describe('ComponentName', () => {
describe('Rendering', () => {
it('should render with basic props', () => {});
it('should render all sizes', () => {});
it('should handle empty state', () => {});
});
describe('Interaction', () => {
it('should call onClick when clicked', () => {});
it('should handle keyboard navigation', () => {});
});
describe('Accessibility', () => {
it('should have no violations', async () => {
const { container } = render(<Component />);
expect(await axe(container)).toHaveNoViolations();
});
});
describe('Edge Cases', () => {
it('should handle very long text', () => {});
});
});- Simple components: 10-15 tests
- Complex components: 20-30 tests
- Components with keyboard nav: +5-10 tests
- Components with animations: +3-5 tests
- Imports from
@design/* - Uses
@extendfor placeholders - Uses tokens for spacing, colors, typography
- No direct
@use 'sass:*'unless necessary
Required Imports:
@use '@design/core.scss';
@use '@design/theme.scss';
@use '@design/tokens.scss';
// Component-specific:
@use '@design/list.scss';
@use '@design/button.scss';
// etc.- BEM-like naming (within modules)
- Descriptive class names
- Consistent naming across components
- No generic names (
container,wrapper→ use specific names)
- Variables declared before extends
- Extends before properties
- Properties before nested rules
- No
@extendinside media queries - Use variables for repeated values
Modern Sass Ordering:
.component {
// 1. Variables
$local-spacing: 0.5rem;
// 2. Extends
@extend %flex;
@extend %border-radius;
// 3. Properties
display: block;
padding: $local-spacing;
// 4. Nested selectors
&:hover {
opacity: 0.8;
}
// 5. Media queries
@media (max-width: 640px) {
padding: tokens.$space-1;
}
}- Mobile-first approach (when appropriate)
- Breakpoint consistency
- No fixed pixel widths (use max-width, min-width)
- Test at common breakpoints: 320px, 640px, 768px, 1024px
- No unused classes
- No commented-out code
- Remove old/deprecated styles
- Use
%list-horizontal-left-wrapfor wrapper - Items use inline flex with
flex-wrap: nowrap - Proper spacing with
:not(:first-child) - Keyboard navigation (ArrowLeft, ArrowRight, Home, End)
- RTL support
- Uses
withOverlayHOC -
aria-modal="true" - Focus trapping
- Escape key to close
- Return focus on close
- Body scroll locking
- Label association
- Error state handling
- Disabled state
- Required indicator
- Validation support
- Proper roles
- Keyboard support
- Disabled state
- Loading state
- Focus indicators
- ❌ Props-to-state anti-pattern
- ❌ Missing useMemo/useCallback dependencies
- ❌ Imperative ref mutations
- ❌ Functions called on every render
- ❌ Unused refs, props, or variables
- ❌ Memory leaks in effects
- ❌ Hard-coded spacing (0.25rem, 1rem, etc.)
- ❌ Hard-coded font sizes
- ❌ Not using placeholders (%font-*, %border-radius, etc.)
- ❌ Inconsistent spacing scale
- ❌ Unused CSS classes
- ❌ Missing ARIA attributes
- ❌ Non-semantic HTML
- ❌ Missing keyboard navigation
- ❌ Missing focus management
- ❌ No aria-current on active items
- ❌ Decorative elements not hidden (aria-hidden)
- ❌ Hard-coded labels
- ❌ No customization options
- ❌ Optional props should be required (e.g., title)
- ❌ No mobile/responsive support
- ❌ Insufficient test coverage (<20 tests for complex components)
- ❌ No accessibility tests
- ❌ No keyboard navigation tests
- ❌ Missing edge case tests
- ❌ Using
anytypes - ❌ Incorrect ref types
- ❌ Missing prop types
- ❌ Loose type definitions
- Read component implementation
- Read TypeScript model
- Read SCSS
- Read tests
- Identify issues using checklist
- Group issues by category
- Prioritize: Critical → Important → Nice-to-have
- Identify breaking changes
- Create task list
- Fix React performance issues first
- Integrate design system
- Add accessibility features
- Enhance UX/DX
- Update TypeScript types
- Write/update tests
- Update SCSS
- Run tests:
pnpm test <component> --run - Run build:
pnpm build - Check for TypeScript errors
- Manual testing (keyboard, screen reader)
- Visual regression testing
- Update component JSDoc comments
- Add migration guide if breaking changes
- Update Storybook examples
Issues Found: 8 major issues
- Props-to-state anti-pattern
- Missing aria-current
- No keyboard navigation
- Missing design system patterns
- Stale useMemo dependencies
Result: 29 tests, all passing, fully accessible
Issues Found: 10 major issues
- Stale closures in useMemo
- Imperative ref mutation
- Hard-coded spacing
- Missing aria-modal, aria-describedby
- Hard-coded button labels
Result: 23 tests, all passing, enhanced UX
@testing-library/react- Component testing@testing-library/user-event- User interaction simulationjest-axe/vitest-axe- Accessibility testingvitest- Test runner
oxlint- Fast Rust-based lintereslint- JavaScript/TypeScript lintingstylelint- SCSS/CSS linting
// ❌ BEFORE
const [items, setItems] = useState(
links.map((link, index) => ({
id: nanoid(),
name: link,
selected: selectedCrumbIndex === index,
}))
);
// Never updates when props change!
// ✅ AFTER
const items = useMemo(
() =>
links.map((link, index) => ({
id: nanoid(),
name: link,
selected: currentIndex === index,
})),
[links, currentIndex]
);// ❌ BEFORE
.header {
margin: 0.25rem 0;
}
.footer {
margin-bottom: 1rem;
margin-right: 2rem;
}
// ✅ AFTER
@use '@design/tokens.scss';
.header {
margin: tokens.$space-1 0;
}
.footer {
margin-bottom: tokens.$space-4;
margin-right: tokens.$space-6;
}// ❌ BEFORE
<div role="dialog">
<h2>{title}</h2>
<div>{children}</div>
</div>
// ✅ AFTER
<div
role="dialog"
aria-modal="true"
aria-labelledby={titleId}
aria-describedby={bodyId}
>
<h2 id={titleId}>{title}</h2>
<section id={bodyId}>{children}</section>
</div>Document Version: 1.0 Last Updated: January 2025 Based On: Breadcrumb & Dialog component refactoring