How to Separate Business Logic from React UI

Move pricing, validation, and other rules out of components into pure functions and custom Hooks so React UI stays thin.

7 min read

To separate business logic from React UI, move the rules that decide what data means into plain functions and custom Hooks. Components that render the results stay thin enough to read top to bottom.

Spot logic hiding in JSX

A component mixes logic into its UI when conditionals, calculations, and fetch calls sit directly inside the returned JSX. The tell is a render block you have to read twice to find the actual markup.

A cart is a good example. The total depends on item prices and quantities, and the discount rules are business decisions.

None of that belongs between the list tags. When a discount threshold or a tax rule changes, the rendering code should not need to change with it.

Move pure rules into plain functions

A calculation that depends only on its arguments is a pure function. Put it in its own module so it can be imported, reused, and tested without rendering a component.

javascriptjavascript
export function calculateTotal(items) {
  return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
 
export function formatPrice(amount) {
  return new Intl.NumberFormat("en-US", {
    style: "currency",
    currency: "USD",
  }).format(amount);
}

The pricing rules now live in one place. If the discount policy changes, the change touches this file and nothing else. A pure function is also safe to call during render because it reads only its arguments and returns a value.

Move stateful rules into a custom Hook

Rules that need to remember something belong in a custom Hook. The Hook owns the item list and exposes an add action, while the total is derived by calling the pure function.

javascriptjavascript
import { useState } from "react";
import { calculateTotal } from "./pricing";
 
export function useCart() {
  const [items, setItems] = useState([]);
 
  function addItem(item) {
    setItems((current) => [...current, item]);
  }
 
  const total = calculateTotal(items);
 
  return { items, total, addItem };
}

The Hook keeps the array update immutable and derives the total instead of storing it twice. Components that call this Hook share the logic but not the cart itself, because each call creates its own state.

Render the results

The component that uses the Hook only renders what it receives. The pricing and cart rules stay out of the markup.

App.jsxApp.jsx
import { useCart } from "./useCart";
import { formatPrice } from "./pricing";
 
function Cart() {
  const { items, total, addItem } = useCart();
 
  return (
    <div>
      <ul>
        {items.map((item) => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
      <p>Total: {formatPrice(total)}</p>
    </div>
  );
}

This component has no business rules of its own. It reads the cart state, formats the total, and renders.

New rules, such as a shipping estimate, slot in as another pure function next to the pricing code. The full mechanics of the Hook side are in How to Create a Custom Hook in React.

Keep render pure, events for actions, effects for sync

After the rules move out, what remains in the component falls into three buckets. Rendering computes output from props and state.

Event handlers run when the user acts. Effects synchronize with something outside React, and they should be rare.

A submit handler that sends the cart is an action, so it belongs in the handler, not in an Effect watching the cart. The reasoning for that split is in You Might Not Need an Effect: Better React Patterns.

Test the logic without the UI

Extracted functions and Hooks are testable in isolation. A test for calculateTotal checks the rule directly with plain data, and a test for useCart can drive the add action and read the returned total. No browser or rendered component is required for the rule itself.

That isolation changes how fast bugs get found. A failing price test points straight at the pricing rule, not at a rendered component full of unrelated markup.

When to split

Use this checklist when a component mixes concerns.

  • Extract a pure function when a rule is reused or complex.
  • Extract a Hook when the rule also owns state or an external subscription.
  • Leave inline logic alone when it is a one-off display decision.
  • Re-check the boundary when a rule needs to change for a different reason than the UI.

For the complementary question of when to pull a whole component out, see When to Extract a Component or Custom Hook.

Rune AI

Rune AI

Key Insights

  • Business rules should not hide inside JSX.
  • Pure calculations belong in plain exported functions.
  • Stateful rules belong in custom Hooks.
  • Components stay pure and only render the results.
  • Extracted logic is easier to test without rendering.
RunePowered by Rune AI

Frequently Asked Questions

Should every rule leave the component?

No. Small display decisions can stay inline. Extract a rule when it is reused, testable on its own, or complex enough to make the JSX hard to read.

Does separating logic make the app slower?

No. Pure functions run the same either way, and custom Hooks add no runtime cost beyond the functions they already call. The main benefits are readability and testability.

Conclusion

Business logic is the rules that decide what data means, and it does not belong inside JSX. Move pure rules into plain functions, move stateful rules into custom Hooks, and let components render the results.