Crafting React Components: Readability, Reusability, and Efficiency

Vishal Yadav - Jul 13 - - Dev Community

In the world of React, writing components is an art. It’s not just about making them work — it’s about making them work well. Today, we’re going to look at how to craft your components like a pro, focusing on readability, reusability, and efficiency.

Create a List Component

Let’s start with a basic List component:

// src/components/List.js
import React from 'react';

const List = ({ data }) => {
  return (
    <ul>
      {data.map((item, index) => (
        <li key={index}>{item}</li>
      ))}
    </ul>
  );
};

export default List;
Enter fullscreen mode Exit fullscreen mode

This component takes an array of data and renders it as a list. While simple, this component can be enhanced to be more versatile and robust.

Improving List Component with Prop Types and Default Props

Enhancing our List component to include Prop Types and Default Props can increase its reliability and usability:

// src/components/List.js
import React from 'react';
import PropTypes from 'prop-types';

const List = ({ data }) => {
  return (
    <ul>
      {data.map((item, index) => (
        <li key={index}>{item}</li>
      ))}
    </ul>
  );
};

List.propTypes = {
  data: PropTypes.arrayOf(PropTypes.string).isRequired,
};

List.defaultProps = {
  data: [],
};

export default List;
Enter fullscreen mode Exit fullscreen mode

By adding Prop Types, we ensure that the data prop is an array of strings, helping to catch potential bugs early. The default props ensure that our component still renders gracefully even if no data is provided.

Enhancing Components with HOCs

Higher-Order Components (HOCs) are a powerful pattern for reusing component logic. They essentially wrap a component to extend its functionality without altering its structure.

Creating a withLoading HOC

For example, a withLoading HOC can be used to display a loading state:

// src/hocs/withLoading.js
import React from 'react';

function withLoading(Component) {
  return function WithLoading({ isLoading, ...props }) {
    if (isLoading) {
      return <div>Loading...</div>;
    }
    return <Component {...props} />;
  };
}

export default withLoading;
Enter fullscreen mode Exit fullscreen mode

This HOC checks the isLoading prop. If it’s true, it renders a “Loading…” message. Otherwise, it renders the wrapped component, allowing for a seamless user experience during data fetching.

Creating a withErrorHandling HOC

Similarly, withErrorHandling is another HOC that can manage error states:

// src/hocs/withErrorHandling.js
import React from 'react';

function withErrorHandling(Component) {
  return function WithErrorHandling({ error, ...props }) {
    if (error) {
      return <div>Error: {error.message}</div>;
    }
    return <Component {...props} />;
  };
}

export default withErrorHandling;
Enter fullscreen mode Exit fullscreen mode

When an error occurs, withErrorHandling displays an error message. Otherwise, it renders the component as usual. This HOC is particularly useful for handling fetch errors or issues within the component lifecycle.

Combining HOCs for Robust Components

By combining withLoading and withErrorHandling, we can create a robust component that handles both loading and error states elegantly:

// src/components/EnhancedList.js
import React from 'react';
import withLoading from '../hocs/withLoading';
import withErrorHandling from '../hocs/withErrorHandling';
import List from './List';

const ListWithLoading = withLoading(List);
const EnhancedList = withErrorHandling(ListWithLoading);

export default EnhancedList;
Enter fullscreen mode Exit fullscreen mode

This approach promotes code reuse and separation of concerns, making our components more maintainable and easier to understand.

Fetching Data with Hooks

React hooks allow us to use state and other React features without writing a class. useFetch is a custom hook that fetches data from an API:

// src/hooks/useFetch.js
import { useState, useEffect } from 'react';

const useFetch = (url) => {
  const [data, setData] = useState([]);
  const [isLoading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      setLoading(true);
      try {
        const response = await fetch(url);
        if (!response.ok) {
          throw new Error('Network response was not ok');
        }
        const json = await response.json();
        setData(json);
      } catch (error) {
        setError(error);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [url]);

  return { data, isLoading, error };
};

export default useFetch;
Enter fullscreen mode Exit fullscreen mode

This hook handles the fetching state, data storage, and errors, making it easy to fetch and display data in our components.

Adding Caching to useFetch Hook

To improve the efficiency of our useFetch hook, we can add basic caching:

// src/hooks/useFetch.js
import { useState, useEffect } from 'react';

const cache = {};

const useFetch = (url) => {
  const [data, setData] = useState([]);
  const [isLoading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      if (cache[url]) {
        setData(cache[url]);
        return;
      }

      setLoading(true);
      try {
        const response = await fetch(url);
        if (!response.ok) {
          throw new Error('Network response was not ok');
        }
        const json = await response.json();
        cache[url] = json;
        setData(json);
      } catch (error) {
        setError(error);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [url]);

  return { data, isLoading, error };
};

export default useFetch;
Enter fullscreen mode Exit fullscreen mode

This caching mechanism stores fetched data in a cache object, reducing redundant network requests and improving performance.

Assembling the App

Finally, we bring everything together in the App component:

// src/App.js
import React from 'react';
import EnhancedList from './components/EnhancedList';
import useFetch from './hooks/useFetch';

const App = () => {
  const { data, isLoading, error } = useFetch('https://api.example.com/data');

  return (
    <div>
      <h1>List Component</h1>
      <EnhancedList data={data} isLoading={isLoading} error={error} />
    </div>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

We use our useFetch hook to load data and pass it to our EnhancedList component, which is enhanced with loading and error handling capabilities through our HOCs.

Conclusion

Crafting components in React involves more than just making them functional; it's about ensuring they are readable, reusable, and efficient. By using HOCs to manage loading and error states, custom hooks to handle data fetching, and adding improvements such as Prop Types and caching, we can build robust and maintainable components that enhance the overall development experience. This approach not only simplifies our code but also makes it easier to manage and scale our applications.

By focusing on these best practices, you can elevate your React development skills and build applications that are not only functional but also elegant and efficient.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .