@okenneth/reactive-state
v1.0.1
Published
A lightweight, reactive state management library for React with TypeScript support
Maintainers
Readme
ReactiveState
A lightweight, reactive state management library for React with TypeScript support.
Features
- Type Safety: Fully typed API using TypeScript.
- Lightweight: Minimal bundle size, avoiding unnecessary dependencies.
- Intuitive API: Simple, hook-based API that feels native to React.
- Fine-Grained Reactivity: Components only re-render when the specific part of the state they depend on changes.
- Flexible: Support for both atomic and global store patterns.
- Optimized Performance: Efficient state updates, avoiding unnecessary renders.
- Persistable State: Support for localStorage or custom storage solutions.
- Asynchronous Actions: Built-in support for async operations.
- Selectors & Derivations: Computed state and optimized subscriptions.
- Middleware Support: Logging, async processing, validation, and more.
- SSR Compatible: Works with server-side rendering frameworks like Next.js.
Installation
npm install @okenneth/reactive-state
# or
yarn add @okenneth/reactive-state
# or
pnpm add @okenneth/reactive-stateBasic Usage
Creating a Store
import { createStore } from '@okenneth/reactive-state';
// Define your state type
interface CounterState {
count: number;
incrementBy: number;
}
// Create a store with initial state
const counterStore = createStore<CounterState>({
initialState: {
count: 0,
incrementBy: 1
}
});
// Use the store directly
counterStore.setState(state => ({
...state,
count: state.count + state.incrementBy
}));
console.log(counterStore.getState()); // { count: 1, incrementBy: 1 }Using Stores in Components
import React from 'react';
import { useStore, useSelector } from '@okenneth/reactive-state';
function Counter() {
// Subscribe to the entire store
const state = useStore(counterStore);
const increment = () => {
counterStore.setState(state => ({
...state,
count: state.count + state.incrementBy
}), 'INCREMENT');
};
return (
<div>
<p>Count: {state.count}</p>
<button onClick={increment}>Increment by {state.incrementBy}</button>
</div>
);
}
function CountDisplay() {
// Subscribe to only the count part of the state
const count = useSelector(
counterStore,
state => state.count
);
// This component only re-renders when count changes
return <p>Current count: {count}</p>;
}Using the Combined State and Setter Hook
import React from 'react';
import { useStoreState } from '@okenneth/reactive-state';
function CounterWithHook() {
// Get both state and setState in one hook
const [state, setState] = useStoreState(counterStore);
const increment = () => {
setState(state => ({
...state,
count: state.count + state.incrementBy
}), 'INCREMENT');
};
return (
<div>
<p>Count: {state.count}</p>
<button onClick={increment}>Increment by {state.incrementBy}</button>
</div>
);
}Async Actions
import { AsyncAction } from '@okenneth/reactive-state';
// Define an async action
const fetchUserData: AsyncAction<UserState> = async (
state,
setState,
getState
) => {
// Show loading state
setState(state => ({ ...state, loading: true }), 'FETCH_USER_START');
try {
// Fetch data from API
const response = await fetch('/api/user');
const userData = await response.json();
// Update state with the result
setState(state => ({
...state,
user: userData,
loading: false
}), 'FETCH_USER_SUCCESS');
return userData;
} catch (error) {
// Handle error
setState(state => ({
...state,
error: error.message,
loading: false
}), 'FETCH_USER_ERROR');
throw error;
}
};
// Dispatch the action
userStore.dispatch(fetchUserData).then(user => {
console.log('User data loaded:', user);
}).catch(error => {
console.error('Failed to load user data:', error);
});Creating Derived State
import { createDerivedStore } from '@okenneth/reactive-state';
// Create a derived store that depends on another store
const doubledCountStore = createDerivedStore(
[counterStore],
(counterState) => ({
doubledCount: counterState.count * 2,
isEven: (counterState.count * 2) % 2 === 0
})
);
// Use in a component with useStore hook
function DoubledCounter() {
const { doubledCount, isEven } = useStore(doubledCountStore);
return (
<div>
<p>Doubled count: {doubledCount}</p>
<p>Is even: {isEven ? 'Yes' : 'No'}</p>
</div>
);
}Using Middleware
import {
createStore,
createLoggerMiddleware,
createPerformanceMiddleware
} from '@okenneth/reactive-state';
// Create a store with middleware
const storeWithMiddleware = createStore({
initialState: { count: 0 },
middleware: [
createLoggerMiddleware({ name: 'CounterStore' }),
createPerformanceMiddleware({ warnIfExceeds: 5 })
]
});Persistable State
import { createStore } from '@okenneth/reactive-state';
// Create a store with persistence enabled
const persistedStore = createStore({
initialState: { preferences: { theme: 'light' } },
persist: {
key: 'app-preferences',
storage: localStorage // Optional, defaults to localStorage
}
});Undo/Redo Functionality
import React from 'react';
import { createStore, createUndoRedoMiddleware } from '@okenneth/reactive-state';
const initialState = { text: '' };
// Create the undo/redo middleware
const { middleware, controls } = createUndoRedoMiddleware();
// Create a store with the middleware
const textStore = createStore({
initialState,
middleware: [middleware]
});
function TextEditor() {
const [state, setState] = useStoreState(textStore);
const handleChange = (e) => {
setState({ text: e.target.value }, 'UPDATE_TEXT');
};
const handleUndo = () => {
controls.undo(newState => textStore.setState(newState));
};
const handleRedo = () => {
controls.redo(newState => textStore.setState(newState));
};
return (
<div>
<textarea
value={state.text}
onChange={handleChange}
/>
<button
onClick={handleUndo}
disabled={!controls.canUndo()}
>
Undo
</button>
<button
onClick={handleRedo}
disabled={!controls.canRedo()}
>
Redo
</button>
</div>
);
}Advanced Usage
Creating a Store Hook Factory
import { createStoreHook } from '@okenneth/reactive-state';
// Create a store and a custom hook for it
const { store: todoStore, useStore: useTodoStore } = createStoreHook({
initialState: {
todos: [],
filter: 'all'
}
});
// Use in components
function TodoList() {
const [state, setState] = useTodoStore();
// Now you can use the state and setState directly
// without having to import the store separately
}Custom Middleware
import { Middleware } from '@okenneth/reactive-state';
// Create a custom middleware
const analyticsMiddleware: Middleware<any> = (nextState, prevState, action) => {
if (action) {
// Send action to analytics service
sendToAnalytics({
action,
timestamp: new Date().toISOString()
});
}
return nextState;
};
// Use the middleware
const storeWithAnalytics = createStore({
initialState: { /* ... */ },
middleware: [analyticsMiddleware]
});SSR Support
ReactiveState works seamlessly with server-side rendering:
// On the server
const serverStore = createStore({ initialState: { /* ... */ } });
// Render with initial data
const html = renderToString(<App store={serverStore} />);
// Serialize state for client hydration
const initialData = serverStore.getState();
// On the client
const clientStore = createStore({
initialState: window.__INITIAL_DATA__ || { /* fallback */ }
});
// Hydrate the application
hydrate(<App store={clientStore} />, document.getElementById('root'));Performance Optimization
ReactiveState is designed for optimal performance:
- Fine-grained reactivity: Components only re-render when their specific dependencies change.
- Shallow equality checks: Prevents unnecessary renders when state references change but values don't.
- Memoized selectors: The
useSelectorhook only triggers updates when the selected state changes. - Batch updates: Multiple state changes within the same event loop are batched to avoid cascading renders.
API Reference
Core
createStore<T>(options: CreateStoreOptions<T>): Store<T>
Creates a new store with the provided options.
createDerivedStore<D, T>(dependencies, deriveFn): Store<T>
Creates a derived store that depends on other stores.
Hooks
useStore<T>(store: Store<T>): T
Subscribes to a store and returns its current state.
useSelector<T, R>(store: Store<T>, selector: (state: T) => R, equalityFn?): R
Subscribes to a specific part of a store's state.
useSetState<T>(store: Store<T>): (updater, action?) => void
Returns a memoized setState function for the store.
useStoreState<T>(store: Store<T>): [T, (updater, action?) => void]
Returns both the state and setState function.
useSelectorState<T, R>(store, selector, equalityFn?): [R, (updater, action?) => void]
Returns both the selected state and setState function.
Middleware
createLoggerMiddleware(options?): Middleware<T>
Creates middleware that logs state changes.
createPerformanceMiddleware(options?): Middleware<T>
Creates middleware that tracks update performance.
createThrottleMiddleware(options?): Middleware<T>
Creates middleware that throttles updates.
createValidationMiddleware(validator): Middleware<T>
Creates middleware that validates state changes.
createUndoRedoMiddleware(options?): { middleware, controls }
Creates middleware and controls for undo/redo functionality.
License
MIT
