Building Reusable UI Components in React

Last updated: July 25, 2023

In modern web development, building reusable UI components is a key principle to ensure a scalable and maintainable codebase. React, with its component-based architecture, provides a perfect platform to create reusable and modular UI elements. In this tutorial, we will explore best practices and techniques for building reusable UI components in React. We will cover multiple examples with step-by-step explanations to help you create versatile and efficient UI components for your projects.

Understanding the Importance of Reusable UI Components


Creating reusable UI components has several advantages, including:

  • Code Reusability: Reusable components reduce duplication and promote a modular code structure, making it easier to maintain and scale your application.
  • Consistent User Experience: Reusing components ensures a consistent look and feel across your application, providing users with a familiar experience.
  • Development Efficiency: Developing reusable components saves time, as you can leverage existing components instead of building from scratch.
  • Easy Maintenance: If a bug is fixed or an improvement is made to a reusable component, the changes are automatically reflected in all instances of that component.
  • Encapsulation: Reusable components encapsulate their internal logic, allowing you to abstract complex functionality and provide a simple interface.

 

 

Building Basic Reusable Components

In this section, we'll start building simple and generic UI components, such as buttons, inputs, and cards. These components will serve as the foundation for more complex elements later in the tutorial.

Button Component

Let's create a Button component that accepts props to customize its appearance and behavior. This will include props for text, color, size, and click handlers.

import React from 'react';
const Button = ({ text, color, size, onClick }) => {
 const buttonStyle = {
   backgroundColor: color,
   fontSize: size === 'large' ? '24px' : '16px',
   padding: '10px 20px',
   borderRadius: '5px',
   color: '#fff',
   cursor: 'pointer',
 };
 return (
   <button style={buttonStyle} onClick={onClick}>
     {text}
   </button>
 );
};
export default Button;

 

Input Component

Next, let's build a reusable Input component with props for type, placeholder, and value. We'll also handle input changes using the onChange event.


import React from 'react';
const Input = ({ type, placeholder, value, onChange }) => {
 return (
   <input
     type={type}
     placeholder={placeholder}
     value={value}
     onChange={onChange}
   />
 );
};
export default Input;

 

Card Component

Now, let's create a Card component that can wrap any content and provide a visually appealing card-like appearance.

import React from 'react';
const Card = ({ children }) => {
 const cardStyle = {
   border: '1px solid #ccc',
   borderRadius: '5px',
   padding: '20px',
   boxShadow: '2px 2px 5px rgba(0, 0, 0, 0.1)',
 };
 return <div style={cardStyle}>{children}</div>;
};
export default Card;

 


Handling Props for Customization

In this section, we'll explore how to use props to customize our reusable components and make them more versatile.

 

Customizable Button Component

Let's enhance our Button component by making it easily customizable with various props for color, icon, and disabled state.

import React from 'react';
const Button = ({ text, color, size, icon, disabled, onClick }) => {
 const buttonStyle = {
   backgroundColor: color || '#007bff', // Default color if not provided
   fontSize: size === 'large' ? '24px' : '16px',
   padding: '10px 20px',
   borderRadius: '5px',
   color: '#fff',
   cursor: disabled ? 'not-allowed' : 'pointer',
   opacity: disabled ? 0.5 : 1,
 };
 return (
   <button style={buttonStyle} onClick={onClick} disabled={disabled}>
     {icon && <span>{icon} </span>}
     {text}
   </button>
 );
};
export default Button;


Styled Input Component

To demonstrate the use of CSS-in-JS libraries, let's create a StyledInput component using styled-components.


import React from 'react';
import styled from 'styled-components';
const InputWrapper = styled.input`
 border: 1px solid #ccc;
 border-radius: 5px;
 padding: 10px;
 font-size: 16px;
 width: 100%;
`;
const Input = ({ type, placeholder, value, onChange }) => {
 return (
   <InputWrapper
     type={type}
     placeholder={placeholder}
     value={value}
     onChange={onChange}
   />
 );
};
export default Input;

In this example, we use styled-components to create a reusable styled input component. The styles are defined using tagged template literals, making the component self-contained and easily reusable.

 

 

5. Compound Components for Complex Structures

In this section, we'll explore compound components, which allow us to build complex UI structures by composing simpler components together.

Tabs Component

Compound components are components that work together to form a larger, more complex component. A Tabs component is a perfect example of this. Let's create a Tabs component with individual Tab and TabPane components.

 

import React, { useState } from 'react';
const Tabs = ({ children }) => {
 const [activeTab, setActiveTab] = useState(0);
 const handleTabChange = (index) => {
   setActiveTab(index);
 };
 return (
   <div>
     <div>
       {React.Children.map(children, (child, index) => (
         <button
           key={index}
           onClick={() => handleTabChange(index)}
           style={{
             backgroundColor: activeTab === index ? '#007bff' : '#ccc',
             color: activeTab === index ? '#fff' : '#000',
             padding: '10px 20px',
             border: 'none',
             borderRadius: '5px',
             marginRight: '10px',
             cursor: 'pointer',
           }}
         >
           {child.props.label}
         </button>
       ))}
     </div>
     <div>{React.Children.toArray(children)[activeTab]}</div>
   </div>
 );
};
const TabPane = ({ children }) => {
 return <div>{children}</div>;
};
const ExampleTabs = () => {
 return (
   <Tabs>
     <TabPane label="Tab 1">
       <p>Content of Tab 1</p>
     </TabPane>
     <TabPane label="Tab 2">
       <p>Content of Tab 2</p>
     </TabPane>
     <TabPane label="Tab 3">
       <p>Content of Tab 3</p>
     </TabPane>
   </Tabs>
 );
};
export default ExampleTabs;

 

 

In this example, we create a Tabs component with individual TabPane components. The activeTab state tracks the currently selected tab, and the handleTabChange function updates the activeTab when a tab is clicked. We use React.Children.map to iterate over the TabPane components and render the corresponding content based on the activeTab state.

 

Building a Reusable Form Component

Building a reusable form component is a valuable skill, as forms are a fundamental part of most web applications. Let's create a reusable Form component that handles form validation, submission, and error handling.

 

Input Validation

To make our Form component more robust, let's add input validation for required fields and email format.

 

import React, { useState } from 'react';
const Form = () => {
 const [formData, setFormData] = useState({
   name: '',
   email: '',
 });
 const [errors, setErrors] = useState({});
 const handleChange = (e) => {
   setFormData({
     ...formData,
     [e.target.name]: e.target.value,
   });
 };
 const handleSubmit = (e) => {
   e.preventDefault();
   // Validate form inputs
   const errors = {};
   if (!formData.name) {
     errors.name = 'Name is required';
   }
   if (!formData.email) {
     errors.email = 'Email is required';
   } else if (!/\S+@\S+\.\S+/.test(formData.email)) {
     errors.email = 'Invalid email format';
   }
   if (Object.keys(errors).length > 0) {
     setErrors(errors);
     return;
   }
   // Form submission logic here...
   console.log('Form submitted:', formData);
 };
 return (
   <form onSubmit={handleSubmit}>
     <div>
       <label>Name:</label>
       <input
         type="text"
         name="name"
         value={formData.name}
         onChange={handleChange}
       />
       {errors.name && <p>{errors.name}</p>}
     </div>
     <div>
       <label>Email:</label>
       <input
         type="email"
         name="email"
         value={formData.email}
         onChange={handleChange}
       />
       {errors.email && <p>{errors.email}</p>}
     </div>
     <button type="submit">Submit</button>
   </form>
 );
};
export default Form;

In this example, we use the useState hook to manage form data and errors. The handleChange function updates the form data state as the user types. The handleSubmit function performs input validation and displays error messages if any field is missing or the email format is incorrect.

 

Form Submission and Error Handling


Next, let's implement the form submission logic and handle server-side errors, if any.

import React, { useState } from 'react';
const Form = () => {
 const [formData, setFormData] = useState({
   name: '',
   email: '',
 });
 const [errors, setErrors] = useState({});
 const handleChange = (e) => {
   setFormData({
     ...formData,
     [e.target.name]: e.target.value,
   });
 };
 const handleSubmit = async (e) => {
   e.preventDefault();
   // Validate form inputs
   const errors = {};
   if (!formData.name) {
     errors.name = 'Name is required';
   }
   if (!formData.email) {
     errors.email = 'Email is required';
   } else if (!/\S+@\S+\.\S+/.test(formData.email)) {
     errors.email = 'Invalid email format';
   }
   if (Object.keys(errors).length > 0) {
     setErrors(errors);
     return;
   }
   // Simulate form submission with a delay (replace with actual API call)
   await new Promise((resolve) => setTimeout(resolve, 1000));
   // Check for server-side errors (replace with actual API response handling)
   const serverErrors = {
     email: 'Email already exists',
   };
   if (Object.keys(serverErrors).length > 0) {
     setErrors(serverErrors);
     return;
   }
   // Form submission success
   console.log('Form submitted:', formData);
 };
 return (
   <form onSubmit={handleSubmit}>
     <div>
       <label>Name:</label>
       <input
         type="text"
         name="name"
         value={formData.name}
         onChange={handleChange}
       />
       {errors.name && <p>{errors.name}</p>}
     </div>
     <div>
       <label>Email:</label>
       <input
         type="email"
         name="email"
         value={formData.email}
         onChange={handleChange}
       />
       {errors.email && <p>{errors.email}</p>}
     </div>
     <button type="submit">Submit</button>
   </form>
 );
};
export default Form;

In this example, we use the handleSubmit function to handle the form submission. We simulate form submission with a delay using the setTimeout function. For server-side error handling, we use a serverErrors object with a sample email error message. You can replace this with actual API response handling for your backend.

 

Creating Accessible Reusable Components

Creating accessible components is crucial to ensure that all users, including those with disabilities, can navigate and interact with your application. Let's explore how to make our reusable components accessible.

ARIA Roles and Attributes

ARIA (Accessible Rich Internet Applications) roles and attributes help provide information to assistive technologies. Let's use ARIA roles and attributes in our Button component.

import React from 'react';
const Button = ({ text, color, size, icon, disabled, onClick }) => {
 const buttonStyle = {
   backgroundColor: color || '#007bff',
   fontSize: size === 'large' ? '24px' : '16px',
   padding: '10px 20px',
   borderRadius: '5px',
   color: '#fff',
   cursor: disabled ? 'not-allowed' : 'pointer',
   opacity: disabled ? 0.5 : 1,
 };
 return (
   <button
     style={buttonStyle}
     onClick={onClick}
     disabled={disabled}
     aria-label={text}
     aria-disabled={disabled}
   >
     {icon && <span>{icon} </span>}
     {text}
   </button>
 );
};
export default Button;

In this example, we use the aria-label attribute to provide a description of the button for screen readers. The aria-disabled attribute is set based on the disabled prop to indicate the button's disabled state to assistive technologies.

 

Best Practices

  • Single Responsibility Principle (SRP): Each component should have a single responsibility and should be focused on a specific task. Avoid creating components that are too large or try to do too much.
  • Props and Composition: Design components with a flexible API using props. Allow users to customize the component's behavior and appearance through props. Utilize composition to combine smaller components into more complex ones.
  • Default Props and Prop Types: Specify default props for optional values and use prop types to enforce prop validation and ensure that the correct data types are passed to the component.
  • Pure Components: Whenever possible, create pure components that are decoupled from application-specific logic and have no side effects. This improves reusability and makes testing easier.
  • Accessibility (a11y): Ensure that your components are accessible to all users, including those with disabilities. Use semantic HTML elements, add appropriate ARIA attributes, and test your components with screen readers and other assistive technologies.
  • CSS-in-JS or Styled Components: Consider using CSS-in-JS libraries or styled components to keep the styles encapsulated within the component. This prevents CSS conflicts and allows for easy customization of styles.
  • State Management: Avoid managing complex state within reusable components. Instead, rely on props to pass data down and use callback functions to communicate changes back to the parent components.
  • Testability: Write unit tests for your components to ensure they work as expected and remain stable when changes are made. Use tools like Jest and Enzyme for testing.
  • Documentation: Provide clear and comprehensive documentation for your reusable components. Describe the purpose of the component, available props, and usage examples. Consider using tools like Storybook to create an interactive component showcase.
  • Versioning and Semantic Versioning: If you plan to publish your components as npm packages, follow semantic versioning (SemVer) to manage changes and updates. Increment the version number appropriately when making breaking changes.
  • Compatibility: Consider the version of React your component will support and communicate it clearly in your documentation. Avoid using deprecated features and keep an eye on React's official updates.
  • Cross-Browser Compatibility: Test your components in different browsers to ensure they work correctly across various environments.
  • Performance Optimization: Optimize your components for performance by avoiding unnecessary re-renders, using React.memo or shouldComponentUpdate, and leveraging the useMemo and useCallback hooks for expensive computations and event handlers.
  • Avoiding Unnecessary Abstraction: While creating reusable components is essential, avoid creating abstractions that make the component hard to understand or use. Balance between reusability and simplicity.

 

Creating reusable components is a journey that never ends. As React evolves, new patterns and best practices will emerge, enabling us to build even more powerful and efficient components. We encourage you to keep exploring, experimenting, and refining your component development skills.


 

Related post