Separation of Concerns: Separating View and Business Logic (Custom Hook)
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."
// ❌ 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.
Separating these two makes the code incredibly clean.
View (Presentational)
Logic (Container / Custom Hook)
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.
// 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.
// 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?
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.
Key Takeaways
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”