How to Create and Use Reusable React Components

    Thursday, March 21, 202413 min read3005 views
    How to Create and Use Reusable React Components

    Reusable React components provide the foundation of React's component-based architecture, enabling developers to create modular, self-contained chunks of UI functionality.

    This method, which progresses from class components to function components and hooks, emphasizes the ideals of reuse, scalability, and maintainability. By following the "Don't Repeat Yourself" (DRY) philosophy, developers can encapsulate similar patterns in a class component, eliminating repetition and guaranteeing code consistency.

    A reusable class component can improve the speed of code creation, testing, and debugging, making complicated applications easier to manage. These class components, which range from simple user interface elements to complex functional components, help to provide a cohesive and consistent user experience.

    As we progress through React programming, we will learn about advanced approaches and best practices that will improve the usefulness of a reusable class component in developing powerful applications.

    Following are the advanced techniques that can be used for building reusable components in React.

    1. Compound Components in React

    2. Render Props and Functions as Children

    3. Higher-Order Components (HOCs)

    4. Custom Hooks for Reusability

    5. Leveraging React Context API

    6. Dynamic Component Composition

    7. Testing Strategies for Reusable Components

    8. Documentation Best Practices

    1. Compound Components in React

    Compound Components are a design concept used in React apps to generate reusable, flexible components. Compound components comprise a parent component, which handles the component's state and logic, and child components, which are responsible for generating the user interface.

    The fundamental advantage of compound components is that they allow developers to construct a single, simple component that can be used in multiple contexts while keeping consistency and flexibility. 

    Let's look at a simple example of a React compound component: a Form component with two child components (Input and SubmitButton). The parent Form component handles the overall form state, while the child components are responsible for individual form elements.

    // Parent component (Form)
    import React, { useState } from 'react';
    
    const Form = ({ children }) => {
      const [formData, setFormData] = useState({});
    
    
    const handleChange = (name, value) => {
        setFormData((prevData) => ({ ...prevData, [name]: value }));
      };
    
      const handleSubmit = (event) => {
        event.preventDefault();
        // Perform form submission logic with formData
        console.log('Form submitted with data:', formData);
      };
    
      // Clone child components and pass necessary props
      const formChildren = React.Children.map(children, (child) =>
        React.cloneElement(child, {
          onChange: handleChange,
        })
      );
    
      return (
        <form onSubmit={handleSubmit}>
          {formChildren}
          <button type="submit">Submit</button>
        </form>
      );
    };
    
    // Child components (Input and SubmitButton)
    const Input = ({ name, onChange }) => (
      <input
        type="text"
        placeholder={`Enter ${name}`}

     onChange={(e) => onChange(name, e.target.value)}
    );
    const SubmitButton = () => <p>Click "Submit" to submit the
     form.</p>;
    
    // Usage
    const App = () => (
    
     <Form>
        <Input name="username" >
       <Input name="email" >
        <SubmitButton >
      </Form>
       );

    The Form component is the parent that controls the entire form state via the formData state. It includes JavaScript functions such as handleChange for updating form data and handleSubmit for handling form submissions. It clones child components and passes down the required props (such as onChange) to allow communication.

    The Input component is a child component that renders the form's input field. Another child component, the SubmitButton, can be used to deliver further information or instructions within the form.

    In conclusion, compound components are an effective method for creating reusable, versatile components in our React applications. By combining a parent component and numerous child components, we may design components that can be customized and reused in a variety of settings.

    2. Render Props and Functions as Children

    Two common React component design patterns for attaining flexibility and composability are

    1. Render Props

    2. Function as Children

    These patterns enable code sharing between components and provide a strong method for creating reusable and customizable UI elements.

    Render Props

    Render Props is a pattern in which a React component receives a JavaScript function as a prop, usually called render or children, which uses a render method to render its content. This pattern enables the React component to transfer dynamic behavior or data to its offspring.

    Render Props are a versatile approach to sharing code between components, allowing for customization of the rendering logic. Components that use Render Props can be very reusable because they delegate rendering to the consuming React component.

    // Parent Component
    class MouseTracker extends React.Component {
        render() {
            return (
    
             <div onMouseMove={(e) => this.props.render(e)}>
               {/* Additional content or UI */}
           </div>
            );
        }
        }
    
    // Usage
    const App = () => (
        <MouseTracker render={(mousePosition) => (
           <p>Mouse position: {`${mousePosition.clientX},
     ${mousePosition.clientY}`}</p>
        )} >
       );

    Function as Children

    It is a pattern in which a React component accepts a function as its child property. The React component then calls this function, supplying appropriate data or behavior as parameters, and enabling dynamic rendering.

    Function as Children enables dynamic composition of components, increasing the flexibility of UI design. It frequently produces clearer JSX syntax, making the code more readable and expressive.

    // Parent Component
    class Counter extends React.Component {
        render() {
          return this.props.children(this.state.count,
    this.incrementCount);
        }
        }
     // Usage
    const App = () => (
       <Counter>
         {(count, increment) => (
           <div>
             <p>Count: {count}</p>
             <button onClick={increment}>Increment</button>
           </div>
        
     )}
        </Counter>
        );

    Render Props are used when you want to pass a single function as a prop to influence how the same component renders. Function as Children can be used to pass numerous JavaScript functions or other components to the React component.

    In conclusion, Render Props and Function as Children are effective React patterns that improve component composability and reusability. The choice between them is determined by your component design's requirements, although both provide developers with useful tools for constructing versatile and maintainable React applications.

    3. Higher-Order Components (HOCs)

    Higher-Order Components (HOCs) are a powerful and adaptable React style that enables component logic reuse by wrapping components with additional functionality. HOCs function like functions, taking a component and returning an improved component that expands its capabilities. This encourages code reuse, abstraction, and concern separation in React applications.

    A Higher-Order Component is a function that accepts a component and returns a new component with additional features or behavior.

    HOCs allow reusing the logic across various components, eliminating code redundancy. They enable the simplification of complex functionalities into separate, manageable components. HOCs encourage component composition, making it easier to create sophisticated user interface structures.

    Let's create an HOC for handling loading states in a component.

     // LoadingHOC.js
      import React from 'react';
    
    const withLoading = (WrappedComponent) => {
      return class WithLoading extends React.Component {
        constructor(props) {
          super(props);
    this.state = { isLoading: false };
       }
    
       toggleLoading = () => {
         this.setState((prevState) => ({ isLoading: !prevState.isLoading
    }));
       };
       render() {
         return (
           <WrappedComponent
             isLoading={this.state.isLoading}
             toggleLoading={this.toggleLoading}
             {...this.props}
           />
         );
       }
     };
    };
    
    // Usage
    const MyComponent = ({ isLoading, toggleLoading }) => (
      <div>
        <button onClick={toggleLoading}>Toggle Loading</button>
         {isLoading ? <p>Loading...</p> : <p>Content loaded successfully
    </p>}
      </div>
    );
      const MyComponentWithLoading = withLoading(MyComponent);

    In the above example, The 'withLoading' HOC simplifies the management of component loading states. It encapsulates the loading logic so that it may be reused across multiple components. By applying the HOC to MyComponent, the resulting MyComponentWithLoading can toggle and display the loading state.

    HOCs are useful when several components need to share logic, such as handling loading states, authentication, or data fetching.HOCs are useful for resolving cross-cutting concerns that apply to multiple areas of your application.

    In conclusion, Higher-Order Components are an important tool in React for increasing code reuse and isolating concerns in component-based designs. Creating HOCs allows developers to efficiently share and abstract complex logic, resulting in a more modular and maintainable codebase.

    4. Custom Hooks for Reusability

    Custom Hooks in React allow you to encapsulate and reuse logic across components, making your code clearer and more maintainable. They enable developers to extract shared functionality, making it simple to manage complicated activities like data collecting and form states across numerous components.

    Examples of Custom Hooks

    1. Data Fetching Hook

    // useDataFetching.js
    import { useState, useEffect } from 'react';
    
    const useDataFetching = (url) => {
      const [data, setData] = useState(null);
      const [isLoading, setLoading] = useState(true);
      const [error, setError] = useState(null);
    
      useEffect(() => {
        const fetchData = async () => {
          try {
           const response = await fetch(url);
            const result = await response.json();
            setData(result);
          } catch (error) {
            setError(error);
          } finally {
            setLoading(false);
         }
        };
    
         fetchData();
      }, [url]);
       return { data, isLoading, error };
    };
    
    // Usage
    const MyComponent = () => {
      const{data,isLoading,error}=('https://api.example.com/data');
    
      if (isLoading) return <p>Loading...</p>;
      if (error) return <p>Error: {error.message}</p>;
    return <p>Data: {data}</p>;
       };

    In the above code, the useDataFetching custom hook manages data fetching by utilizing React's useState and useEffect. It encapsulates the asynchronous process of fetching data from a specified URL and updating the component's state variables (data, isLoading, and error).

    2. Form State Management Hook

    // useForm.js
    import { useState } from 'react';
    
    const useForm = (initialState) => {
      const [values, setValues] = useState(initialState);
    
      const handleChange = (event) => {
        const { name, value } = event.target;
        setValues({ ...values, [name]: value });
      };
    
      const resetForm = () => {
       setValues(initialState);
      };
    
     return { values, handleChange, resetForm };
    };
    
    // Usage
    const MyForm = () => {
      const { values, handleChange, resetForm } = useForm({ username: '',
     password: '' });
    
      const handleSubmit = (event) => {
       event.preventDefault();
        // Handle form submission using values
       resetForm();
      };
    
      return (
       <form onSubmit={handleSubmit}>
          <input type="text" name="username" value={values.username}
      onChange={handleChange} >
       <input type="password" name="password" value={values.password}
          onChange={handleChange} >
          <button type="submit">Submit</button>
        </form>
      );
       };

    In the above code, the useForm custom hook facilitates form state management in React components. It employs the useState hook to maintain form values and provides functions like handleChange to update the values dynamically and resetForm to reset the form to its initial state.

    In the MyForm component, the hook handles form input changes, submission, and resetting, showcasing a concise and reusable solution for managing form state in React applications.

    Choose names that clearly explain the function of the custom hook, with the practice of beginning with "use". Aim for a single responsibility in each custom hook, focusing on a particular piece of functionality. Design bespoke hooks with reusability in mind, making them adaptable to a variety of contexts.

    Document your custom hooks completely, including parameters, return values, and usage examples. Make custom hooks customizable by accepting parameters, which allows them to be used in a variety of settings.

    In conclusion,  custom hooks in React are an effective way to encapsulate and reuse code across components. Developers can construct flexible and successful custom hooks by following best practices such as descriptive naming, single responsibility, reusability, documentation, and parameterization.

    These hooks improve code reuse, promote clean and modular code structures, and make it easier to manage complex functionality across numerous components, such as data fetching or form state management. 

    5. Leveraging React Context API

    The React Context API enables developers to handle states at a higher level, providing a simple and effective solution for sharing global data between components. By creating reusable contexts, developers can streamline state management and address common challenges like theming or authentication with ease.

    Creating a Reusable Context

    First, define a context

    // ThemeContext.js
    import { createContext, useState, useContext } from 'react';
    
    const ThemeContext = createContext();
    
    export const ThemeProvider = ({ children }) => {
      const [theme, setTheme] = useState('light');
    
      const toggleTheme = () => {
        setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light'));
      };
    
      return (
        <ThemeContext.Provider value={{ theme, toggleTheme }}>
          {children}
        </ThemeContext.Provider>
      );
        };
    
    export const useTheme = () => {
      return useContext(ThemeContext);
     };

    Next, use the reusable context in components.

    // ThemedComponent.js
    import React from 'react';
    import { useTheme } from './ThemeContext';
    
    const ThemedComponent = () => {
      const { theme, toggleTheme } = useTheme();
    
      return (
        <div style={{ background: theme === 'light' ? '#fff' : '#333', color: theme === 'light' ? '#000' : '#fff' }}>
          <h2>Themed Component</h2>
          <p>Current Theme: {theme}</p>
          <button onClick={toggleTheme}>Toggle Theme</button>
        </div>
      );
      };

    Practical Example given below - Theming

    // App.js
    import React from 'react';
    import { ThemeProvider } from './ThemeContext';
    import ThemedComponent from './ThemedComponent';
    
    const App = () => {
      return (
        <ThemeProvider>
          <ThemedComponent />
        </ThemeProvider>
      );
        };

    A practical Example given below - Authentication

      // AuthContext.js
    import { createContext, useState, useContext } from 'react';
    
    const AuthContext = createContext();
    
    export const AuthProvider = ({ children }) => {
      const [isAuthenticated, setAuthenticated] = useState(false);
    
     const login = () => {
        setAuthenticated(true);
      };
    
      const logout = () => {
        setAuthenticated(false);
      };
    
      return (
        <AuthContext.Provider value={{ isAuthenticated, login, logout }}>
          {children}
        </AuthContext.Provider>
        );
     };
    
    export const useAuth = () => {
      return useContext(AuthContext);
    };

    In conclusion, the React Context API facilitates the development of reusable global state management solutions in React. Encapsulating state logic in contexts allows developers to easily exchange and access global data across components, enabling a clean and modular architecture.

    6. Dynamic Component Composition

    Dynamic component composition in React is a great method for increasing the adaptability and flexibility of user interfaces. By dynamically generating components based on data or user-defined components, developers may construct interfaces that react intelligently to changing situations, resulting in a more scalable and maintainable codebase.

    Dynamic component composition enables interfaces to react to changing data, user choices, or application situations, resulting in a more seamless experience. It makes it easier to create reusable components that may be used throughout an application, minimizing redundancy and increasing uniformity.

    Dynamically combining components enables the creation of scalable applications capable of handling a wide range of circumstances while avoiding excessive code duplication.

    Rendering Components Based on Data

    import React from 'react';
    
    const DynamicComponentRenderer = ({ data }) => {
      return (
        <div>
            {data.map((item, index) => (
           <DynamicComponent key={index} type={item.type} {...item.props}
    />
         ))}
        </div>
      );
        };
    
    const DynamicComponent = ({ type, ...props }) => {
      switch (type) {
        case 'Button':
         return <button {...props}>Click me</button>;
        case 'Input':
          return <input {...props} />;
        default:
          return null;
      }
        };

    Conditional Rendering Based on User Interactions

    import React, { useState } from 'react';
    
    const DynamicUserInterface = () => {
      const [isLoggedIn, setLoggedIn] = useState(false);
    
     return (
        <div>
         {isLoggedIn ? <UserDashboard /> : <LoginScreen onLogin={() =>
    setLoggedIn(true)} />}
       </div>
     );
    };

    Design components with adaptable prop structures to handle changes in data or user requirements. Use HOCs to dynamically improve component functionality, letting components in React adapt to their usage context. Use render properties to provide dynamic rendering logic to components, allowing them to respond to changes in data or user interactions.

    In conclusion, dynamic component react composition is essential for creating adaptive and scalable interfaces in React. By dynamically generating components based on data or user inputs, developers may construct flexible and reusable user interfaces that can easily adjust to changing requirements.

    Using tactics like flexible props, HOCs, and render props ensures that components in React are versatile and adaptive across multiple contexts, resulting in a more manageable and future-proof code base.

    7. Testing Strategies for Reusable Components

    Testing is critical for ensuring that reusable class components are reliable, maintainable, and functional. Comprehensive testing helps to detect defects early, allows code modifications without worry of damaging current functionality, and guarantees that components in React work as expected across multiple contexts.

    Testing Strategies for Advanced Component Patterns

    1. Unit Testing

    Check that individual functions and units of code within a class component perform properly.

    Example with Jest:

    // myComponent.jsReact developers can readily adapt interfaces to changing needs by distinguishing between controlled and uncontrolled components and utilizing dynamic valid functional component composition. Employing immutable data patterns provides solid state management, and comprehensive testing with explicit documentation improves app component reliability and usability, resulting in more efficient and fun React development.
    export const addNumbers = (a, b) => a + b;
    
    // myComponent.test.js
    import { addNumbers } from './myComponent';
    
    test('addNumbers function adds two numbers correctly', () => {
      expect(addNumbers(2, 3)).toBe(5);
    });

    2. Integration Testing

    Ensure that different units of a react component work together as expected.

    Example using Jest and React Testing Library:

    // myComponent.js
    import React from 'react';
    
    const MyComponent = ({ value }) => <div>{value}</div>;
    
    export default MyComponent;
    
    // myComponent.test.js
    import React from 'react';
    import { render } from '@testing-library/react';
    import MyComponent from './myComponent';
    
    test('MyComponent renders the provided value', () => {
      const { getByText } = render(<MyComponent value="Hello, World!" />);
      expect(getByText('Hello, World!')).toBeInTheDocument();
    });

    3. Snapshot Testing

    Capture a snapshot of the rendered component and compare it to future renders to detect any unexpected changes.

    Example using Jest and React Testing Library:

    // myComponent.js
    import React from 'react';
    
    const MyComponent = ({ value }) => <div>{value}</div>;
    
    export default MyComponent;
    
    // myComponent.test.js
    
    import React from 'react';
    import { render } from '@testing-library/react';
    import MyComponent from './myComponent';
    
        test('MyComponent snapshot matches', () => {
         const { asFragment } = render(<MyComponent value="Snapshot Test" />);
         expect(asFragment()).toMatchSnapshot();
        });

    Keep tests separate to ensure they are independent and run consistently. Mocks should be used only when necessary to avoid overly artificial scenarios and maintain test realism. Cover edge cases in your tests to guarantee that your components can handle all possible scenarios. To maintain tests current and effective, they should be updated regularly with code updates.

    In conclusion, comprehensive testing is required to ensure the quality and reliability of reusable components. Using a combination of unit testing, integration testing, and snapshot testing with popular libraries such as Jest and React Testing Library ensures that components behave as intended in various contexts.

    Following the best testing techniques helps to create a strong and manageable testing suite, giving users confidence in the functioning and reusability of React components.

    8. Best Practices for Documentation

    Clear documentation is required to promote component reuse in React. It serves as a guide for developers, allowing them to better grasp the functionality, usage, and customization of each component. Well-documented components enable developers to use existing solutions, increasing productivity and consistency across projects.

    Documentation helps other developers understand how to utilize other components and which props and methods are accessible. This means that we should provide clear and succinct documentation for each component, including examples of how to use it and the props it accepts.

    We may use tools like Storybook or Docz to generate live documentation that demonstrates how our components appear and behave in various contexts. Documenting our components will make it easier for other developers to use and contribute to our source.

    Conclusion

    To summarise, understanding advanced strategies for constructing reusable React components is critical for developing agile and scalable online apps. Understanding concepts like compound components, render properties, and custom hooks helps the creation of modular and adaptive user interface elements. Higher-order components (HOCs) and the Context API are effective tools for sharing functionality and handling data on a larger scale.

    React developers can readily adapt interfaces to changing needs by distinguishing between controlled and uncontrolled components and utilizing dynamic valid functional component composition. Employing immutable data patterns provides solid state management, and comprehensive testing with explicit documentation improves app component reliability and usability, resulting in more efficient and fun React development.

    24

    Related articles

    This website uses cookies to analyze website traffic and optimize your website experience. By continuing, you agree to our use of cookies as described in our Privacy Policy.