@diginet/use-reactive v5.0.2
___ _ _
_ _ ___ ___| _ \___ __ _ __| |_(_)_ _____
| || (_-</ -_) / -_) _` / _| _| \ V / -_)
\_,_/__/\___|_|_\___\__,_\__|\__|_|\_/\___|useReactive
useReactive is a custom React hook that provides a reactive state object using a Proxy. Methods on the object use this to access members, enabling an object-oriented approach to encapsulating data and behavior. You can subscribe to property changes.
State modifications can be saved to a history with support for undo, redo, revert and snapshot / restore.
A companion React context is available for sharing reactive state effectively across a component hierarchyโsee createReactiveStore below. The hook returned by this function uses another Proxy to make the store object properties used by each component reactive.
Minimal example:
const [state] = useReactive(
{
count: 0,
increment() {
this.count++;
}
}
...
<p>Count: { state.count }</p>
<button onClick={ state.increment }>Increment</button>Basic usage in a React component using subscribe and history:
const MyComponent = () => {
const [state, subscribe, history] = useReactive(
{ // Reactive state object with methods
count: 0,
increment() {
this.count++;
}
},
{ // Options
async init(state, subscribe, history) {
// init method only runs once
subscribe(() => [ this.count ], (state, key, value, previous) => {
console.log(`${ key } changed from ${ previous } to ${ value }`);
})
history.enable(true); // Enable history
this.count++;
await delay(5000);
history.undo(); // Undo the latest change to the state object
}
});
return <div>
<p>Count: { state.count }</p>
<button onClick={ state.increment }>Increment</button>
</div>
}Note: The function increment above can be declared without the function keyword when using object shorthand syntax.
Features
- โก Fine-grained reactivity with direct property access.
- ๐๏ธ Full TypeScript support with generics for
IntelliSenseand type checking. - ๐ Auto-bound methods on the state object, allowing
thisto be used inside them. ๐ฏ One-time initialization function can be specified in the options argument.
๐งฎ Supports computed properties (via getters).
โก Efficient rendering: State changes trigger only partial React re-renders.
- โณ Supports asynchronous methods seamlessly.
- ๐ณ Deeply nested reactive objects are fully supported.
- ๐ Effect handling similar to
useEffect, configurable via options. - ๐ก Property-level subscriptions to react to specific state changes.
- ๐ Built-in history management with undo functionality.
Live preview
https://stackblitz.com/edit/vitejs-vite-mcpb2gpf?file=src%2FApp.tsx
Contents
Installation
npm install @diginet/use-reactiveor
yarn add @diginet/use-reactiveAPI
JavaScript (TL;DR)
const [state, subscribe, history] = useReactive(inputState, options)
inputState: The state object with properties and methods.options?: An options object, see below.
The returned tuple:
state: The reactive state object (a Proxy) to the initial state that is continuously updated on subsequent re-renders with any changes such as props and functions. Functions are bound to this object making it possible to usethis.subscribe: Optional subscribe method with a callback. Subscribe to single properties or all properties of an object.history: Optional API for history functions such as undo, redo, snapshot and restore. Undo and restore applies the individual changes in reverse order.
TypeScript
const [ state, subscribe, history ] = useReactive<T extends object>( inputState: T, options?: RO<T> ): [ T, S<T>, H<T> ]T is the state object, S is a subscribe function and E is an effect function, like React useEffect. RO is an options object, see below.
Parameters:
inputState: The state object. Can be of any type. Functions may be async and thethiskeyword will refer to the returned Proxy of this object. Functions can be declared without thefunctionkeyword (object shorthand notation). Do not use arrow functions, they will makethisrefer to the global scope.options?: Optional object with options:
init?: Function to runs once only. Arguments:- state: The reactive state object. Can also be referenced by
thisif not using an arrow function. - subscribe: The subscribe function.
- history: The history API
- state: The reactive state object. Can also be referenced by
effects?: Array of side effects that can run when a dependency changes. Each effect is a pair of:- An effect function. Return a cleanup function if needed. Arguments:
- state: The reactive state object. Can also be referenced by
thisif not using an arrow function. - subscribe: The subscribe function.
- history: The history API
- state: The reactive state object. Can also be referenced by
- A function returning a dependency array.
- An effect function. Return a cleanup function if needed. Arguments:
historySettings?: Settings for the
historyfunction.
Returns:
- A tuple:
[0]: The state object wrapped by a Proxy that updates the React state reactively when its properties change.[1]: A function for subscribing to property changes.- Arguments:
targets: A function returning a state property or an array of state properties to subscribe to.callback: A callback function(this: T, key: keyof T, value: unknown, previous: unknown)
- Returns an unsubscribe function
- Arguments:
- 2: A
historyinterface with undo, redo, revert any previous change, and rollback to any previous state. See history below.
Upgrade
Version 2.0: useReactive returns a tuple with the reactive object and a subscribe function. Simply change const state = useReactive({}) to const [state] = useReactive({}), disregarding the new subscribe function.
Version 2.1 adds history.
Version 3.0 uses a function to return the dependence array for effects. This makes it possible to directly reference properties of the reactive state object using the this keyword.
Version 4.0: the useReactive function has an options argument instead of individual items. The context is now an object with state, subscribe and history properties.
Examples
- Using components props and other data
- Simple counter
- Array values
- Computed property
- Async state update
- Nested state
- Single effect
- Multiple effects
- Subscribe
Using components props and other data
Props passed to a component can be used directly by methods on the useReactive object. The object state is retained while methods use the latest values:
const Sum = ({ value }: { value: number }) => {
const [someState, setSomeState] = useState(0)
const [state] = useReactive({
initial: 100,
get sum() {
return this.initial + value + someState;
}
});
return (
<div>
<h3>Sum example</h3>
<p>Sum: { state.sum }</p>
<button onClick={() => setSomeState(someState + 2) }>Increase someState</button>
</div>
);
};
const TestSum = () => {
const [state] = useReactive({ value: 0 });
return <div>
<Sum value={ state.value } />
<button onClick={() => state.value++ }>Increment value</button>
</div>
}Simple counter
const Counter = () => {
const [state] = useReactive({ count: 0 });
return (
<div>
<h3>Counter</h3>
<p>Count: {state.count}</p>
<button onClick={() => state.count++}>Increment</button>
<button onClick={() => state.count--}>Decrement</button>
</div>
);
};Array values
const ArrayExample = () => {
const [state] = useReactive({
todos: ['hello'],
addWorld() {
this.todos = [...this.todos, ' world'];
},
addExclamation() {
this.todos.push('!'); // Also reactive
}
});
return (
<div>
_________________________________
<h3>Array Example</h3>
<p>todos: {state.todos.map(todo => todo)}</p>
<button onClick={state.addWorld}>Add world</button>
<button onClick={state.addExclamation}>Add !</button>
</div>
);
};Computed property
const ComputedPropertyExample = () => {
const [state] = useReactive({
count: 2,
get double() {
return this.count * 2;
},
});
return (
<div>
_________________________________
<h3>Computed Property</h3>
<p>Count: {state.count}</p>
<p>Double: {state.double}</p>
<button onClick={() => state.count++}>Increment</button>
</div>
);
};Async state update
const AsyncExample = () => {
const [state] = useReactive({
count: 0,
async incrementAsync() {
await new Promise((resolve) => setTimeout(resolve, 1000));
this.count++;
},
});
return (
<div>
_________________________________
<h3>Async Update</h3>
<p>Count: {state.count}</p>
<button onClick={() => state.incrementAsync()}>Increment Async</button>
</div>
);
};Nested state
const NestedStateExample = () => {
const [state] = useReactive({
nested: { value: 10 },
});
return (
<div>
_________________________________
<h3>Nested State</h3>
<p>Nested Value: {state.nested.value}</p>
<button onClick={() => state.nested.value++}>Increment Nested</button>
</div>
);
};Single effect
const SingleEffectExample = () => {
const [state] = useReactive(
{ count: 0 },
{
effects: [[
function () {
console.log("Count changed:", this.count);
return () => console.log('bye');
},
function () {
return [this.count];
}
]]
}
);
return (
<div>
_________________________________
<h3>Single Effect</h3>
<p>Count: {state.count}</p>
<button onClick={() => state.count++}>Increment</button>
</div>
);
};Multiple effects
const MultipleEffectsExample = () => {
const [state] = useReactive(
{ count: 0, text: "Hello" },
{
effects: [
[function () { console.log("Count changed:", this.count); }, function () { return [this.count];}],
[function () { console.log("Text changed:", this.text); }, function () { return [this.text];}],
]
}
);
return (
<div>
_________________________________
<h3>Multiple Effects</h3>
<p>Count: {state.count}</p>
<p>Text: {state.text}</p>
<button onClick={() => state.count++}>Increment Count</button>
<button onClick={() => (state.text = "World")}>Change Text</button>
</div>
);
};Subscribe
Features
Subscribe example
const SubscribedCounter = () => {
const [state, subscribe] = useReactive({
count: 0,
},
{
init(state, subscribe) {
subscribe(() => [this.count], (key, value, previous) => {
console.log(`${key} changed from ${previous} to ${value}`);
});
}
});
return (
<div>
<h3>Subscribed Counter</h3>
<p>Count: {state.count}</p>
<button onClick={() => state.count++}>Increment</button>
<button onClick={() => state.count--}>Decrement</button>
</div>
);
};Subscription API
subscribe(targets: () => unknown | unknown[], callback: SC<T>, recursive?: boolean) => () => void
| Argument | Description |
|---|---|
targets | A function returning a reference to a property in the reactive object such as function() { return this.counter; } or a function returning an array of properties: function() { return [this.counter1, this.counter2, this.obj]; }. |
callback | A callback function with arguments key, value and previous where key is the property name, value is the new property value and previous is the previous property value. |
recursive | For nested object properties, subscribe to all properties when true. Default is false. Use 'deep' to subscribe also to deeply nested object properties. |
History
The useReactive hook provides a built-in history tracking system that allows you to undo, revert and rollback state changes efficiently. The current point in the history can be saved as a snapshot for later restore.
The history interface is returned as the optional third element of the tuple returned by useReactive.
Features
- Undo โ Revert all or the last state change.
- Redo โ Reapply the last undone state change or all undone changes.
- Revert/rollback โ Undo multiple changes.
- Snapshot/restore โ Save or restore a point in history.
Usage example
const ExampleComponent = () => {
const [state, , history] = useReactive(
{ count: 0 },
{ historySettings: { enabled: true } }
);
return (
<div>
<h2>Count: {state.count}</h2>
<button onClick={() => state.count++}>Increment</button>
<button onClick={() => history.undo()}>Undo</button>
<button onClick={() => history.redo()}>Redo</button>
</div>
);
};History API
| Function | Description |
|---|---|
| enable(enabled?: boolean, maxDepth?: number) | Enable or disable history tracking (unless enabled is left out). Returns current state. |
undo(index?: number): void | Undo the last change or up to a given index (0 for all). |
redo(all?: boolean): void | Redo the last undo or all of the saved redo stack. |
revert(index: number): void | Undo a specific change by index. |
snapshot(): string | null | Save a point in history. Return value null signifies the empty state (all saved changes will removed). |
restore(id: string | null): void | Restore a saved snapshot. |
clear(): void | Clear the history. |
createReactiveStore
The createReactiveStore function provides a shared reactive state using a React context and useReactive. It allows components to access and modify a reactive state while ensuring property updates trigger re-renders. Only components using a changed property will re-render.
Usage
Define a shared store
Initialize the global store like this:
import { createReactiveStore } from "@diginet/useReactive";
const [ ReactiveStoreProvider, useReactiveStore ] = createReactiveStore({
counter: 0,
user: { name: "John Doe", age: 30 },
});Wrap the application
Wrap your components with yourReactiveStoreProvider to enable global state access:
function App() {
return (
<ReactiveStoreProvider>
<div>
<h3>Shared reactive state example</h3>
<Counter />
<UserInfo />
</div>
</ReactiveStoreProvider>
);
}Use the global store in components
const Counter = () => {
const [store, subscribe, history] = useReactiveStore();
return (
<div>
<h4>Counter: {store.counter}</h4>
<button onClick={() => store.counter++}>Increment</button>
<button onClick={() => store.counter--}>Decrement</button>
</div>
);
};
const UserInfo = () => {
const [store, subscribe, history] = useReactiveStore();
return (
<div>
<h4>User: {store.user.name}, Age: {store.user.age}</h4>
<button onClick={() => store.user.age++}>Increase Age</button>
</div>
);
};Features
Reactive Global State - Changes automatically trigger re-renders.
Lightweight & Simple - No external state management libraries needed.
Flexible - Can be used in any React project requiring a shared state.
This makes createReactiveStore a tool for managing shared reactive state in React applications.
useReactiveStore API
| Return tuple | Description |
|---|---|
| store | The reactive state object that is the shared store. |
| subscribe | A subscribe function, see Subscribe above. |
| history | The history interface, see History above. |
Contributions
Contributions are welcome, create a pull request.
Add tests for new functionality.
Debugging tests with breakpoints can be done from Visual Studio Codes JavaScript console with:
npm test
or
npx vitest --inspect --run --no-file-parallelism --testTimeout=3600000 --max-workers=1License
MIT
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago