React|Architecture and Design

Separation of Concerns: Separating View and Business Logic (Custom Hook)

4

Chris opened the OrderPage.tsx file he created. It was a staggering 800 lines long.

Half the file is packed with useState and useEffect, and the other half is a complex "div hell."

typescript
// ❌ A Horrible Hybrid
function OrderPage() {
  const [user, setUser] = useState(...);
  const [cart, setCart] = useState(...);
  
  // Business Logic: Fetching Data
  useEffect(() => { /* 50 lines */ }, []);
  
  // Business Logic: Payment Processing
  const handlePayment = () => { /* 100 lines */ };

  // View Logic: Rendering
  return (
    <div>
      {/* ... 500 lines of JSX ... */}
    </div>
  );
}

A designer asked, "Can you please change the button color?" Chris opened the code, but his hands started shaking, fearing he might accidentally break the payment logic.

This happens because the UI (Showing) and the Logic (Doing) are tangled together like a single entity.

Today, let's explore the technique to tear these two apart. While this was formerly known as the "Presentational & Container" pattern, we will look at its modern evolution: the Custom Hook pattern.

1. Dividing Roles: The Chef and the Plating Specialist

A React component does two things.

  • How does it work? (Logic): Data fetching, state management, event handlers.
  • How does it look? (View): HTML structure, styles, animation.
  • Separating these two makes the code incredibly clean.

    View (Presentational)

  • Draws the screen receiving only Props.
  • Contains (almost) no logic like useState or useEffect.
  • Extremely easy to reuse.
  • Logic (Container / Custom Hook)

  • Fetches and processes data.
  • Defines event handlers.
  • Knows absolutely nothing about what the UI looks like.
  • 2. Refactoring: Extracting Logic with Custom Hooks

    Let's tear apart Chris's 800-line code.

    Step 1. Isolate Business Logic into a Hook (useOrder.ts)

    Cut out all state and functions and move them to a file named useOrder.

    typescript
    // useOrder.ts (Custom Hook)
    import { useState, useEffect } from 'react';
    
    // Manages logic only. Does NOT return JSX.
    export function useOrder() {
      const [cart, setCart] = useState<CartItem[]>([]);
      const [isLoading, setIsLoading] = useState(false);
    
      useEffect(() => {
        // API call logic...
      }, []);
    
      const handlePayment = async () => {
        // Complex payment validation logic...
      };
    
      const addToCart = (item: CartItem) => {
        // Add to cart logic...
      };
    
      // Return only the data and functions the View needs.
      return {
        cart,
        isLoading,
        handlePayment,
        addToCart,
      };
    }

    Step 2. Simplify the View Component (OrderPage.tsx)

    Now, the component can empty its brain and simply draw whatever the hook provides.

    typescript
    // OrderPage.tsx (View)
    import { useOrder } from './useOrder';
    
    function OrderPage() {
      // 1. Delegate logic to the Hook (Done in one line!)
      const { cart, isLoading, handlePayment } = useOrder();
    
      if (isLoading) return <Spinner />;
    
      // 2. Focus ONLY on rendering
      return (
        <div className="order-page">
          <h1>Place Order</h1>
          <CartList items={cart} />
          <PaymentButton onClick={handlePayment} />
        </div>
      );
    }

    3. Benefits of Separation

    What is good about splitting them like this?

  • Readability: As soon as you open OrderPage, the structure is visible at a glance: "Ah, this page shows an order list and a payment button."
  • Testability: UI testing (Cypress, etc.) is slow and difficult. However, since the useOrder hook is just a JavaScript function, you can verify the logic using only Unit Tests (Vitest) without the UI.
  • Reusability: What if the 'My Page' section also needs order payment logic? You can simply reuse the useOrder hook.
  • 4. Caution: Over-Engineering

    Chris gets excited and starts trying to split every single button and input field into hooks. This must stop.

    The Colocation Principle

    "Place code as close as possible to where it's relevant."

    If code is separated too far apart (requiring constant file switching), maintenance becomes harder.

  • Split: When logic exceeds 100 lines or is reused elsewhere.
  • Keep: Simple UI states like an isOpen toggle are better left inside the component.
  • Key Takeaways

  • Separation of Concerns: Do not mix "Logic (Doing)" with "View (Showing)."
  • Custom Hook: This is the standard way to separate logic in modern React.
  • View Component: The more you make it a "Dumb" component that only receives Props and draws, the better.
  • Balance: Instead of unconditional separation, you need the sense to divide appropriately based on component complexity.

  • We have finished organizing the internals of the component. But what if the data coming from the outside (Backend) doesn't match my code?

    To prevent the nightmare of having to rewrite all frontend code whenever the backend API changes, we need DTO and Mapper patterns.

    Continuing in: “Frontend DTOs: Building a Mapper Layer Unshaken by Backend API Changes”

    🔗 References

  • Dan Abramov - Presentational and Container Components (The origin of this pattern, though hooks are recommended now)
  • React Docs - Reusing Logic with Custom Hooks
  • Comments (0)

    0/1000 characters
    Loading comments...
    Separation of Concerns: Separating View and Business Logic (Custom Hook) | VXD Blog