@vorthain/react-state
Version:
A minimal and reactive state management library for React, enabling automatic UI updates through direct, mutable state changes.
688 lines (546 loc) • 16.5 kB
Markdown
# 🚀 @vorthain/react-state
[](https://www.npmjs.com/package/@vorthain/react-state)
[](https://www.npmjs.com/package/@vorthain/react-state)
[](https://bundlephobia.com/package/@vorthain/react-state)
**Zero-configuration reactive state for React**
Write natural, mutable code and watch your components update automatically. No reducers, no dispatchers, no complex patterns.
```jsx
const state = useVstate({
count: 0,
increment: () => state.count++,
get isEven() { return state.count % 2 === 0; }
});
return (
<div>
<p>{state.count} ({state.isEven ? 'Even' : 'Odd'})</p>
<button onClick={state.increment}>+1</button>
</div>
);
```
## Installation
```bash
npm install @vorthain/react-state
```
## Why Choose Simplicity?
Compare these two approaches for updating a nested property:
**Traditional React (useReducer + immutable updates):**
```jsx
const drawersReducer = (state, action) => {
switch (action.type) {
case 'UPDATE_PAGE_TITLE':
return {
...state,
drawers: state.drawers.map((drawer) => ({
...drawer,
folders: drawer.folders.map((folder) => ({
...folder,
pages: folder.pages.map((page) =>
page.id === action.pageId ? { ...page, title: action.newTitle } : page
)
}))
}))
};
default:
return state;
}
};
// Usage
dispatch({ type: 'UPDATE_PAGE_TITLE', pageId: page.id, newTitle: page.title + ' - Edited' });
```
**Vorthain (direct mutation):**
```jsx
page.title = page.title + ' - Edited';
```
## Quick Start
### Local State with useVstate
```jsx
import { useVstate } from '@vorthain/react-state';
function Counter() {
const state = useVstate({
count: 0,
increment: () => state.count++,
decrement: () => state.count--,
get doubled() { return state.count * 2; }
});
return (
<div>
<p>Count: {state.count}</p>
<p>Doubled: {state.doubled}</p>
<button onClick={state.increment}>+1</button>
<button onClick={state.decrement}>-1</button>
</div>
);
}
```
### Global State with useVglobal
**1. Create your stores:**
```jsx
// stores/TodoStore.js
export class TodoStore {
/** @param {import('./RootStore').RootStore} rootStore */
constructor(rootStore) {
this.rootStore = rootStore; // For cross-store communication
this.todos = [];
}
addTodo = (text) => {
this.todos.push({
id: Date.now(),
text,
done: false,
priority: 'medium'
});
// Can access other stores
this.rootStore.userStore.updateLastActivity();
}
toggleTodo = (id) => {
const todo = this.todos.find(t => t.id === id);
if (todo) todo.done = !todo.done;
}
get completedCount() {
return this.todos.filter(t => t.done).length;
}
}
// stores/UserStore.js
export class UserStore {
/** @param {import('./RootStore').RootStore} rootStore */
constructor(rootStore) {
this.rootStore = rootStore;
this.name = 'John';
this.preferences = { theme: 'dark' };
this.lastActivity = Date.now();
}
updateLastActivity = () => {
this.lastActivity = Date.now();
}
setTheme = (theme) => {
this.preferences.theme = theme;
}
}
// stores/RootStore.js
import { TodoStore } from './TodoStore';
import { UserStore } from './UserStore';
export class RootStore {
constructor() {
this.todoStore = new TodoStore(this);
this.userStore = new UserStore(this);
}
// Root-level computed properties can access all stores
get appTitle() {
return `${this.userStore.name}'s Todos (${this.todoStore.completedCount} done)`;
}
}
```
**2. Create a hook that initializes and provides the store:**
```jsx
// hooks/useStore.js - Everything in one place!
import { createVorthainStore, useVglobal } from '@vorthain/react-state';
import { RootStore } from '../stores/RootStore';
// Initialize store
createVorthainStore(RootStore);
// Export typed hook for use in components
/** @returns {import('../stores/RootStore').RootStore} */
export const useStore = () => useVglobal();
```
**3. Import and use anywhere:**
```jsx
// components/TodoApp.jsx
import { useStore } from '../hooks/useStore';
function TodoApp() {
const store = useStore(); // Full autocomplete!
return (
<div>
<h1>{store.appTitle}</h1>
<p>User: {store.userStore.name}</p>
<p>Todos: {store.todoStore.completedCount}/{store.todoStore.todos.length}</p>
<button onClick={() => store.todoStore.addTodo('New Task')}>
Add Todo
</button>
{store.todoStore.todos.map(todo => (
<div key={todo.id}>
<input
type="checkbox"
checked={todo.done}
onChange={() => store.todoStore.toggleTodo(todo.id)}
/>
{todo.text}
</div>
))}
</div>
);
}
// components/UserProfile.jsx
import { useStore } from '../hooks/useStore';
function UserProfile() {
const store = useStore(); // Same hook, full autocomplete!
return (
<div>
<h2>{store.userStore.name}</h2>
<button onClick={() => store.userStore.setTheme('light')}>
Switch Theme
</button>
</div>
);
}
```
## Fine-Grained Reactivity with vGrip
Optimize components to only re-render when their specific dependencies change:
```jsx
import { vGrip } from '@vorthain/react-state';
// Individual todo items only re-render when their data changes
const TodoItem = vGrip(({ todo }) => {
return (
<div>
<input
type="checkbox"
checked={todo.done}
onChange={() => todo.done = !todo.done}
/>
<span>{todo.text}</span>
<button onClick={() => todo.priority = 'high'}>
Mark High Priority
</button>
</div>
);
});
// Todo list only re-renders when array changes
const TodoList = vGrip(() => {
const store = useVglobal();
return (
<div>
<h2>Todos ({store.todos.length})</h2>
{store.todos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</div>
);
});
```
## Batching Updates with vAction
Batch multiple mutations into a single re-render:
```jsx
import { vAction } from '@vorthain/react-state';
const addManyTodos = () => {
vAction(() => {
// All mutations happen, then one re-render
for (let i = 0; i < 100; i++) {
store.todos.push({ text: `Todo ${i}` });
}
store.user.preferences.lastBulkAdd = Date.now();
});
};
```
## Core Features
### Direct Mutations
Everything works with natural JavaScript:
```jsx
// Objects
state.user.name = 'Jane';
state.user.settings.theme = 'light';
// Arrays
state.items.push(newItem);
state.items[0] = updatedItem;
state.items.splice(2, 1);
// Maps
state.cache.set('key', value);
state.cache.delete('key');
// Sets
state.tags.add('react');
state.tags.delete('vue');
```
### Computed Properties (Getters)
```jsx
const state = useVstate({
todos: [],
filter: 'all',
get filteredTodos() {
switch(state.filter) {
case 'done': return state.todos.filter(t => t.done);
case 'active': return state.todos.filter(t => !t.done);
default: return state.todos;
}
},
get completedCount() {
return state.todos.filter(t => t.done).length;
},
get activeCount() {
return state.todos.filter(t => !t.done).length;
},
get completedPercentage() {
return state.todos.length > 0
? Math.round((state.completedCount / state.todos.length) * 100)
: 0;
}
});
```
### Deep Reactivity
Nested changes are automatically tracked:
```jsx
const state = useVstate({
company: {
name: 'Acme Corp',
employees: [
{ name: 'John', tasks: ['Design', 'Code'] },
{ name: 'Jane', tasks: ['Test', 'Deploy'] }
]
}
});
// All of these trigger re-renders:
state.company.name = 'New Corp';
state.company.employees[0].name = 'Johnny';
state.company.employees[1].tasks.push('Document');
```
## TypeScript & JavaScript Support
### TypeScript
**Full TypeScript support with proper typing:**
**Note on Strict Mode:** If you have `"strict": true` in your `tsconfig.json`, `useVstate`'s type inference works best when you provide an initializer function.
**Do this:**
```tsx
const state = useVstate(() => ({ count: 0 }));
```
**Instead of this:**
```tsx
const state = useVstate({ count: 0 });
```
```tsx
// stores/RootStore.ts
import { UserStore } from './UserStore';
import { TodoStore } from './TodoStore';
interface Todo {
id: number;
text: string;
done: boolean;
priority: 'low' | 'medium' | 'high';
}
export class TodoStore {
rootStore: RootStore;
todos: Todo[] = [];
constructor(rootStore: RootStore) {
this.rootStore = rootStore;
}
// ... other methods
}
export class UserStore {
rootStore: RootStore;
name = 'Jane';
constructor(rootStore: RootStore) {
this.rootStore = rootStore;
}
// ... other methods
}
export class RootStore {
todoStore: TodoStore;
userStore: UserStore;
constructor() {
this.todoStore = new TodoStore(this);
this.userStore = new UserStore(this);
}
}
// hooks/useStore.ts - Initialize and export typed hook
import { createVorthainStore, useVglobal } from '@vorthain/react-state';
import { RootStore } from '../stores/RootStore';
// Initialize store (happens once)
createVorthainStore(RootStore);
// Export typed hook
export const useStore = (): RootStore => {
return useVglobal();
};
```
### JavaScript
**Full autocomplete support using JSDoc:**
```jsx
// stores/TodoStore.js
export class TodoStore {
/** @param {import('./RootStore').RootStore} rootStore */
constructor(rootStore) {
this.rootStore = rootStore;
this.todos = [];
}
// ... methods
}
// stores/UserStore.js
export class UserStore {
/** @param {import('./RootStore').RootStore} rootStore */
constructor(rootStore) {
this.rootStore = rootStore;
this.name = 'John';
}
// ... methods
}
// stores/RootStore.js
import { TodoStore } from './TodoStore';
import { UserStore } from './UserStore';
export class RootStore {
constructor() {
this.todoStore = new TodoStore(this);
this.userStore = new UserStore(this);
}
get appTitle() {
return `${this.userStore.name}'s Todos`;
}
}
// hooks/useStore.js - Initialize and export typed hook
import { createVorthainStore, useVglobal } from '@vorthain/react-state';
import { RootStore } from '../stores/RootStore';
// Initialize store (happens once)
createVorthainStore(RootStore);
// Export typed hook
/** @returns {RootStore} */
export const useStore = () => {
return useVglobal();
};
```
## API Reference
### `useVstate(initialState)`
Creates reactive local state for a component.
```jsx
const state = useVstate({
count: 0,
increment: () => state.count++
});
```
### `createVorthainStore(StoreClass)`
Initializes global store. Call once at app startup.
```jsx
createVorthainStore(RootStore);
```
### `useVglobal()`
Returns the global store instance.
```jsx
const store = useVglobal();
```
### `vGrip(Component)`
Higher-order component that adds fine-grained reactivity. Component only re-renders when accessed properties change.
```jsx
const OptimizedComponent = vGrip(({ data }) => {
return <div>{data.value}</div>;
});
```
### `vAction(fn)`
Batches multiple mutations into a single re-render.
```jsx
vAction(() => {
state.a = 1;
state.b = 2;
state.c = 3;
});
```
## Common Patterns
### Cross-Store Communication
```jsx
// For JavaScript with autocomplete
class TodoStore {
/** @param {RootStore} rootStore */
constructor(rootStore) {
this.rootStore = rootStore;
this.todos = [];
}
addTodo = (text) => {
this.todos.push({ text, done: false });
// Access other stores with full autocomplete
this.rootStore.uiStore.showNotification('Todo added!');
this.rootStore.analyticsStore.track('todo_added');
}
}
class RootStore {
constructor() {
this.todoStore = new TodoStore(this);
this.uiStore = new UIStore(this);
this.analyticsStore = new AnalyticsStore(this);
}
}
```
### Async Actions
```jsx
const state = useVstate({
data: null,
loading: false,
error: null,
async fetchData() {
state.loading = true;
state.error = null;
try {
const response = await fetch('/api/data');
state.data = await response.json();
} catch (err) {
state.error = err.message;
} finally {
state.loading = false;
}
}
});
```
### Computed Values with Parameters
```jsx
const state = useVstate({
todos: [],
// Use a method instead of getter for parameterized computed values
getTodosByPriority(priority) {
return state.todos.filter(t => t.priority === priority);
},
// Can still use getters for simple computed values
get highPriorityCount() {
return state.getTodosByPriority('high').length;
}
});
```
### Combining Local and Global State
```jsx
function TodoEditor() {
const store = useVglobal();
const state = useVstate({
editingId: null,
tempText: '',
startEdit(todo) {
state.editingId = todo.id;
state.tempText = todo.text;
},
saveEdit() {
const todo = store.todos.find(t => t.id === state.editingId);
if (todo) {
todo.text = state.tempText;
state.editingId = null;
}
},
get isEditing() {
return state.editingId !== null;
}
});
return (
<div>
{store.todos.map(todo => (
<div key={todo.id}>
{state.editingId === todo.id ? (
<input
value={state.tempText}
onChange={e => state.tempText = e.target.value}
onBlur={state.saveEdit}
/>
) : (
<span onClick={() => state.startEdit(todo)}>
{todo.text}
</span>
)}
</div>
))}
</div>
);
}
```
---
**Ready to simplify your state management?**
```bash
npm install @vorthain/react-state
```
---
## How It Works
<small>
The magic behind `@vorthain/react-state` is a system of **ES6 Proxies** that wraps your state objects. This allows the library to intercept property access and mutations without requiring you to use special functions or immutable patterns.
* **Dependency Tracking**: When your component renders, every property of the state object you access (e.g., `state.count`) is recorded. The `get` trap of the Proxy registers the component as a "subscriber" to that specific property. For computed properties (getters), it tracks all the underlying properties they access, creating a dependency graph.
* **Automatic Updates**: When you mutate a property (e.g., `state.count++`), the Proxy's `set` trap is triggered. It looks up all the components that subscribed to that property and queues them for a re-render. This process is highly efficient because only the components that actually depend on the changed data are updated.
* **Data Structure Support**: The library uses specialized Proxy handlers to support various data structures, ensuring reactivity is preserved everywhere:
* **Objects**: Plain objects are deeply wrapped in Proxies. Any change to a nested property (e.g., `state.user.preferences.theme = 'dark'`) will trigger an update.
* **Arrays**: Methods that mutate arrays (`push`, `pop`, `splice`, etc.) are intercepted. Calling `state.items.push(newItem)` notifies subscribers of both the new item and the array's `length` property.
* **Maps & Sets**: Methods like `set`, `add`, `delete`, and `clear` are also proxied. Changing a Map or Set will correctly trigger updates for components that iterate over them or access their `size`.
* **Fine-Grained Rendering (`vGrip`)**: The `vGrip` HOC enhances this by creating a dedicated tracker for each wrapped component. Instead of the entire component subscribing to state changes, `vGrip` tracks dependencies *during the render phase* and ensures the component only re-renders if the exact data it used has changed.
* **Update Batching (`vAction`)**: To prevent multiple, rapid-fire re-renders from a series of mutations, `vAction` wraps them in a batch. It collects all notifications and then triggers a single, consolidated re-render at the end of the action.
</small>