Separating Business Logic from UI: A Step-by-Step Guide for Better Code Organization
In modern web development, particularly with React, maintaining clean and maintainable code is crucial. One key principle for achieving this is separating business logic from the user interface (UI). This approach enhances code readability, reusability, and scalability, making it easier to test and modify your application.
The Problem: Mixed Concerns in UI Components
Consider a scenario where you’re building a ProductView component in React to display a product’s details, such as its title and cost, along with an "Add to basket" button. Initially, your code might look like this:
const ProductView = ({ productId }) => {
const [product, setProduct] = useState();
useEffect(() => {
const retrieveProduct = async () => {
const response = await fetch(`https://api.sample.com/products/${productId}`);
setProduct(response.json());
};
retrieveProduct();
}, [productId]);
const onAdd = () => {
// ... add logic here
};
return (
<Wrapper>
<Title>{product.title}</Title>
<Cost>{product.salePrice || product.cost}</Cost>
<Button onClick={onAdd}>Add to basket</Button>
</Wrapper>
);
};
In this version, the UI component (ProductView) handles both the data fetching (business logic) and the rendering (UI logic). This tight coupling creates several issues:
Problem: the UI should focus on rendering, not on how data is fetched or processed.
Step 1: First Abstraction – Move Data Fetching to a Custom Hook
To address this, we can extract the data-fetching logic into a custom hook, useProduct. This separates the business logic (fetching and managing the product data) from the UI:
const useProduct = ({ productId }) => {
const [product, setProduct] = useState();
useEffect(() => {
const retrieveProduct = async () => {
const response = await fetch(`https://api.sample.com/products/${productId}`);
setProduct(response.json());
};
retrieveProduct();
}, [productId]);
return { product };
};
Now, the ProductView component can use this hook:
const ProductView = ({ productId }) => {
const { product } = useProduct({ productId });
const onAdd = () => {
// ... add logic here
};
return (
<Wrapper>
<Title>{product.title}</Title>
<Cost>{product.salePrice || product.cost}</Cost>
<Button onClick={onAdd}>Add to basket</Button>
</Wrapper>
);
};
The UI no longer worries about how to fetch the product, focusing instead on rendering the data. However, there’s still room for improvement, particularly with the cost calculation logic.
Step 2: Further Abstraction – Handle Cost Calculation
The Cost component still contains business logic in the form of {product.salePrice || product.cost}, deciding which cost to display. This logic should also be abstracted away from the UI. Let’s move it into the useProduct hook or a separate utility function:
Recommended by LinkedIn
const useProduct = ({ productId }) => {
const [product, setProduct] = useState();
useEffect(() => {
const retrieveProduct = async () => {
const response = await fetch(`https://api.sample.com/products/${productId}`);
setProduct(response.json());
};
retrieveProduct();
}, [productId]);
return { product: { ...product, totalCost: product.salePrice || product.cost } };
};
Now, ProductView can use the totalCost directly:
const ProductView = ({ productId }) => {
const { product } = useProduct({ productId });
const onAdd = () => {
// ... add logic here
};
return (
<Wrapper>
<Title>{product.title}</Title>
<Cost>{product.totalCost}</Cost>
<Button onClick={onAdd}>Add to basket</Button>
</Wrapper>
);
};
By moving it to the hook, we ensure the UI remains clean and focused on presentation.
Step 3: Ultimate Separation – Move Business Logic to Helpers and APIs
For the cleanest separation, move the business logic (e.g., cost calculation and API calls) outside the component entirely into dedicated files. Create a helpers/product.js file for cost parsing and an api/product.js file for API interactions:
// helpers/product.js
export const processProduct = (product) => {
return {
title: product.title,
totalCost: product.salePrice || product.cost,
// ... other parsed properties
};
};
// api/product.js
export const fetchProductById = async (id) => {
const response = await fetch(`https://api.sample.com/products/${id}`);
const product = processProduct(response.json());
return product;
};
Update useProduct to use these helpers:
const useProduct = ({ productId }) => {
const [product, setProduct] = useState();
useEffect(() => {
const retrieveProduct = async () => {
const response = await productsApi.fetchProductById(productId);
setProduct(response);
};
retrieveProduct();
}, [productId]);
return { product };
};
Finally, ProductView remains purely UI-focused:
const ProductView = ({ productId }) => {
const { product } = useProduct({ productId });
const onAdd = () => {
// ... add logic here
};
return (
<Wrapper>
<Title>{product.title}</Title>
<Cost>{product.totalCost}</Cost>
<Button onClick={onAdd}>Add to basket</Button>
</Wrapper>
);
};
This structure allows for easier testing, maintenance, and reuse of business logic across the application.
Benefits of Separation
Separating business logic from UI offers several advantages:
By following this progressive abstraction, you create a more robust and maintainable codebase, adhering to the principles of clean architecture in React applications.
Solid guide! I love how you break down the separation step-by-step—moving from hooks to dedicated helpers and APIs really simplifies maintenance.
Thanks for sharing!
Great breakdown of separating business logic from UI in React! Keeping components clean and focused on rendering makes code more maintainable, testable, and scalable. Love the step-by-step approach to abstraction! 🔥👏