For years, one of the most repeated “clean code” rules in frontend development has been:
“Keep logic out of your components.”
It sounds great, right?
Clean separation, easy testing, reusable hooks… the dream.
But if you’ve ever worked on a large scale enterprise UI, you know that dream can turn into a maintenance nightmare.
Let me tell you what happens when you take that rule too literally.
The Problem: When “Clean” Code Becomes Chaos
A few years ago, I was building an enterprise ERP with multiple microfrontends each responsible for managing users, products, invoices, and other business entities.
Our UI library was textbook clean: tables, forms, modals all reusable, all “dumb.”
Every component got its data and behavior through hooks and services.
On paper, it looked great.
In reality? It fell apart. (a chaos to maintain)
Each entity (Users, Orders, Invoices, etc.) needed the same behaviors:
- Pagination
- Filtering & search
- Validation
- Loading & error states
- Permissions
- Notifications
- API retries
Because we followed the “no logic in components” rule , all of this logic lived outside in hooks like useUsers, useOrders, useInvoices…
Each one was slightly different, managed its own state, and required constant synchronization with forms and services.
When an API changed or a new feature was added, I had to patch 20 different hooks all doing almost the same thing.
Loading spinners looked different. Error messages weren’t consistent. Pagination behaved differently across modules.
Debugging was chaos. Deployments were stressful. Consistency died slowly.
Where This Rule Goes Wrong
At this point, you might think: “That’s just bad implementation.”
But it’s deeper than that.
The “no logic in components” rule often confuses three different types of logic:
| Type | Description | Should it live in a component? |
|---|---|---|
| UI logic | Visual state (open/close, input focus, local state) | ✅ Yes |
| Presentation logic | Pagination, filters, sorting, data transforms | ✅ Often yes |
| Business logic | Domain rules, workflows, calculations | ❌ No |
The problem wasn’t that logic existed it was that the wrong kind of logic was banished.
Hooks made logic shareable, but not self-contained.
Every page still needed to wire the same behaviors manually: loading, retries, validation, toasts, etc.
In the end, we had reusable functions, not reusable experiences.
The Realization: Reusability Isn’t About Imports
That’s when it hit me:
Reusability isn’t about how many times you can import a hook it’s about how few times you have to think about it.
I didn’t need more hooks.
I needed behavior that traveled with the component itself something I could drop anywhere, and it would just work.
That’s when Parameterized Smart Components were born.
Instead of separating logic from components, I started bringing generic behavior back in, in a structured way.
I created a component like this:
<CrudPage
type="User"
service={getUsersAPI}
columns={userColumns}
pageSize={20}
/>
<CrudPage
type="Order"
service={getOrdersAPI}
columns={orderColumns}
pageSize={50}
/>
That’s it. Each CrudPage handled fetching, pagination, filtering, validation, errors, and CRUD operations internally. The only differences were parameters: the entity type, the service, and the table configuration.
Results:
No duplicate logic: One component for all entities.
Consistent UX: Same loading states, same errors, same pagination.
Easy maintenance: Fix it once, everywhere updates.
Faster development: Drop it in and move on.
Why Hooks Alone Weren’t Enough
Hooks solved reusability of data fetching, not user experience. They exposed state but didn’t define behavior. Developers still had to manually glue together:
-
Loading indicators
-
Notifications
-
Permissions
-
Form validation
-
API retries
That’s not scalable in enterprise systems.
Hooks are perfect for small teams and isolated screens, but once you have 10+ microfrontends, the boilerplate becomes unmanageable.
But Wait Don’t these components Become “God Components”?
That’s a fair question. And it’s the first trap to avoid.
Smart components should own behavior, not visuals. The solution is to make them headless smart components logic + state, but the rendering is customizable.
Example:
<CrudPage type="User" service={getUsersAPI}>
{({ data, pagination, actions, isLoading }) => (
<Table
data={data}
pagination={pagination}
onEdit={actions.edit}
isLoading={isLoading}
/>
)}
</CrudPage>
This gives you:
-
Centralized behavior (fetching, retries, filters)
-
Customizable UI (different table layouts, cards, or lists)
-
Easy testing (mock render function instead of full DOM)
-
Extensibility (plugins, lifecycle hooks, custom filters)
That’s how you avoid the “bloated component” trap.
The key is balance.
Smart components should handle generic behavior, but expose hooks for custom extensions.
This approach doesn’t stop at tables and forms. It’s a pattern that could scale to Server Driven UI (SDUI) or Hybrid Frameworks (Even drag and drop if you’re patient enough and have cash to burn) Because when your components are parameterized and self-contained, you can start rendering them dynamically based on server configuration, roles, or permissions.
That’s the next evolution of frontends: From “logicless UI” → to “smart, composable, configurable UI.”