Separating Business Logic from UI: A Step-by-Step Guide for Better Code Organization

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:

  • Maintainability: If the API endpoint or data structure changes, you must update the component logic, increasing the risk of errors.
  • Testability: Testing the UI becomes challenging because it’s intertwined with API calls and data processing.
  • Reusability: The logic is not easily reusable across other components or parts of the application.

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:

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:

  • Improved Testability: You can test business logic independently of the UI, using mock data or APIs without rendering components.
  • Scalability: As your application grows, isolated logic is easier to modify or extend without affecting the UI.
  • Reusability: Business logic can be shared across multiple components or even projects.
  • Clarity: Developers can quickly understand the purpose of each file—UI for presentation, helpers/APIs for logic.

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.

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! 🔥👏

To view or add a comment, sign in

More articles by Daniil Pronchenkov

Others also viewed

Explore content categories