Getting Started
Install
npm install @react-logic/react-logic
This package includes the three core pieces: core (the useLogic hook), state (state, computedState, effect, batch), and di (inject, Injector, tokens).
Optional helpers like asyncState and fetchState live in @react-logic/utils. Install it when you need them:
npm install @react-logic/utils
A first logic class
import { useLogic, state, computedState } from '@react-logic/react-logic';
class CounterLogic {
count = state(0);
doubled = computedState(() => this.count() * 2);
inc() { this.count(this.count() + 1); }
dec() { this.count(this.count() - 1); }
}
export const Counter = () => {
const logic = useLogic(CounterLogic);
return (
<div>
<p>Count: {logic.count()} (×2: {logic.doubled()})</p>
<button onClick={() => logic.inc()}>+</button>
<button onClick={() => logic.dec()}>−</button>
</div>
);
};
That's the whole loop:
- The class holds state via
state()and derived values viacomputedState(). useLogiccreates an instance and re-renders the component when any signal it reads changes.- The component reads a signal by calling it (
logic.count()) and writes by calling it with a value (this.count(next)).
No useState, no useEffect, no manual caching. The logic class is the source of truth.
Sharing state with dependency injection
When two components need the same state, lift it to a service — another class — and inject it into both logic classes:
import { inject } from '@react-logic/react-logic';
class CartStore {
items = state<Item[]>([]);
add(item: Item) { this.items([...this.items(), item]); }
}
class CartCount {
cart = inject(CartStore);
}
class CartList {
cart = inject(CartStore);
}
// No setup needed — both classes resolve `CartStore` from the root scope,
// and get the same instance.
<Header /> {/* uses CartCount → reads cart.items().length */}
<Sidebar /> {/* uses CartList → reads cart.items() */}
That's it. Services are registered in a root scope by default, so inject(CartStore) just works.
Optionally, use <Injector provide={[…]}> when you want to scope a service to a subtree (independent instances, or providing a different implementation for that branch).
Cleanup
Anything a logic class or service starts (intervals, listeners, subscriptions) is cleaned up via onDestroy():
import { onDestroy } from '@react-logic/react-logic';
class TimeService {
now = state(Date.now());
constructor() {
const id = setInterval(() => this.now(Date.now()), 1000);
onDestroy(() => clearInterval(id));
}
}
For logic classes, the callback fires on component unmount. For services, on <Injector> unmount.
Where to next
- Concepts — how signals, DI, and scopes work.
- Guides — walkthroughs of each part of the library.
- Recipes — code examples for common problems.
- API Reference — generated reference for every export.