javascriptroom guide

Creating Reusable Components in React: A Best Practices Guide

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

  1. Single Responsibility Principle: Keep Components Focused
  2. Props Design: Clarity, Type Safety, and Flexibility
  3. Component Composition: Favor Composition Over Inheritance
  4. State Management: Controlled vs. Uncontrolled Components
  5. Custom Hooks: Extract Reusable Logic
  6. Styling Reusable Components: Scoped, Consistent, and Adaptable
  7. Testing: Ensure Reliability Across Use Cases
  8. Documentation: Make Components Discoverable and Usable
  9. Performance Optimization: Avoid Unnecessary Re-renders
  10. Versioning and Distribution: Share Components with Confidence
  11. Conclusion
  12. 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).

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 onClick when 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-docgen to 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

  1. Bundle the Component: Use tools like rollup or tsup to 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)
    };
  2. Semantic Versioning:

    • MAJOR (1.0.0): Breaking changes.
    • MINOR (0.1.0): New features, backward-compatible.
    • PATCH (0.0.1): Bug fixes, backward-compatible.
  3. Peer Dependencies: Specify react and react-dom as 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

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

  1. Single Responsibility Principle: Keep Components Focused
  2. Props Design: Clarity, Type Safety, and Flexibility
  3. Component Composition: Favor Composition Over Inheritance
  4. State Management: Controlled vs. Uncontrolled Components
  5. Custom Hooks: Extract Reusable Logic
  6. Styling Reusable Components: Scoped, Consistent, and Adaptable
  7. Testing: Ensure Reliability Across Use Cases
  8. Documentation: Make Components Discoverable and Usable
  9. Performance Optimization: Avoid Unnecessary Re-renders
  10. Versioning and Distribution: Share Components with Confidence
  11. Conclusion
  12. 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).

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 (
    <