paint-brush
Mastering React Component Design for Reusable UI Components by@saurabhd
215 reads

Mastering React Component Design for Reusable UI Components

by Saurabh DhariwalDecember 1st, 2024
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Learn essential React component design patterns for creating reusable, maintainable UI components. Explore presentational vs. container components, higher-order components, render props, compound components, and hooks to enhance your React development efficiency.
featured image - Mastering React Component Design for Reusable UI Components
Saurabh Dhariwal HackerNoon profile picture

React has completely changed the process of building user interfaces for developers. In a nutshell, React teaches us the concept of components—namely, self-contained, reusable pieces of the UI. Designing reusable but maintainable and scalable components is the nucleus of any React application. In this blog post, we are going to examine several key component design patterns that will help you build sturdy and reusable UI components.


Component design patterns are standardized methods of creating and organizing components to solve specific problems in UI design and architecture. These patterns offer a structured way of managing component logic, data flow, and UI composition to make it easier to build complex applications with a clean and maintainable codebase.

1. Presentational and Container Components

One of the basic design patterns in React is separating presentational and container components.

Presentational Components

Presentational components are mainly concerned with how things look. They are mostly stateless and focus on rendering UI based on props. These components receive data and callbacks as props and do not handle data-fetching logic or state management.

Example:

const Button = ({ label, onClick }) => {
  //
return <button onClick={onClick}>{label}</button>;
};

Container Components

Container components manage the state and logic of your application. They fetch data, handle stateful logic, and pass data down to presentational components.

Example:

class ButtonContainer extends React.Component {
    handleClick = () => {
        console.log('Button clicked!');
    };

    render()
return <Button label="Click Me" onClick={this.handleClick} />;
    }
}

Benefits

  • Separation of Concerns: This pattern makes your components easier to reason about by separating UI from business logic.
  • Reusability: Presentational components can be reused across different parts of the application without being tied to specific logic.

2. Higher-Order Components (HOCs)

Higher-order components are a high-level pattern for the reuse of the logic of a component. A higher-order component is a function that accepts a component and returns a new component, maybe with extended functionality.

Example

Imagine you wish to add loading functionality. You can create an HOC to handle the loading state.

const withLoading = (WrappedComponent) => {
    return class extends React.Component {
``
state = { loading: true };

        componentDidMount() {
            setTimeout(() => this.setState({ loading: false }), 2000); // Simulate API call
        }

        render()
return this.state.loading? <div>Loading.</div> : <WrappedComponent {.this.props} />;
        }
    };
};

const MyComponent = () => <div>Data Loaded!</div>;
const MyComponentWithLoading = withLoading(MyComponent);

Advantages

  • Code Reusability: HOCs allow shared logic without changing the original component.
  • Separation of Concerns: Logic for scenarios like data fetching, state management, or subscription can be encapsulated within HOCs.

3. Render Props

Render props is a pattern for sharing code between components using a prop that is a function. This function will return a React element, allowing for greater flexibility.

Example:

You can create a simple counter that uses render props to control the displayed count.

class Counter extends React.Component {
state = { count: 0 };

    increment = () => {
        this.setState((prevState) => ({ count: prevState.count + 1 }));
    };

    render() {
        return this.props.render(this.state.count, this.increment);
    }
}

const App = () => (
    <Counter render={(count, increment) => (
<div>
            <p>Count: {count}</p>
            <button onClick={increment}>Increment</button>
        </div>
    )} />
);

Benefits

  • Flexibility: The render props pattern allows for greater flexibility as components can control their rendering logic.
  • Shared Behavior: Easily shares stateful logic without modifying the component structure.

4. Compound Components

The compound component pattern is a set of components working together. This pattern is quite useful when you want to create a more complex component with subcomponents that are tightly related.

Example:

Imagine a tab component: Tab and TabPanel.

const Tabs = ({ children }) =>
const [activeIndex, setActiveIndex] = React.useState(0);
    
    return (
        <div>
            <div>
                {React.Children.map(children, (child, index) =>
React.cloneElement(child, {
                        isActive: index === activeIndex,
onClick: () => setActiveIndex(index),
                    })
                )}
</div>
            <div>
                {React.Children.toArray(children)[activeIndex].props.children}
            </div>
        </div>
    );
};

const Tab = ({ isActive, onClick, children }) => (
    <button style={{ fontWeight: isActive? 'bold' : 'normal' }} onClick={onClick}>
        {children}
    </button>
);

Advantages

  • Encapsulation: The compound component encapsulates the behavior and, hence the child can communicate freely without extra prop drilling.
  • Increased Readability: The depth hierarchy describes the structure between elements in clear terms.

5. Hooks for Reusability

Hooks is extracted into React to be able to move the stateful logic around the code without affecting the component structure. Custom hooks are great for encapsulation of logics.

Example

Custom hook to use form's state.

const useForm = (initialValues) => {
  const [values, setValues] = React.useState(initialValues);
 
  const handleChange = (event) =>
setValues((prevValues) => ({
           .prevValues,
            [event.target.name]: event.target.value,
        }));
    };

    return [values, handleChange];
};

const MyForm = () => {
const [formValues, handleChange] = useForm({ name: '', email: '' });

return (
<form>
    <input
        name="name"
        value={formValues.name}
        onChange={handleChange}
    />
    <input
        name="email"
        value={formValues.email}
        onChange={handleChange}
    />
</form>
)
);
};

Benefits

  • Simplifies Logic Sharing: Custom hooks allow you to encapsulate logic in a way that is independent of components.
  • Clean Components: Promotes cleaner, more focused component implementation.

Conclusion

Crafting reusable UI components in React requires understanding and applying design patterns to promote code reusability, maintainability, and scalability. Whether you opt for presentational and container components, HOCs, render props, compound components, or hooks, each pattern has its unique benefits that can improve your development process.


With these component design patterns in hand, you will become empowered to build complex applications even more efficiently as React continues to grow. Accept these patterns and begin experimenting with them within your projects, and see your components become flexible and easy to maintain.