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.
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.
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.
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
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.
Frequently Asked Questions
Should every rule leave the component?
Does separating logic make the app slower?
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.
More in this topic
How to Build a Dropdown Menu in React
Build a React dropdown menu with the ARIA menu button pattern. Handle open and close, keyboard arrows, and clicks outside the menu.
How to Animate React Components with Motion
Animate React components with the Motion library. Set up motion, add enter, hover, and exit animations, and respect reduced motion.
Headless UI Components Explained: Logic Without Locked Styling
Understand headless UI components and how libraries like Radix give you unstyled, accessible behavior that you style yourself.