Single Responsibility Principle in React: Writing Better Components

Oscar Bustos

Learn how to apply the Single Responsibility Principle to create more maintainable React components. Start your journey into SOLID principles with practical examples.

6 min read

The Single Responsibility Principle (SRP) is one of the fundamental principles of good software design. It’s part of the SOLID principles, a set of guidelines that help us write better, more maintainable code. Today, let’s explore how we can apply SRP to our React components.

What is Single Responsibility Principle?

The principle states that a module (or in our case, a component) should have only one reason to change. In other words, a component should do one thing, and do it well.

The Problem: A Common Anti-Pattern

Let’s look at a typical React component that’s trying to do too much:

const ProductCard = () => {
  const [product, setProduct] = useState<Product | null>(null);
  const [inCart, setInCart] = useState(false);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    fetchProduct();
  }, []);

  const fetchProduct = async () => {
    try {
      const response = await fetch("/api/product/123");
      const data = await response.json();
      setProduct(data);
      checkIfInCart(data.id);
    } catch (e) {
      setError(e as Error);
    } finally {
      setLoading(false);
    }
  };

  const checkIfInCart = async (productId: string) => {
    const response = await fetch(`/api/cart/${productId}`);
    const data = await response.json();
    setInCart(data.inCart);
  };

  const addToCart = async () => {
    try {
      await fetch("/api/cart", {
        method: "POST",
        body: JSON.stringify({ productId: product?.id }),
      });
      setInCart(true);
    } catch (e) {
      setError(e as Error);
    }
  };

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  if (!product) return <div>Product not found</div>;

  return (
    <div className="product-card">
      <img src={product.image} alt={product.name} />
      <h2>{product.name}</h2>
      <p>{product.description}</p>
      <div className="price">${product.price}</div>
      <button 
        onClick={addToCart}
        disabled={inCart}
      >
        {inCart ? 'In Cart' : 'Add to Cart'}
      </button>
    </div>
  );
};

This component is doing too many things:

  • Fetching product data
  • Managing cart state
  • Handling loading and error states
  • Rendering the product UI
  • Managing cart interactions

A Better Approach: Separation of Concerns

Let’s break this down into focused components and hooks:

// Data fetching hook
const useProduct = (productId: string) => {
  const [product, setProduct] = useState<Product | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    fetchProduct();
  }, [productId]);

  const fetchProduct = async () => {
    try {
      const response = await fetch(`/api/product/${productId}`);
      const data = await response.json();
      setProduct(data);
    } catch (e) {
      setError(e as Error);
    } finally {
      setLoading(false);
    }
  };

  return { product, loading, error };
};

// Cart management hook
const useCart = (productId: string) => {
  const [inCart, setInCart] = useState(false);

  useEffect(() => {
    checkIfInCart();
  }, [productId]);

  const checkIfInCart = async () => {
    const response = await fetch(`/api/cart/${productId}`);
    const data = await response.json();
    setInCart(data.inCart);
  };

  const addToCart = async () => {
    await fetch("/api/cart", {
      method: "POST",
      body: JSON.stringify({ productId }),
    });
    setInCart(true);
  };

  return { inCart, addToCart };
};

// Presentation component
const ProductDisplay = ({
  product,
  inCart,
  onAddToCart,
}: {
  product: Product;
  inCart: boolean;
  onAddToCart: () => void;
}) => (
  <div className="product-card">
    <img src={product.image} alt={product.name} />
    <h2>{product.name}</h2>
    <p>{product.description}</p>
    <div className="price">${product.price}</div>
    <button 
      onClick={onAddToCart}
      disabled={inCart}
    >
      {inCart ? 'In Cart' : 'Add to Cart'}
    </button>
  </div>
);

// Container component
const ProductCard = ({ productId }: { productId: string }) => {
  const { product, loading, error } = useProduct(productId);
  const { inCart, addToCart } = useCart(productId);

  if (loading) return <LoadingSpinner />;
  if (error) return <ErrorMessage error={error} />;
  if (!product) return <NotFound message="Product not found" />;

  return (
    <ProductDisplay 
      product={product} 
      inCart={inCart} 
      onAddToCart={addToCart}
    />
  );
};

Benefits of This Approach

  1. Separation of Concerns: Each piece has a clear, single responsibility
  2. Reusability: Hooks and components can be reused across the application
  3. Testability: Each part can be tested in isolation
  4. Maintainability: Changes to one aspect (like cart logic) don’t affect others
  5. Flexibility: Easy to modify or replace individual pieces

How to Identify Violations of SRP

Here’s a practical tip: If you’re describing your component and find yourself using “and”, you might be violating SRP. For example:

  • “This component fetches product data and manages cart state”
  • “This hook handles product loading and cart operations”

These are signs that you should consider splitting your code into more focused pieces.

Conclusion

The Single Responsibility Principle is about creating components that are focused and maintainable. While it might seem like more work initially to split things up, the benefits in terms of maintainability, testability, and reusability are well worth it.

In future posts, we’ll explore other SOLID principles and how they can help us write better React applications. Stay tuned!

Happy coding! 🚀

Is it a match?

If you need help or advice, feel free to drop us a message via social media or email

Contact me