Skip to main content

The Paradox of use-less-react: getting more from React by using less React

· 8 min read
Fabio Fognani
random nerd

Given that use-less-react deliberately employs an object-oriented philosophy distinct from React's functional-hook paradigm, it might seem that the two tools are in opposition, and that use-less-react wants to "fight" React. In reality, the opposite is true: use-less-react is designed to simplify and lighten React. Its core mandate is to free complex business logic from the constraints of UI-centric APIs. Specifically, Hooks.

By integrating classic OOP patterns for domain management, we gain immediate and important benefits:

  • True Dependency Injection (DI): solving the complex problem of service management that React handles poorly.
  • Intuitive testability: shifting complex state management and side-effects out of functional component lifecycles (where testing is often heavy and unintuitive) into pure, easily testable classes.
  • Elimination of boilerplate: eradicating common frustrations like Stale Closures and the overuse of useCallback / useMemo required merely to appease the dependency array linter.

Thus, by minimizing the use of React-specific API for your state, a paradox unfolds: by using less React, React can ultimately be used more. By adopting use-less-react's philosophy, our code not only gains in terms of scalability, but it also becomes accessible to a broader range of developers (including Node.js back-end developers), and it becomes viable for more sophisticated project types where the current limitations of Hooks severely restrict front-end expressiveness.


React: between dominance and perceived decline​

React is not in crisis. It is in a phase of maturity and fragmentation.

1. The undisputed dominance​

In terms of pure usage and market presence, React is the absolute king.

  • Usage and Adoption: Surveys like the "State of JS" consistently place React at the top for "Usage." The vast majority of companies using a front-end technology, use React.
  • Ecosystem and Jobs: React's ecosystem (Next.js, React Native, millions of NPM packages) and the number of job openings are unmatched. From this perspective, it is the safest and most stable choice on the market.

2. The erosion of hype and satisfaction​

This is where the "perceived decline" lies. The community's enthusiasm is shifting.

  • Satisfaction and interest: The same "State of JS" surveys show that developer satisfaction and interest in React have been declining for several consecutive years.
  • The competitor allure: Alternatives like Svelte and SolidJS boast extremely high satisfaction and interest rates. Many developers find these tools simpler, faster, and inherently free of the complexity (Hooks, V-DOM overhead, dependency arrays) that plague modern React development.

3. The true source of friction: internal complexity​

React's real problem is not external competitors, but the increasing complexity introduced by its own team and partners (Next.js/Vercel).

  • From simple library to complex framework: The introduction of React Server Components (RSC) and the App Router has fragmented the philosophy. React has ceased to be "just a UI library," becoming a paradigm that mixes server and client logic in a confusing way, steepening the learning curve.
  • The abandonment of simplicity: React's original promise — simple, declarative UI — has been replaced by a complex architecture focused also on server-side performance.

The frustrations we feel today stem from this conflict: a powerful system that has become significantly harder to use correctly.


The historical flaw: why OOP failed in Class Components​

To understand why use-less-react works, we must first understand why OOP (within React) was abandoned in the first place.

The old Class Components model was an an OOP implementation that had huge problems for UI description:

  • Wrapper hell: To reuse stateful logic, developers had to rely on Higher-Order Components (HOCs) or Render Props, creating an unreadable, layered, and difficult-to-debug DOM tree.
  • The mental model of the lifecycle: Methods like componentDidMount forced developers to think in terms of time, not state. This imposed an imperative, rather than declarative approach. Logic for a single feature often had to be scattered across many different methods, making the code fragile.

Hooks solved these UI problems by embracing a model of functional composition.


The new pains: why Hooks are the current issue​

The paradox arises: React abandoned OOP because it was problematic for the UI, but in doing so, it forced us to use the Hooks API (born for the UI) also for Business Logic, where OOP actually excels.

We needed a clear line between UI and Business Logics to delimit the realm of hooks, but there's no such line. Hooks are a mediocre tool for domain and global state logic. Their misuse is the root of React's three biggest architectural pain points:

1. Dependency hell and stale closures​

Hooks live within closures and "capture" state and prop values as they were at the time of their creation.

  • The problem: if you omit a value from a useEffect dependency array, the effect continues to use a stale version of that value, leading to unpredictable bugs.
  • The "solution": by just curing the symptom, the exhaustive-deps lint rule forces us to include everything, which in turn causes the next problem...

2. The plague of memoization (useMemo and useCallback)​

Since the dependency array is now filled with objects and functions, your useEffect re-runs on every single render, because in JavaScript, {} is never equal to {}.

  • The problem: You are forced to wrap every non-trivial function and object in a useCallback or useMemo solely to "stabilize" the Hook dependencies.
  • The result: Boilerplate code everywhere, consuming time and compute resources just to appease the React algorithm, not because your business logic requires it.

3. The absence of true Dependency Injection (DI)​

React does not have a true Dependency Injection system. The official answer is useContext, but this is a glorified service locator with a couple flaws:

  • any component consuming a context (useContext(MyContext)) re-renders every time any piece of data in that context changes, even if that component is only interested in an unchanged piece of data. This creates bad performance cascades
  • you can’t really switch providers dynamically at runtime, as they’re inherently static

An architectural antidote​

use-less-react is the bridge that resolves these issues, allowing React and OOP to excel in their respective domains:

ComponentRoleTechnologyAdvantages
Business LogicThe "Brain": Domain management, rules, services.OOP/VanillaJS ClassesTrue DI, Pure Testability, Classic Patterns.
User InterfaceThe "Face": Rendering, local event handling.React Components (JSX, Hook)Native V-DOM reactivity, UI composition.
The BridgeLogic → UI Synchronization.useReactiveInstance (built on useSyncExternalStore)Selective notifications, optimized performance.

Here is how the class-based architecture magically solves the three React pain points:

React Pain Pointuse-less-react Solution
Stale ClosuresNo Stale Closures: Class methods read this. this is always the current instance. The concept of "stale state" disappears.
useMemo/useCallback HellNo Memoization Boilerplate: Your instance is created once. It is stable by definition. You can pass store.addTodo at any level without ever needing to wrap it in useCallback.
Absence of True DITrue Dependency Injection: Your classes are OOP. They can receive dependencies in their constructor (new UserManager(authService)), solving a problem that OOP solved 30 years ago.

The trade-off (notifications)​

The only trade-off is the manual notification management via the notify method (or the @Notifies decorator). But this is nothing new: it’s just how any Observer pattern works. Actually, it’s a beneficial compromise:

  • it's explicit: you know exactly which lines of code trigger reactivity. And you can test those lines like everything else, if it’s non-trivial and you want to be sure it works correctly.
  • it's limited: this notification logic is confined to the frontier layer (the classes that "link" the pure logics with the UI). All internal logic remains clean.

The paradoxical thesis: more React, with less React​

use-less-react doesn't kill React; it aids its longevity and development quality.

  • Lightening the ecosystem: it enables direct, wrapper-free integration with existing vanilla JavaScript libraries. So no more installing both some-package and react-some-package.
  • More accessibility: it lowers the barrier to entry. Front-end developers coming from different frameworks (like Angular, Vue or Svelte), or Back-end developers (Node.js/TypeScript) can contribute directly to the business logic, isolated from the complexity of Hooks. From day one.
  • Easier to test: forget UI-specific utilities like act and waitForNextUpdate that weigh down the tests even when you're testing non UI-specific logics.
  • Focus on the core: it allows React to return to its most efficient role - a rendering engine for the V-DOM and a local UI state manager.
  • A sustainable choice: by making business logic easier to test, manage, and write, you alleviate the primary sources of developer frustration that push teams toward competing frameworks.
  • Seamless integration: use-less-react seamlessly integrates with data and logics coming from "standard" React API. These two worlds aren't mutually exclusive, they can effectively cooperate.

In conclusion, use-less-react offers an elegant way out of React's current "friction." By reintroducing powerful OOP patterns where they are most effective, you achieve a clearer, more testable, and more sustainable architecture.

If you are seeking stability, testability, and code that is easier to maintain, don't be afraid to use less React logic to help your entire React project work better.

Your App Just Got an Undo Button: Adding the Memento Pattern to use-less-react

· 6 min read
Fabio Fognani
random nerd

I'm thrilled to announce that the Memento Pattern is now officially supported and deeply integrated into the core of use-less-react! (from v0.6.0)

Read the technical documentation here!

This means adding robust "Undo" and "Redo" functionality to your complex state managers has never been easier, allowing your users to step back in time with a single click.

Wait, What is the Memento Pattern?​

Imagine you're designing a complex graphics editor, a state-heavy spreadsheet, or even just a simple text input. Your user makes a mistake. They expect to hit Ctrl+Z (or Cmd+Z) and revert their action.

The Half-Truth of React: MVVM Contaminated

· 6 min read
Fabio Fognani
random nerd

React is often categorized as an MVVM (Model-View-ViewModel) framework. This classification is based on two core principles it perfectly enforces:

  1. Unidirectional Data Flow: State flows strictly from Model to View (M → VM → V). The View does not directly mutate the Model; it sends commands back to the ViewModel/Component.
  2. Conceptual Separation: The code conceptually separates the Logic (VM) from the Presentation (V).

However, this is only half the truth. The full truth is that React's design, particularly with Hooks, enforces a critical architectural fusion that undermines the pattern's primary benefit—pure testability:

  • View (V): Your JSX output.
  • ViewModel (VM): The logic that exposes state and actions.

The fundamental issue is that Hooks mandate the fusion of the VM into the V. Your functional component contains the JSX, but it also becomes the seat of the ViewModel (via useState, useEffect, etc.). This fusion is the root of contamination: it forces business logic to become dependent on React's rendering APIs, effectively getting in the way of testability and killing separation of concerns.

Hooks Kill Architecture: The Price Of Sacrificing Classes

· 7 min read
Fabio Fognani
random nerd

The introduction of Hooks in React was presented as a revolution, freeing us from the complexity of classes and their lifecycle methods. We willingly accepted the trade-off: cleaner code, less boilerplate.

But in this process, we imposed a set of rules upon ourselves — often not fully grasped — that have effectively rendered entire categories of Design Patterns unused, and in some cases impossible.

We accepted the price of sacrificing classes, a core feature of the language we're using, and this is something I must admit I overlooked, back in the day. Now I'm asking myself how could that happen. The paradox is clear: the world's most popular UI library abruptly failed to support a core language feature, thereby crippling the expressive potential available to engineers. How could the front-end community think this was a good idea, from an architectural standpoint?

By the way, this is not a debate about "classes vs functions": it is a warning about what we lost at a structural level.

Implementing the State Pattern for safe Auth Flow Management with use-less-react

· 10 min read
Fabio Fognani
random nerd

Introduction: The Inevitable Complexity of User Flows​

Imagine you're tasked with implementing a standard authentication flow in a React application. The requirement seems simple:

  1. The app must first check the session (look for a stored token).
  2. If no session is found, redirect to the login page.
  3. If the session is valid, transition to the authenticated state, granting access to core features.

Most React developers immediately reach for a large, central custom hook / context to manage this logic. They will write something like this:

// File: use-auth.ts

function useAuth() {
const [status, setStatus] = useState<'checking' | 'login' | 'authenticated'>('checking');
const [user, setUser] = useState(null);

useEffect(() => {
// Initial session check logic here
const session = getSession();
if (session.user) {
setStatus('authenticated');
setUser(session.user);
} else {
setsStatus('login');
}
}, []);

const login = async (email: string, password: string) => {
const session = await loginApi(email, password);
if (session.user) {
setStatus('authenticated');
setUser(session.user);
}
};

return { status, login, user };
};

The Inevitable Feature Creep​

Then, reality hits. A new requirement lands on your desk: the user must not only be authenticated but must also complete their profile with additional data like address, company ID, etc., before accessing the main app.

This means ripping into the heart of your flow logic, modifying all tests for the central hook, and introducing nested conditionals.

You modify your hook, adding the new pending-profile state. After many delicate changes, you arrive at what you believe is a stable solution.

But then, life happens again. A new, critical security requirement arrives: You must integrate a mandatory 2-Factor Authentication (2FA) step between login/signup and the profile completion stage.

This is a true nightmare. It forces you to re-engineer the same block of code again. The logic for transition, validation, and conditional routing is now becoming complex and fragile:

// File: use-auth.ts

const login = async (email: string, password: string) => {
const session = await loginApi(data);
if (session.user) {
setUser(session.user)
}
};

useEffect(() => {
if (!is2FASet(user)) {
setStatus('2fa-required')
} else if (!isProfileComplete(user)) {
setStatus('pending-profile');
} else {
setStatus('authenticated');
}
}, [user]);

This monolithic approach has failed even in our very simplified example. We are constantly violating the Open/Closed Principle (OCP): modifying existing, working code instead of just extending it.

The Architectural Question​

Are we truly following the right architectural path for managing complex, evolving application behavior?

The answer is no. With use-less-react, you can manage this exact problem using a powerful object-oriented design pattern: the State Pattern, implemented as a Finite State Machine (FSM). This approach uses the power of Object-Oriented Programming (OOP) to encapsulate behavior, making your logic robust, extensible, and easy to test.

Architectural Guardrails: How Clean Separation of Concerns Dramatically Simplifies PR Reviews

· 8 min read
Fabio Fognani
random nerd

Introduction: The High Cost of Unconstrained Complexity​

In software development, the true cost of a feature is measured not just by the time it takes to write, but by the time it takes to debug, change, and — last but not least — review. When logic is unconstrained, a simple feature often turns into a complex pull request (PR) that demands deep, time-consuming investigation.

This post contrasts two architectural approaches to demonstrate how Separation of Concerns, enforced by a class-based Domain Model (like in use-less-react), dramatically simplifies the PR review process.


Scenario 1: Hooks-based implementation​

Imagine a PR submitted by a junior developer for a new feature. The core logic is housed in a single component file, putting together data and logics from different hooks.

The Component-Centric Trap: Why Domain Logic Needs Its Own Lifecycle

· 5 min read
Fabio Fognani
random nerd

Introduction​

The prevalence of React Hooks has anchored application state and logic firmly within the concept of Component. While this simplifies local UI state, it creates a fundamental architectural problem for complex applications: the subordination of core business logic to the React Component Lifecycle.

Logic packaged in a custom hook is intrinsically tied to where it is executed — it only "lives" as long as the component that uses it is mounted, and its execution is dependent on the component's render cycle.

This is the core argument for elevating state and logic into independent, vanilla Domain Entities.


1. Subordination to the Component Lifecycle​

A custom hook, by definition, must adhere to the Rules of Hooks. This dependency means that the lifecycle of the data and the logic it contains is entirely governed by the useEffect and useLayoutEffect calls within its composition.

Ensuring Type Safety: Integrating Zod for Secure State Hydration

· 4 min read
Fabio Fognani
random nerd

Introduction: The Challenge of Hydration​

In modern web applications, State Hydration is the process of restoring serialised application state from an external source (like a server-side render payload, a persistent layer, or a network cache) into a live JavaScript object. This is a critical and highly vulnerable step.

Since the source data is external and therefore untrusted, directly assigning it to a live object instance can lead to fatal runtime type errors if the data structure is corrupted or outdated.

This is where the combination of the Object-Oriented Design of our Domain Model and a robust Runtime Validation Library like Zod provides a secure, quick, and elegant solution.

The Boilerplate Tax 💸

· 6 min read
Fabio Fognani
random nerd

How use-less-react finally fixes React's boilerplate tax and makes clean architecture practical.​

You're a good developer. You believe in clean architecture. You've heard about separating your app into two "worlds":

  1. the World of Presentation (the UI): Your React components. The "Primary Adapter" that translates user clicks and keyboard tapping into application events.
  2. the World of Logic (the Core): Your business rules and application state. The pure TypeScript functions and classes that don't know or care about UI details.

You start building a new feature. Let's say, a simple dropdown menu.

"This is just UI state," you think. "It's simple. I'll just use useState."

function MyMenu() {
const [isOpen, setIsOpen] = useState(false); // Local. Easy.

return (
<div>
<button onClick={() => setIsOpen(o => !o)}>Toggle</button>
{isOpen && <div>Menu Content</div>}
</div>
);
}

This is clean & pragmatic. You're keeping state as local as possible until a different necessity arises.

Then, a new requirement lands on your desk.

"We're building a tutorial wizard, and it needs to be able to open that menu automatically to show the user where to click."

...and suddenly, you're not so sure about what's "local" and what's not.

Introducing @dxbox/use-less-react: React, without...

· 3 min read
Fabio Fognani
random nerd

There’s beauty in simplicity.
We build software on layers of abstractions, patterns, and libraries — and sometimes, we forget what all of that complexity was supposed to achieve in the first place.

@dxbox/use-less-react was born out of a simple question: can we use React purely as a reactive layer, without letting it invade the logics of our applications?

Or, if I may simplify:

What if React state management could be done in plain JavaScript?