Frontend Development 18 min read

Five React Component Design Patterns: Compound, Controlled, Custom Hooks, Props Getters, and State Reducer

The article reviews five React component design patterns—Compound, Controlled, Custom Hooks, Props Getters, and State Reducer—explaining how each improves reusability, API simplicity, and state management while weighing their advantages, disadvantages, and ideal use‑cases for scalable, maintainable UI development.

Sohu Tech Products
Sohu Tech Products
Sohu Tech Products
Five React Component Design Patterns: Compound, Controlled, Custom Hooks, Props Getters, and State Reducer

As a React developer, you may face several common problems:

How to build a highly reusable component that can adapt to different business scenarios?

How to create a component with a simple API that is easy to use?

How to build a component that is scalable both in UI and functionality?

To solve these issues, five React component design patterns are introduced and their advantages and disadvantages are compared.

1. Compound Component Pattern

The compound component pattern creates more complex components by composing several simple components. Each simple component handles a specific piece of functionality, keeping the logic separated and making the overall component highly reusable and feature‑complete.

If you need a highly customizable component with an easy‑to‑understand API , this pattern avoids deep Props passing.

import React, { useState } from 'react';

// Basic button component
const Button = ({ label, onClick }) => (
{label}
);

// Basic text input component
const TextBox = ({ value, onChange }) => (
);

// Compound component
const LoginPanel = () => {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');

  const handleLogin = () => {
    console.log(`Logging in with ${username} and ${password}`);
  };

  return (
setUsername(e.target.value)} />
setPassword(e.target.value)} />
);
};

// Usage example
const App = () => (
);

export default App;

In this example, LoginPanel is a compound component that contains two basic TextBox components and a Button with login logic.

Advantages:

Reduced API complexity: Props are passed directly to the appropriate child components instead of being packed into a single container.

High reusability: Basic components can be reused across many scenarios.

Logic separation: Each basic component focuses on a single task.

Component count increase: More component layers can increase JSX lines and make the code more complex.

Not suitable for all cases: For simple scenarios, introducing a compound component may be unnecessary.

Applicable scenarios:

Forms and form fields: Split a form into multiple field components, each handling its own input or validation logic.

Dialogs and modals: Separate title, content, and action buttons into independent components for reuse.

2. Controlled Component Pattern

The controlled component pattern turns a component into a controlled one by letting external Props drive its internal state. It is commonly used in form components.

import React, { useState } from 'react';

const Button = ({ label, onClick }) => (
{label}
);

const TextBox = ({ value, onChange }) => (
);

// Controlled compound component
const ControlledLoginPanel = () => {
  const [loginData, setLoginData] = useState({ username: '', password: '' });

  const handleInputChange = (e) => {
    const { name, value } = e.target;
    setLoginData(prev => ({ ...prev, [name]: value }));
  };

  const handleLogin = () => {
    console.log(`Logging in with ${loginData.username} and ${loginData.password}`);
  };

  return (
);
};

// Usage example
const App = () => (
);

export default App;

Here, ControlledLoginPanel is a controlled component whose input values are managed by React state.

Advantages:

More control: Internal state is exposed, allowing external manipulation.

Consistency and predictability: State is a single source of truth, making behavior easier to reason about.

Disadvantages:

More boilerplate: Each input requires its own state and handler, increasing code size.

Performance overhead: Every input change triggers a state update and re‑render.

Not suitable for simple forms: The added complexity can be overkill for straightforward use cases.

Applicable scenarios:

Dynamic form elements: When you need to add or remove fields dynamically.

Modal control: When the visibility of a modal is driven by props.

3. Custom Hooks Pattern

Custom Hooks extract component logic into reusable functions. The hook provides state, handlers, and other internal logic, while the component focuses on rendering.

import React, { useState } from 'react';

const Button = ({ label, onClick }) => (
{label}
);

const TextBox = ({ value, onChange, placeholder }) => (
);

// Custom hook handling login form logic
const useLoginForm = () => {
  const [loginData, setLoginData] = useState({ username: '', password: '' });

  const handleInputChange = (e) => {
    const { name, value } = e.target;
    setLoginData(prev => ({ ...prev, [name]: value }));
  };

  const handleLogin = () => {
    console.log(`使用用户名 ${loginData.username} 和密码 ${loginData.password} 登录`);
  };

  return { loginData, handleInputChange, handleLogin };
};

// Component using the custom hook
const ControlledLoginPanel = () => {
  const { loginData, handleInputChange, handleLogin } = useLoginForm();

  return (
);
};

// Usage example
const App = () => (
);

export default App;

The hook useLoginForm encapsulates state and handlers, allowing the component to stay clean and UI‑focused.

Advantages:

Logic reuse: Hooks can be shared across multiple components.

Simpler components: Rendering code is separated from business logic.

Disadvantages:

Higher implementation complexity: Understanding the split between hook and component may require extra effort.

Applicable scenarios:

Data fetching and processing logic that needs to be shared.

Encapsulating side‑effects for better maintainability.

4. Props Getters Pattern

Props Getters simplify the integration of hook‑based logic by returning a set of props through a getter function. The component receives these getters and spreads them onto the appropriate JSX elements.

import React, { useState } from 'react';

const Button = ({ getLabel, handleClick }) => (
{getLabel()}
);

const TextBox = ({ getValue, onChange, placeholder }) => (
);

const ControlledLoginPanel = ({ getUsernameProps, getPasswordProps, handleLogin }) => (
'Login'} handleClick={handleLogin} />
);

// Hook providing getters
const useLoginForm = () => {
  const [loginData, setLoginData] = useState({ username: '', password: '' });

  const handleInputChange = name => e => {
    const { value } = e.target;
    setLoginData(prev => ({ ...prev, [name]: value }));
  };

  const handleLogin = () => {
    console.log(`Logging in with ${loginData.username} and ${loginData.password}`);
  };

  const getUsernameProps = () => ({
    getValue: () => loginData.username,
    onChange: handleInputChange('username'),
  });

  const getPasswordProps = () => ({
    getValue: () => loginData.password,
    onChange: handleInputChange('password'),
  });

  return { getUsernameProps, getPasswordProps, handleLogin };
};

// Usage example
const App = () => {
  const { getUsernameProps, getPasswordProps, handleLogin } = useLoginForm();
  return (
);
};

export default App;

This pattern reduces nesting depth and improves component separation, but it introduces additional abstraction.

Advantages:

Ease of use: Developers only need to pass the getter to the correct JSX element.

Separation of concerns: Components receive only the data they need, keeping logic outside.

Flatter component hierarchy: Compared with pure hook patterns, nesting can be reduced.

Disadvantages:

Reduced visibility: Getters hide the underlying implementation, making the component feel like a black box.

More callbacks: The pattern may introduce many small functions, increasing perceived complexity.

External dependencies: Components rely on externally provided getters, which can affect encapsulation.

Applicable scenarios:

Data filtering in display components.

Form validation logic that should be invoked from outside the component.

5. State Reducer Pattern

The State Reducer pattern delegates state‑update logic to a reducer function, offering fine‑grained control over complex state transitions.

import React, { useState } from 'react';

const TextInput = ({ getInputProps }) => {
  const inputProps = getInputProps();
  return
;
};

const StateReducerExample = () => {
  const [inputValue, setInputValue] = useState('');

  const stateReducer = (state, changes) => {
    switch (Object.keys(changes)[0]) {
      case 'value':
        if (changes.value.length > 10) {
          return state; // Prevent change if length > 10
        }
        break;
      default:
        break;
    }
    return { ...state, ...changes };
  };

  const getInputProps = () => ({
    value: inputValue,
    onChange: e => setInputValue(stateReducer({ value: e.target.value })),
  });

  return (
State Reducer Example
);
};

export default StateReducerExample;

In this example, StateReducerExample limits the input length to 10 characters via the stateReducer function.

Advantages:

Flexible state management: Custom reducer functions enable sophisticated state handling.

Better code organization: Centralizing state logic makes the component easier to maintain.

Clear update flow: The reducer makes each state transition explicit.

Disadvantages:

Added complexity: Introducing a reducer can make the code harder to follow, especially with many cases.

Potential redundancy: Simple state changes may require boilerplate.

Overkill for simple scenarios: The pattern may be unnecessary for straightforward components.

Applicable scenarios:

Complex state management with multiple interrelated values.

Asynchronous state updates that need coordinated handling.

Situations where you want to control or block state updates based on conditions.

Conclusion

These five React component design patterns illustrate the trade‑off between flexibility (control) and complexity. The more flexible a component, the more powerful its functionality, but also the higher its complexity. Developers should choose the pattern that best fits their business logic and target audience.

Reference

React Component Design Patterns

Design PatternsfrontendJavaScriptState ManagementReactHooksComponent Design
Sohu Tech Products
Written by

Sohu Tech Products

A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.