Table of Contents
- Single Responsibility Principle: Keep Components Focused
- Props Design: Clarity, Type Safety, and Flexibility
- Component Composition: Favor Composition Over Inheritance
- State Management: Controlled vs. Uncontrolled Components
- Custom Hooks: Extract Reusable Logic
- Styling Reusable Components: Scoped, Consistent, and Adaptable
- Testing: Ensure Reliability Across Use Cases
- Documentation: Make Components Discoverable and Usable
- Performance Optimization: Avoid Unnecessary Re-renders
- Versioning and Distribution: Share Components with Confidence
- Conclusion
- References
1. Single Responsibility Principle: Keep Components Focused
The Single Responsibility Principle (SRP) states that a component should do one thing and do it well. When a component has a single responsibility, it becomes easier to understand, test, and reuse. Conversely, components with multiple responsibilities (e.g., fetching data, rendering UI, and handling business logic) are brittle and hard to adapt to new use cases.
Example: A Violation of SRP
Consider a UserProfile component that fetches user data, displays the profile, and handles edit functionality:
// ❌ Too many responsibilities: data fetching + rendering + editing
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [isEditing, setIsEditing] = useState(false);
const [formData, setFormData] = useState({});
useEffect(() => {
// Fetch user data (responsibility 1)
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => setUser(data));
}, [userId]);
const handleEdit = () => setIsEditing(true); // Handle editing (responsibility 2)
const handleSubmit = (e) => { /* Save edits */ }; // Handle submission (responsibility 3)
if (!user) return <Spinner />;
return (
<div className="profile">
{isEditing ? (
<form onSubmit={handleSubmit}>
{/* Edit form */}
</form>
) : (
<div>
<h1>{user.name}</h1>
<button onClick={handleEdit}>Edit</button>
</div>
)}
</div>
);
}
This component tries to do everything, making it hard to reuse (e.g., if you need a read-only profile elsewhere).
Refactoring with SRP
Split into focused components:
UserProfileFetcher: Fetches data and passes it to a child component.UserProfileView: Displays user data (read-only).UserProfileEditor: Handles editing logic.
// ✅ Fetches data (single responsibility)
function UserProfileFetcher({ userId, children }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => setUser(data));
}, [userId]);
return children(user); // Pass data to child via render prop
}
// ✅ Displays profile (single responsibility)
function UserProfileView({ user }) {
return (
<div className="profile">
<h1>{user.name}</h1>
<p>Email: {user.email}</p>
</div>
);
}
// ✅ Handles editing (single responsibility)
function UserProfileEditor({ user, onSave }) {
const [formData, setFormData] = useState(user);
const handleSubmit = (e) => {
e.preventDefault();
onSave(formData);
};
return (
<form onSubmit={handleSubmit}>
<input
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
/>
<button type="submit">Save</button>
</form>
);
}
// Usage: Combine components for specific use cases
function App() {
return (
<UserProfileFetcher userId={123}>
{(user) => (
user ? <UserProfileView user={user} /> : <Spinner />
)}
</UserProfileFetcher>
);
}
Now, each component can be reused independently (e.g., UserProfileView in a dashboard, UserProfileEditor in an admin panel).
2. Props Design: Clarity, Type Safety, and Flexibility
Props are the interface of a component—they define how it can be customized. Well-designed props make components intuitive to use and reduce bugs. Key principles include:
Use Clear, Descriptive Prop Names
Avoid ambiguous names like data or config. Instead, use specific names like user, isDisabled, or onSubmit.
Enforce Type Safety with PropTypes or TypeScript
Unvalidated props are a common source of bugs. Use PropTypes (for JavaScript) or TypeScript to define expected prop types.
Example with PropTypes
import PropTypes from 'prop-types';
function Button({ label, variant, isDisabled, onClick }) {
return (
<button
className={`btn btn-${variant}`}
disabled={isDisabled}
onClick={onClick}
>
{label}
</button>
);
}
Button.propTypes = {
label: PropTypes.string.isRequired, // Required string
variant: PropTypes.oneOf(['primary', 'secondary', 'danger']).isRequired, // Restricted values
isDisabled: PropTypes.bool, // Optional boolean (defaults to false)
onClick: PropTypes.func.isRequired, // Required function
};
Button.defaultProps = {
isDisabled: false, // Default value
};
Example with TypeScript
TypeScript provides stricter type safety and better IDE support:
type ButtonVariant = 'primary' | 'secondary' | 'danger';
interface ButtonProps {
label: string;
variant: ButtonVariant;
isDisabled?: boolean; // Optional prop
onClick: () => void;
}
function Button({ label, variant, isDisabled = false, onClick }: ButtonProps) {
return (
<button
className={`btn btn-${variant}`}
disabled={isDisabled}
onClick={onClick}
>
{label}
</button>
);
}
Avoid Prop Drilling
Prop drilling (passing props through multiple layers of components) makes code hard to maintain. Use composition (via children or render props) or context to share data without drilling.
Example: Using children to Avoid Drilling
Instead of passing user through intermediate components:
// ❌ Prop drilling
function App() {
const user = { name: "Alice" };
return <Header user={user} />;
}
function Header({ user }) {
return <Nav user={user} />; // Unnecessary pass-through
}
function Nav({ user }) {
return <UserMenu user={user} />; // Finally used here
}
Use children to inject the UserMenu directly:
// ✅ Composition with children
function App() {
const user = { name: "Alice" };
return (
<Header>
<UserMenu user={user} /> {/* Inject UserMenu directly */}
</Header>
);
}
function Header({ children }) {
return (
<header>
<Logo />
{children} {/* Children are rendered here */}
</header>
);
}
3. Component Composition: Favor Composition Over Inheritance
React encourages composition over inheritance for code reuse. Composition lets you build complex components by combining simpler ones, whereas inheritance creates tight coupling and limits flexibility.
Key Composition Patterns
1. children Prop
The children prop allows a component to wrap and render arbitrary content, making it highly flexible.
Example: A reusable Card component:
function Card({ children, title }) {
return (
<div className="card">
{title && <h2 className="card-title">{title}</h2>}
<div className="card-body">{children}</div> {/* Arbitrary content */}
</div>
);
}
// Usage: Different content, same Card structure
function ProfileCard() {
return (
<Card title="User Profile">
<p>Name: Alice</p>
<p>Email: [email protected]</p>
</Card>
);
}
function ProductCard() {
return (
<Card title="Wireless Headphones">
<img src="/headphones.jpg" alt="Product" />
<p>Price: $99.99</p>
</Card>
);
}
2. Render Props
A render prop is a function prop that a component uses to render content. This is useful when components need to share logic but render different UIs.
Example: A MouseTracker component that shares mouse position logic:
// Render prop component: shares mouse position logic
function MouseTracker({ render }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (e) => setPosition({ x: e.clientX, y: e.clientY });
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
return render(position); // Call render prop with position
}
// Usage: Render different UIs with the same logic
function MouseCoordinates() {
return (
<MouseTracker render={(position) => (
<div>Mouse position: ({position.x}, {position.y})</div>
)} />
);
}
function MouseFollower() {
return (
<MouseTracker render={(position) => (
<div
style={{
position: 'absolute',
left: position.x,
top: position.y,
width: '20px',
height: '20px',
backgroundColor: 'red'
}}
/>
)} />
);
}
4. State Management: Controlled vs. Uncontrolled Components
Reusable components often need to manage state, but deciding where to put that state is critical. React components can be controlled (state managed by parent) or uncontrolled (state managed internally).
Controlled Components
A controlled component receives its state via props and notifies the parent of changes via a callback (e.g., onChange). This gives the parent full control.
Example: A controlled Input component:
function Input({ value, onChange, placeholder }) {
return (
<input
type="text"
value={value} // State from parent
onChange={(e) => onChange(e.target.value)} // Notify parent of changes
placeholder={placeholder}
/>
);
}
// Usage: Parent manages state
function Form() {
const [username, setUsername] = useState('');
return (
<Input
value={username}
onChange={setUsername}
placeholder="Enter username"
/>
);
}
Uncontrolled Components
An uncontrolled component manages its own state internally, exposing it via a ref. Use this when the parent doesn’t need to sync with the component’s state.
Example: An uncontrolled FileUpload component:
function FileUpload({ onUpload }) {
const fileInputRef = useRef(null);
const handleClick = () => fileInputRef.current.click();
const handleFileChange = (e) => {
if (e.target.files[0]) {
onUpload(e.target.files[0]);
}
};
return (
<div>
<button onClick={handleClick}>Upload File</button>
<input
type="file"
ref={fileInputRef}
style={{ display: 'none' }}
onChange={handleFileChange}
/>
</div>
);
}
When to Use Which?
- Controlled: Use when the parent needs to validate, persist, or sync state (e.g., form inputs).
- Uncontrolled: Use for one-off interactions (e.g., file uploads) or when the parent doesn’t need to manage the state.
5. Custom Hooks: Extract Reusable Logic
Custom hooks let you extract component logic into reusable functions. They are ideal for sharing logic across components (e.g., form handling, data fetching, or animation).
Example: useForm Hook
Extract form logic into a hook to reuse across form components:
// Custom hook for form management
function useForm(initialValues) {
const [values, setValues] = useState(initialValues);
const handleChange = (e) => {
const { name, value } = e.target;
setValues({ ...values, [name]: value });
};
const resetForm = () => setValues(initialValues);
return { values, handleChange, resetForm };
}
// Usage in a component
function LoginForm() {
const { values, handleChange, resetForm } = useForm({
email: '',
password: '',
});
const handleSubmit = (e) => {
e.preventDefault();
console.log('Submitted:', values);
resetForm();
};
return (
<form onSubmit={handleSubmit}>
<input
name="email"
value={values.email}
onChange={handleChange}
placeholder="Email"
/>
<input
name="password"
type="password"
value={values.password}
onChange={handleChange}
placeholder="Password"
/>
<button type="submit">Login</button>
</form>
);
}
Now, useForm can be reused in SignupForm, CheckoutForm, etc.
6. Styling Reusable Components: Scoped, Consistent, and Adaptable
Styling reusable components requires balancing scoping (to avoid conflicts), consistency (across the app), and adaptability (to different themes or contexts).
Popular Styling Approaches
1. CSS Modules
CSS Modules scope styles locally by default, preventing class name collisions.
Example: Button.module.css
/* Scoped to Button component */
.button {
padding: 8px 16px;
border-radius: 4px;
border: none;
cursor: pointer;
}
.primary {
background: blue;
color: white;
}
.secondary {
background: gray;
color: black;
}
Button.jsx:
import styles from './Button.module.css';
function Button({ label, variant = 'primary' }) {
return (
<button className={`${styles.button} ${styles[variant]}`}>
{label}
</button>
);
}
2. Styled Components
Styled Components lets you write CSS-in-JS, with styles tied directly to components. It supports dynamic styling via props.
Example:
import styled from 'styled-components';
const StyledButton = styled.button`
padding: 8px 16px;
border-radius: 4px;
border: none;
cursor: pointer;
/* Dynamic styles based on props */
background: ${(props) =>
props.variant === 'primary' ? 'blue' :
props.variant === 'secondary' ? 'gray' : 'red'};
color: ${(props) => props.variant === 'primary' ? 'white' : 'black'};
`;
function Button({ label, variant = 'primary' }) {
return <StyledButton variant={variant}>{label}</StyledButton>;
}
3. Utility-First (Tailwind CSS)
Tailwind provides utility classes for rapid styling. It’s highly customizable but requires discipline to avoid messy class lists.
Example:
function Button({ label, variant = 'primary' }) {
const baseClasses = "px-4 py-2 rounded border-none cursor-pointer";
const variantClasses = {
primary: "bg-blue-500 text-white",
secondary: "bg-gray-500 text-black",
}[variant];
return <button className={`${baseClasses} ${variantClasses}`}>{label}</button>;
}
7. Testing: Ensure Reliability Across Use Cases
Reusable components are used in multiple contexts, so testing is critical to ensure they behave as expected. Use Jest for test running and React Testing Library for rendering components and simulating user interactions.
Example: Testing a Button Component
Test cases to cover:
- Renders the label correctly.
- Applies the correct variant class.
- Calls
onClickwhen clicked.
import { render, screen, fireEvent } from '@testing-library/react';
import Button from './Button';
describe('Button', () => {
test('renders label correctly', () => {
render(<Button label="Click Me" variant="primary" onClick={() => {}} />);
expect(screen.getByText('Click Me')).toBeInTheDocument();
});
test('applies variant class', () => {
const { rerender } = render(
<Button label="Primary" variant="primary" onClick={() => {}} />
);
expect(screen.getByText('Primary')).toHaveClass('btn-primary');
// Test secondary variant
rerender(<Button label="Secondary" variant="secondary" onClick={() => {}} />);
expect(screen.getByText('Secondary')).toHaveClass('btn-secondary');
});
test('calls onClick when clicked', () => {
const handleClick = jest.fn();
render(<Button label="Click" variant="primary" onClick={handleClick} />);
fireEvent.click(screen.getByText('Click'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
});
8. Documentation: Make Components Discoverable and Usable
Even the best reusable components are useless if no one knows how to use them. Documentation should explain:
- Purpose and use cases.
- Props (types, defaults, required status).
- Examples of common usage.
- Edge cases or limitations.
Tools for Documentation
1. Storybook
Storybook is a tool for building and documenting UI components in isolation. It lets you showcase variants, interact with components, and generate docs.
Example Storybook story for Button:
// Button.stories.jsx
import Button from './Button';
export default {
title: 'Components/Button',
component: Button,
argTypes: {
variant: {
control: { type: 'select', options: ['primary', 'secondary', 'danger'] },
},
},
};
export const Primary = (args) => <Button {...args} />;
Primary.args = { label: 'Primary Button', variant: 'primary' };
export const Secondary = (args) => <Button {...args} />;
Secondary.args = { label: 'Secondary Button', variant: 'secondary' };
2. README Files
Include a README.md with:
- A description of the component.
- Props table (use
react-docgento auto-generate). - Code examples.
Example README.md snippet:
# Button
A reusable button component with multiple variants.
## Props
| Name | Type | Default | Required | Description |
|------------|-----------------------|-----------|----------|----------------------------|
| label | string | - | Yes | Text to display on the button. |
| variant | 'primary' \| 'secondary' \| 'danger' | 'primary' | No | Button style variant. |
| isDisabled | boolean | false | No | Whether the button is disabled. |
| onClick | () => void | - | Yes | Callback when button is clicked. |
## Examples
### Primary Button
```jsx
<Button label="Submit" variant="primary" onClick={() => alert('Submitted!')} />
## 9. Performance Optimization: Avoid Unnecessary Re-renders
Reusable components are often rendered frequently, so optimizing performance is key. Use these tools to prevent unnecessary re-renders:
### `React.memo`
Memoize functional components to skip re-renders if props haven’t changed.
```jsx
const Button = React.memo(function Button({ label, onClick }) {
console.log('Button re-rendered'); // Only logs if label/onClick change
return <button onClick={onClick}>{label}</button>;
});
useMemo and useCallback
useMemo: Memoize expensive calculations.useCallback: Memoize functions passed as props to memoized components (prevents them from being recreated on every render).
Example:
function Parent() {
const [count, setCount] = useState(0);
// Memoize the callback to avoid re-rendering Button
const handleClick = useCallback(() => {
console.log('Button clicked');
}, []); // Empty dependency array: callback never changes
return (
<div>
<Button label="Click" onClick={handleClick} />
<button onClick={() => setCount(count + 1)}>Increment {count}</button>
</div>
);
}
10. Versioning and Distribution: Share Components with Confidence
If you’re sharing components across projects or teams, package them for distribution. Use npm or Yarn to publish, and follow semantic versioning (SemVer) to communicate changes.
Key Steps for Distribution
-
Bundle the Component: Use tools like
rolluportsupto bundle into ES modules (ESM) and CommonJS (CJS).Example
rollup.config.js:export default { input: 'src/index.ts', output: [ { file: 'dist/index.js', format: 'cjs' }, { file: 'dist/index.esm.js', format: 'esm' }, ], external: ['react'], // Mark react as external (consumer provides it) }; -
Semantic Versioning:
MAJOR(1.0.0): Breaking changes.MINOR(0.1.0): New features, backward-compatible.PATCH(0.0.1): Bug fixes, backward-compatible.
-
Peer Dependencies: Specify
reactandreact-domas peer dependencies to avoid duplicate React instances.Example
package.json:{ "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }
Conclusion
Creating reusable components in React is a skill that pays dividends in maintainability, scalability, and developer productivity. By following these best practices—focusing on single responsibility, designing clear props, using composition, extracting logic with custom hooks, testing rigorously, and documenting thoroughly—you can build components that are flexible, reliable, and a joy to use.
Remember, reusability is not about writing components that work for every scenario, but rather components that can be easily adapted to most scenarios. Start small, iterate based on usage, and prioritize clarity over complexity.
References
- React Official Docs: Components and Props
- React Official Docs: Composition vs Inheritance
- PropTypes
- TypeScript for React
- Storybook
- React Testing Library
- Jest
- Semantic Versioning
- Rollup.js<|FCResponseEnd|># Creating Reusable Components in React: A Best Practices Guide
Introduction
React’s component-based architecture has revolutionized how we build user interfaces, emphasizing modularity, reusability, and maintainability. At the heart of this paradigm lies the concept of reusable components—self-contained, flexible building blocks that can be shared across projects, teams, and even organizations.
Well-designed reusable components reduce redundancy, enforce consistency, and accelerate development. However, creating truly reusable components requires more than just writing functional code; it demands careful consideration of props, state, composition, styling, testing, and documentation. Without these best practices, components can become rigid, difficult to maintain, or overly specific to a single use case.
In this guide, we’ll explore the key principles and actionable strategies for building reusable React components that stand the test of time. Whether you’re a beginner looking to level up your component design or a seasoned developer aiming to standardize your team’s workflow, this article will provide the tools you need to create components that are flexible, predictable, and easy to use.
Table of Contents
- Single Responsibility Principle: Keep Components Focused
- Props Design: Clarity, Type Safety, and Flexibility
- Component Composition: Favor Composition Over Inheritance
- State Management: Controlled vs. Uncontrolled Components
- Custom Hooks: Extract Reusable Logic
- Styling Reusable Components: Scoped, Consistent, and Adaptable
- Testing: Ensure Reliability Across Use Cases
- Documentation: Make Components Discoverable and Usable
- Performance Optimization: Avoid Unnecessary Re-renders
- Versioning and Distribution: Share Components with Confidence
- Conclusion
- References
1. Single Responsibility Principle: Keep Components Focused
The Single Responsibility Principle (SRP) states that a component should do one thing and do it well. When a component has a single responsibility, it becomes easier to understand, test, and reuse. Conversely, components with multiple responsibilities (e.g., fetching data, rendering UI, and handling business logic) are brittle and hard to adapt to new use cases.
Example: A Violation of SRP
Consider a UserProfile component that fetches user data, displays the profile, and handles edit functionality:
// ❌ Too many responsibilities: data fetching + rendering + editing
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [isEditing, setIsEditing] = useState(false);
const [formData, setFormData] = useState({});
useEffect(() => {
// Fetch user data (responsibility 1)
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => setUser(data));
}, [userId]);
const handleEdit = () => setIsEditing(true); // Handle editing (responsibility 2)
const handleSubmit = (e) => { /* Save edits */ }; // Handle submission (responsibility 3)
if (!user) return <Spinner />;
return (
<div className="profile">
{isEditing ? (
<form onSubmit={handleSubmit}>
{/* Edit form */}
</form>
) : (
<div>
<h1>{user.name}</h1>
<button onClick={handleEdit}>Edit</button>
</div>
)}
</div>
);
}
This component tries to do everything, making it hard to reuse (e.g., if you need a read-only profile elsewhere).
Refactoring with SRP
Split into focused components:
UserProfileFetcher: Fetches data and passes it to a child component.UserProfileView: Displays user data (read-only).UserProfileEditor: Handles editing logic.
// ✅ Fetches data (single responsibility)
function UserProfileFetcher({ userId, children }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => setUser(data));
}, [userId]);
return children(user); // Pass data to child via render prop
}
// ✅ Displays profile (single responsibility)
function UserProfileView({ user }) {
return (
<div className="profile">
<h1>{user.name}</h1>
<p>Email: {user.email}</p>
</div>
);
}
// ✅ Handles editing (single responsibility)
function UserProfileEditor({ user, onSave }) {
const [formData, setFormData] = useState(user);
const handleSubmit = (e) => {
e.preventDefault();
onSave(formData);
};
return (
<form onSubmit={handleSubmit}>
<input
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
/>
<button type="submit">Save</button>
</form>
);
}
// Usage: Combine components for specific use cases
function App() {
return (
<UserProfileFetcher userId={123}>
{(user) => (
user ? <UserProfileView user={user} /> : <Spinner />
)}
</UserProfileFetcher>
);
}
Now, each component can be reused independently (e.g., UserProfileView in a dashboard, UserProfileEditor in an admin panel).
2. Props Design: Clarity, Type Safety, and Flexibility
Props are the interface of a component—they define how it can be customized. Well-designed props make components intuitive to use and reduce bugs. Key principles include:
Use Clear, Descriptive Prop Names
Avoid ambiguous names like data or config. Instead, use specific names like user, isDisabled, or onSubmit.
Enforce Type Safety with PropTypes or TypeScript
Unvalidated props are a common source of bugs. Use PropTypes (for JavaScript) or TypeScript to define expected prop types.
Example with PropTypes
import PropTypes from 'prop-types';
function Button({ label, variant, isDisabled, onClick }) {
return (
<button
className={`btn btn-${variant}`}
disabled={isDisabled}
onClick={onClick}
>
{label}
</button>
);
}
Button.propTypes = {
label: PropTypes.string.isRequired, // Required string
variant: PropTypes.oneOf(['primary', 'secondary', 'danger']).isRequired, // Restricted values
isDisabled: PropTypes.bool, // Optional boolean (defaults to false)
onClick: PropTypes.func.isRequired, // Required function
};
Button.defaultProps = {
isDisabled: false, // Default value
};
Example with TypeScript
TypeScript provides stricter type safety and better IDE support:
type ButtonVariant = 'primary' | 'secondary' | 'danger';
interface ButtonProps {
label: string;
variant: ButtonVariant;
isDisabled?: boolean; // Optional prop
onClick: () => void;
}
function Button({ label, variant, isDisabled = false, onClick }: ButtonProps) {
return (
<button
className={`btn btn-${variant}`}
disabled={isDisabled}
onClick={onClick}
>
{label}
</button>
);
}
Avoid Prop Drilling
Prop drilling (passing props through multiple layers of components) makes code hard to maintain. Use composition (via children or render props) or context to share data without drilling.
Example: Using children to Avoid Drilling
Instead of passing user through intermediate components:
// ❌ Prop drilling
function App() {
const user = { name: "Alice" };
return <Header user={user} />;
}
function Header({ user }) {
return <Nav user={user} />; // Unnecessary pass-through
}
function Nav({ user }) {
return <UserMenu user={user} />; // Finally used here
}
Use children to inject the UserMenu directly:
// ✅ Composition with children
function App() {
const user = { name: "Alice" };
return (
<Header>
<UserMenu user={user} /> {/* Inject UserMenu directly */}
</Header>
);
}
function Header({ children }) {
return (
<header>
<Logo />
{children} {/* Children are rendered here */}
</header>
);
}
3. Component Composition: Favor Composition Over Inheritance
React encourages composition over inheritance for code reuse. Composition lets you build complex components by combining simpler ones, whereas inheritance creates tight coupling and limits flexibility.
Key Composition Patterns
1. children Prop
The children prop allows a component to wrap and render arbitrary content, making it highly flexible.
Example: A reusable Card component:
function Card({ children, title }) {
return (
<div className="card">
{title && <h2 className="card-title">{title}</h2>}
<div className="card-body">{children}</div> {/* Arbitrary content */}
</div>
);
}
// Usage: Different content, same Card structure
function ProfileCard() {
return (
<Card title="User Profile">
<p>Name: Alice</p>
<p>Email: [email protected]</p>
</Card>
);
}
function ProductCard() {
return (
<Card title="Wireless Headphones">
<img src="/headphones.jpg" alt="Product" />
<p>Price: $99.99</p>
</Card>
);
}
2. Render Props
A render prop is a function prop that a component uses to render content. This is useful when components need to share logic but render different UIs.
Example: A MouseTracker component that shares mouse position logic:
// Render prop component: shares mouse position logic
function MouseTracker({ render }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (e) => setPosition({ x: e.clientX, y: e.clientY });
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
return render(position); // Call render prop with position
}
// Usage: Render different UIs with the same logic
function MouseCoordinates() {
return (
<MouseTracker render={(position) => (
<div>Mouse position: ({position.x}, {position.y})</div>
)} />
);
}
function MouseFollower() {
return (
<MouseTracker render={(position) => (
<div
style={{
position: 'absolute',
left: position.x,
top: position.y,
width: '20px',
height: '20px',
backgroundColor: 'red'
}}
/>
)} />
);
}
4. State Management: Controlled vs. Uncontrolled Components
Reusable components often need to manage state, but deciding where to put that state is critical. React components can be controlled (state managed by parent) or uncontrolled (state managed internally).
Controlled Components
A controlled component receives its state via props and notifies the parent of changes via a callback (e.g., onChange). This gives the parent full control.
Example: A controlled Input component:
function Input({ value, onChange, placeholder }) {
return (
<input
type="text"
value={value} // State from parent
onChange={(e) => onChange(e.target.value)} // Notify parent of changes
placeholder={placeholder}
/>
);
}
// Usage: Parent manages state
function Form() {
const [username, setUsername] = useState('');
return (
<Input
value={username}
onChange={setUsername}
placeholder="Enter username"
/>
);
}
Uncontrolled Components
An uncontrolled component manages its own state internally, exposing it via a ref. Use this when the parent doesn’t need to sync with the component’s state.
Example: An uncontrolled FileUpload component:
function FileUpload({ onUpload }) {
const fileInputRef = useRef(null);
const handleClick = () => fileInputRef.current.click();
const handleFileChange = (e) => {
if (e.target.files[0]) {
onUpload(e.target.files[0]);
}
};
return (
<div>
<button onClick={handleClick}>Upload File</button>
<input
type="file"
ref={fileInputRef}
style={{ display: 'none' }}
onChange={handleFileChange}
/>
</div>
);
}
When to Use Which?
- Controlled: Use when the parent needs to validate, persist, or sync state (e.g., form inputs).
- Uncontrolled: Use for one-off interactions (e.g., file uploads) or when the parent doesn’t need to manage the state.
5. Custom Hooks: Extract Reusable Logic
Custom hooks let you extract component logic into reusable functions. They are ideal for sharing logic across components (e.g., form handling, data fetching, or animation).
Example: useForm Hook
Extract form logic into a hook to reuse across form components:
// Custom hook for form management
function useForm(initialValues) {
const [values, setValues] = useState(initialValues);
const handleChange = (e) => {
const { name, value } = e.target;
setValues({ ...values, [name]: value });
};
const resetForm = () => setValues(initialValues);
return { values, handleChange, resetForm };
}
// Usage in a component
function LoginForm() {
const { values, handleChange, resetForm } = useForm({
email: '',
password: '',
});
const handleSubmit = (e) => {
e.preventDefault();
console.log('Submitted:', values);
resetForm();
};
return (
<form onSubmit={handleSubmit}>
<input
name="email"
value={values.email}
onChange={handleChange}
placeholder="Email"
/>
<input
name="password"
type="password"
value={values.password}
onChange={handleChange}
placeholder="Password"
/>
<button type="submit">Login</button>
</form>
);
}
Now, useForm can be reused in SignupForm, CheckoutForm, etc.
6. Styling Reusable Components: Scoped, Consistent, and Adaptable
Styling reusable components requires balancing scoping (to avoid conflicts), consistency (across the app), and adaptability (to different themes or contexts).
Popular Styling Approaches
1. CSS Modules
CSS Modules scope styles locally by default, preventing class name collisions.
Example: Button.module.css
/* Scoped to Button component */
.button {
padding: 8px 16px;
border-radius: 4px;
border: none;
cursor: pointer;
}
.primary {
background: blue;
color: white;
}
.secondary {
background: gray;
color: black;
}
Button.jsx:
import styles from './Button.module.css';
function Button({ label, variant = 'primary' }) {
return (
<