Skip to main content
Version: v1.0

Introduction

Core principle

Logics first. React where strictly needed.

Over 8 years of working with React, I've repeatedly observed a problematic tendency: there is no such thing as a clear separation between UI and business logic. Business logic ends up mixed with React components through custom hooks, cascades of useEffect, useMemo, useCallback, and useRef.

This leads to some challenges:

  • Business logic becomes tightly coupled to React's lifecycle
  • Code becomes harder to test and requires React's testing utilities
  • Reusability across different contexts (SSR, workers, CLI tools) becomes difficult or impossible
  • The mental model becomes more complex as projects grow
  • Code Reviews become harder, because "everything can do everything" (components orchestrating hooks for business logics, hooks returning JSX... IYKYK).

The proposal

use-less-react is an experimental library that proposes an alternative approach: establishing a clear boundary by relegating React's role to the View layer only. It doesn't exclude the use of React, but offers a different path for organizing code.

The idea is simple: write your business logic in plain JavaScript objects or classes that exist outside React's lifecycle, and connect them to React components through a thin reactive layer.

A concrete example

Let's look at a real scenario. Here's a common pattern we've all seen many times:

// Business logic mixed with React
function Counter() {
const [count, setCount] = useState(0);
const [name, setName] = useState("");

const handleIncrement = useCallback(() => {
setCount(c => c + 1);
}, []);

return <div>{count}</div>;
}

This looks innocent because it's tiny example - and it works. But notice how the logic is embedded in the React component. If you wanted to use this logic in a Node.js script, a worker, or test it without React, you'd need to duplicate it in some way.

And it's not just a matter of common sense or good design principles. React forces you to write logics and state management under its API - but wasn't it supposed to be a UI library?

Since when you have to choose a UI library in order to determine how to write logics?

With use-less-react, you can structure components and logics differently, and separate their concerns clearly:

import { useDisposable } from '@dxbox/use-less-react/client';

function Counter() {
// this counter will be automatically disposed on component unmount
const counter = useDisposable(() => new ReactiveValue(0));
// this component subscribes to changes to the "count" reactive value
const { count } = useReactiveValues({ count: counter });

return (
<div>
// use the reactive value listened by the hook
{count}
// use the stable counter reference to call methods
<button onClick={() => counter.set(c => c + 1)}>+</button>
</div>
);
}

That's it. Decoupled business logics and UI, with automatic disposal of reactive properties to avoid memory leaks.

The key difference from the useState ("classic React") approach?

The business logic now exists completely independently of React. It doesn't use React API - it's totally, blissfully unaware of React. It's just plain JavaScript with some reactive properties (basically observables). The React component is a thin layer that subscribes to changes and renders the UI.

Now you can move your simple counter in a class (if you need inheritance) or a plain object (if you need/prefer composition).

Now you can add it to a ReactiveStore, with other reactive properties. Now you can use batched notifications with extreme control to avoid unnecessary re-renders, or you can use transaction-like API, to be able to rollback changes to many reactive properties in the blink of an eye.

Now you can use prefabs like Memento (for undo/redo capabilities), or Finite-State Machines (for wizards, or complex authentication flows, for example).

All of this without the need for many third party libraries. For example, many React users now use Zustand for state management, and have to install an additional plugin named Zundo for integrating Memento functionalities in their stores.

All of this without the need for third party libraries. And all of this, independently of the UI library. Pure, easy to test, elegant logics.

Did I catch your attention now?


React's Official Documentation says:

React is a JavaScript library for rendering user interfaces (UI). UI is built from small units like buttons, text, and images. React lets you combine them into reusable, nestable components. From web sites to phone apps, everything on the screen can be broken down into components.

How you write business logic should never depend on which UI library you choose.

Plain objects instead of classes

Don't like classes, or need a more flexible object composition? You can use plain objects, they are supported as well.

import { makeDisposableObject } from "@dxbox/use-less-react/classes";

function createCounter() {
// define your reactive values
const count = new ReactiveValue(0);

// use makeDisposableObject to avoid memory leaks
return makeDisposableObject({
count,
increment: () => { // define your logics
count.set((c) => c + 1);
},
});
// Automatically detects 'count' as disposable and adds dispose() method
}

// counter.tsx - the React component
import { useDisposable } from '@dxbox/use-less-react/client';

function Counter() {
// still fully usable with useDisposable
const counter = useDisposable(() => createCounter());
// still fully reactive
const { count } = useReactiveValues({ count: counter.count });

return (
<div>
{count}
<button onClick={() => counter.increment()}>+</button>
</div>
);
}

What this kind of approach enables

1. Code that works everywhere

Since the business logic isn't tied to React, you can use it in any context:

// Works in Node.js
const manager = new CounterManager();
manager.increment();
console.log(manager.count.get()); // 1

// Works in a Web Worker
// Works in a CLI tool
// Works in tests without React

2. Easier testing

You can test business logic without React testing utilities:

// Simple unit test, no React needed
test("increment increases count", () => {
const manager = new CounterManager();
manager.increment();
expect(manager.count.get()).toBe(1);
});

No act or waitForNextUpdate calls in here. Easier to read and to understand, right?

3. Better reusability

The same logic can be used in different contexts. For example, in a Next.js app, you might use the same classes on both server and client:

// Server-side
const counter = createCounter();
const initialCount = counter.count.get();

// Client-side
import { useDisposable } from "@dxbox/use-less-react/client";

function ClientComponent() {
const counter = useDisposable(() => createCounter());
// Same logic, different context
// Automatically disposed when component unmounts
}

4. Using derived values

You can compose reactive values together with ComputedValues:

@AutoDispose
class ShoppingCart implements Disposable {
items = new ReactiveArray<CartItem>([]);
// items is automatically detected and disposed (has Symbol.dispose)

total = new ComputedValue(
() => this.items.get().reduce((sum, item) => sum + item.price, 0),
[this.items],
);
// total is automatically detected and disposed (has Symbol.dispose)

[Symbol.dispose](): void {
// Custom cleanup logic if needed
}
}

This is just a regular JavaScript class. The reactive values (ReactiveArray, ComputedValue) are properties that can notify React (or any other system) when they change.

5. Type safety

Everything is fully typed with TypeScript:

const store = new ReactiveStore({
count: new ReactiveValue(0),
user: new ReactiveObject({ name: "", age: 0 }),
});

// TypeScript knows the types
store.values.count.set(10); // OK
store.values.user.set({ name: "John", age: 30 }); // OK
store.values.count.set("invalid"); // Type error

The reactive layer

The "magic" that connects plain JavaScript objects to React is simple: reactive values can notify subscribers when they change. React components subscribe to these changes through two hooks:

  • useReactiveValues: Subscribe to individual reactive values
  • useReactiveStoreValues: Subscribe to values from a store. You can think of stores as sets of reactive values upon which you can perform batched notifications, or transaction-like blocks of codes (for performing rollbacks, if you need to). Learn more in the API Reference docs.

These hooks handle the subscription and re-rendering automatically. The business logic doesn't need to know about React at all.

When this approach makes sense

This approach is particularly useful when:

  • you have complex business logic that you want to keep separate from UI concerns
  • you need to reuse logic across different contexts (client, server, workers)
  • you want to test business logic without React testing utilities
  • you're building applications that need to work in both SSR and CSR contexts
  • you prefer organizing code in objects or classes rather than hooks

An experimental approach

This is an experimental library exploring an alternative way to structure React applications. It's not meant to replace React hooks or suggest that the traditional approach is "wrong". Instead, it offers a different perspective on how to organize code when you want a clear separation between business logic and UI.

The goal is to let React do what it does best — rendering UI — while keeping business logic in plain JavaScript that can work anywhere.

Note: when I say "plain JavaScript", I mean the business logic doesn't depend on React.

You may think: "What's the difference between using ReactiveValue from use-less-react and using useState from React?"

The difference is that useState only works within React's context. ReactiveValue is a separate abstraction: reactivity is handled by the library, and React only "observes" the changes. It's the difference between "logic that uses React" and "logic that React can use": you can use ReactiveValue anywhere, while you can use useState only in a React application, and specifically in a client-side context.