Component Architecture Design
Component Design Principles
Single Responsibility
A component should do one thing. The criterion: if a component has more than one reason to change, it has too many responsibilities.
// Bad: UserProfile both displays and edits information
function UserProfile({ user, onSave }) { /* ... */ }
// Good: Split display and editing
function UserProfileCard({ user }) { /* Read-only display */ }
function UserProfileForm({ user, onSave }) { /* Edit form */ }
Interface Contract
A component’s Props are its API. Good interface design follows:
- Minimality: Only pass necessary data, not entire objects
- Clarity: Use TypeScript to define Props types
- Defaults: Provide reasonable defaults for optional Props
interface ButtonProps {
variant: "primary" | "secondary" | "danger"; // Union type to limit options
size?: "sm" | "md" | "lg"; // Optional, has default
disabled?: boolean;
onClick?: () => void;
children: React.ReactNode;
}
Controlled vs. Uncontrolled
// Controlled component: state controlled externally
function Input({ value, onChange }) {
return <input value={value} onChange={(e) => onChange(e.target.value)} />;
}
// Uncontrolled component: state managed internally
function Input({ defaultValue }) {
const [value, setValue] = useState(defaultValue);
return <input value={value} onChange={(e) => setValue(e.target.value)} />;
}
Selection principle: Form data needs real-time validation/linkage/submission → controlled; simple input doesn’t need external awareness → uncontrolled. You can also design to support both modes:
function Input({ value: controlledValue, defaultValue, onChange }) {
const isControlled = controlledValue !== undefined;
const [internalValue, setInternalValue] = useState(defaultValue);
const value = isControlled ? controlledValue : internalValue;
return (
<input
value={value}
onChange={(e) => {
if (!isControlled) setInternalValue(e.target.value);
onChange?.(e.target.value);
}}
/>
);
}
Component Communication Patterns
graph TD
A["Component Communication"] --> B["Parent-Child"]
A --> C["Cross-level"]
A --> D["Sibling/Unrelated"]
B --> B1["Props Passing"]
B --> B2["Callback Functions"]
B --> B3["Ref Forwarding"]
C --> C1["Context / Provide-Inject"]
C --> C2["Event Bus"]
D --> D1["State Management"]
D --> D2["Event Bus"]
| Pattern | Use Case | Characteristics |
|---|---|---|
| Props / Events | Parent-child | Most direct, clear data flow |
| Provide / Inject | Ancestor-descendant | Cross-level passing, avoids layer-by-layer Props |
| Ref + ImperativeHandle | Parent calls child method | Exposes imperative API (e.g., focus, scroll) |
| State Management | Global sharing | Single data source, cross-component sync |
| Event Bus | Any components | Decoupled but hard to trace, use cautiously |
// Ref forwarding: parent component calls child method
const Input = forwardRef((props, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => inputRef.current.focus(),
clear: () => { inputRef.current.value = ""; },
}));
return <input ref={inputRef} />;
});
// Parent component
const inputRef = useRef();
inputRef.current.focus(); // Imperative call
Reuse Pattern Evolution
Higher-Order Components (HOC)
// Wraps component to inject additional capability
function withAuth(WrappedComponent) {
return function AuthComponent(props) {
const { user } = useAuth();
if (!user) return <Redirect to="/login" />;
return <WrappedComponent {...props} user={user} />;
};
}
const ProtectedPage = withAuth(Dashboard);
Problems: nesting hell (withA(withB(withC(Component)))), unclear Props sources, lost Refs.
Render Props
function MouseTracker({ render }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
// ...listen to mouse movement
return render(position);
}
<MouseTracker render={({ x, y }) => <Cursor x={x} y={y} />} />
Problems: callback nesting is hard to read, cannot optimize in shouldComponentUpdate.
Hooks Reuse
function useAuth() {
const [user, setUser] = useState(null);
const login = useCallback(async (credentials) => {
const data = await api.login(credentials);
setUser(data);
}, []);
return { user, login, isLoggedIn: !!user };
}
// Usage: logic and state naturally grouped
function Dashboard() {
const { user, isLoggedIn } = useAuth();
if (!isLoggedIn) return <Navigate to="/login" />;
return <h1>Welcome, {user.name}</h1>;
}
Hooks have become the mainstream modern reuse pattern: no nesting, clear sources, type-safe.
Component Library Design
Design Tokens
Design Tokens are the atomic variables of a design system—colors, spacing, font sizes, shadows, etc.:
:root {
/* Colors */
--color-primary-500: #3b82f6;
--color-danger-500: #ef4444;
/* Spacing */
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 16px;
/* Font sizes */
--font-size-sm: 12px;
--font-size-md: 14px;
--font-size-lg: 18px;
/* Border radius */
--radius-sm: 4px;
--radius-md: 8px;
}
Theme System
// Runtime theme switching
const themes = {
light: { "--color-bg": "#ffffff", "--color-text": "#1f2328" },
dark: { "--color-bg": "#0d1117", "--color-text": "#e6edf3" },
};
function ThemeProvider({ theme, children }) {
return (
<div style={themes[theme]}>
{children}
</div>
);
}
Accessibility (A11y)
function Dialog({ open, onClose, title, children }) {
return (
<div role="dialog" aria-modal="true" aria-labelledby="dialog-title"
hidden={!open}>
<h2 id="dialog-title">{title}</h2>
{children}
<button onClick={onClose} aria-label="Close dialog">✕</button>
</div>
);
}
Key principles: semantic HTML, ARIA attributes, keyboard operability, color contrast compliance.
Storybook-Driven Component Documentation and Testing
Storybook provides an isolated development environment for components:
// Button.stories.jsx
export default {
title: "Components/Button",
component: Button,
argTypes: {
variant: { control: "select", options: ["primary", "secondary", "danger"] },
},
};
export const Primary = {
args: { variant: "primary", children: "Click Me" },
};
export const Disabled = {
args: { variant: "primary", disabled: true, children: "Disabled" },
};
Value of Storybook:
- Visual documentation: Each Story is one usage of the component
- Interaction testing: Combined with Playwright for interaction tests
- Visual regression: Chromatic screenshot comparison, preventing unexpected style changes
- Development isolation: Unaffected by application state, focus on the component itself
Comments