repostate
v2.0.0
Published
A lightweight state management solution for React applications
Downloads
11
Readme
RepoState v2.0.0
RepoState is a lightweight state management solution for React applications. It provides global state management with hooks for accessing and updating the state at different paths within your state tree. RepoState allows for a clean and modular way to manage application state with support for dynamic state composition.
Installation
npm install repostateQuick Start (TypeScript)
import RepoState, { useRepoState, type ReducerConfig } from 'repostate';
// Define your state types
interface UserState {
name: string;
email: string;
preferences: {
theme: 'light' | 'dark';
notifications: boolean;
};
}
interface AppState {
user: UserState;
settings: {
theme: 'light' | 'dark';
};
}
// Add state with reducers
const reducers: ReducerConfig[] = [
{
path: 'user',
type: 'update',
reducer: (state: UserState, updates: Partial<UserState>) => ({ ...state, ...updates })
},
{
path: 'user.preferences.theme',
type: 'toggle',
reducer: (theme: 'light' | 'dark') => theme === 'dark' ? 'light' : 'dark'
}
];
RepoState.add<AppState>({
user: {
name: 'John',
email: '[email protected]',
preferences: { theme: 'dark', notifications: true }
},
settings: { theme: 'dark' }
}, reducers);
// Use in components with full type safety
const UserProfile: React.FC = () => {
const [user, dispatchUser] = useRepoState<UserState>('user');
const [theme, dispatchTheme] = useRepoState<'light' | 'dark'>('user.preferences.theme');
return (
<div>
<p>Name: {user.name}</p>
<button onClick={() => dispatchUser('update', { name: 'Jane' })}>
Change Name
</button>
<p>Theme: {theme}</p>
<button onClick={() => dispatchTheme('toggle')}>
Toggle Theme
</button>
</div>
);
};Installation
npm install repostateUsage
Adding State
RepoState uses the add() API that allows you to build your state incrementally. This is perfect for modular applications, micro-frontends, or lazy-loaded features.
import RepoState from 'repostate';
// Initialize with your core state
RepoState.add({
user: {
name: 'John',
age: 30
},
settings: {
theme: 'dark'
}
});
// Later, add more state as features are loaded
RepoState.add({
user: {
preferences: {
notifications: true
}
},
cart: {
items: [] as string[]
}
});
// Results in merged state:
// {
// user: { name: 'John', age: 30, preferences: { notifications: true } },
// settings: { theme: 'dark' },
// cart: { items: [] }
// }Adding State with Reducers
You can add state and reducers together for a complete feature module. Reducers are functions that take the current state and the new value, then return an updated state. They only need to focus on the specific portion of the state related to their business logic, without requiring knowledge of the entire state, making them modular and easier to maintain.
import RepoState, { type ReducerConfig } from 'repostate';
// Define types for better type safety
interface CounterState {
value: number;
}
interface NotificationState {
unread: number;
}
// Add state with its related reducers
const reducers: ReducerConfig[] = [
{ path: 'counter.value', type: 'increase', reducer: (state: number, value: number) => state + value },
{ path: 'counter.value', type: 'reset', reducer: () => 0 },
{ path: 'notifications.unread', type: 'increment', reducer: (state: number) => state + 1 },
{ path: 'notifications.unread', type: 'clear', reducer: () => 0 }
];
RepoState.add(
{
counter: { value: 0 },
notifications: { unread: 0 }
},
reducers
);
// Or add reducers separately for existing state
RepoState.addReducer('settings.theme', 'toggle', (state: 'dark' | 'light') =>
state === 'dark' ? 'light' : 'dark'
);State Conflicts
RepoState prevents accidental state conflicts by throwing errors when you try to overwrite existing values:
RepoState.add({ user: { name: 'John' } });
RepoState.add({ user: { name: 'Jane' } }); // ❌ Throws: State conflict detected at path: user.name
// Instead, add non-conflicting properties
RepoState.add({ user: { email: '[email protected]' } }); // ✅ Works fineUsing RepoState in a React App
Wrap your app or specific components with the RepoState.Provider to make the state available:
import React from 'react';
import RepoState from 'repostate';
const App: React.FC = () => (
<RepoState.Provider>
<YourComponent />
</RepoState.Provider>
);
export default App;Accessing and Updating State
You can use the useRepoState and useRepoDispatch hooks to access and update the state.
Modular Feature Development
Here's how you can build features incrementally using the add API:
// Core app state
RepoState.add({
app: {
loading: false,
version: '1.0.0'
}
});
// User feature module
RepoState.add(
{
user: {
profile: { name: 'John', email: '[email protected]' },
preferences: { theme: 'dark', notifications: true }
}
},
[
{ path: 'user.profile', type: 'update', reducer: (state, updates) => ({ ...state, ...updates }) },
{ path: 'user.preferences.theme', type: 'toggle', reducer: (theme) => theme === 'dark' ? 'light' : 'dark' }
]
);
// Shopping cart feature module (loaded separately)
RepoState.add(
{
cart: {
items: [],
total: 0
}
},
[
{ path: 'cart.items', type: 'add', reducer: (items, newItem) => [...items, newItem] },
{ path: 'cart.items', type: 'remove', reducer: (items, itemId) => items.filter(item => item.id !== itemId) },
{ path: 'cart.total', type: 'calculate', reducer: (_, items) => items.reduce((sum, item) => sum + item.price, 0) }
]
);Using useRepoState Hook
The useRepoState hook allows you to access and update a specific part of the state by specifying a statePath. If the statePath is null, undefined, or '@', the entire state is returned.
import { useRepoState } from 'repostate';
const UserProfile = () => {
const [userProfile, dispatchUserProfile] = useRepoState('user.profile');
const [theme, dispatchTheme] = useRepoState('user.preferences.theme');
return (
<div>
<p>Name: {userProfile.name}</p>
<button onClick={() => dispatchUserProfile('update', { name: 'Jane' })}>
Change Name
</button>
<p>Theme: {theme}</p>
<button onClick={() => dispatchTheme('toggle')}>
Toggle Theme
</button>
</div>
);
};If the type in the dispatch call is null, the default behavior is to directly overwrite the state at the specified statePath with the provided value. For example:
const [userName, dispatchUserName] = useRepoState('user.name');
//...
<button onClick={() => dispatchUserName(null, 'Jane')}>Change Username</button>Accessing the Entire State
If you want to access the entire state, you can pass null, undefined, or '@' as the statePath.
const [state, dispatch] = useRepoState(); // Or use '@' or nullUsing useRepoDispatch Hook
The useRepoDispatch hook provides a global dispatch function that can be used to update any part of the state. The dispatch function takes three arguments: statePath, type, and value.
import { useRepoDispatch } from 'repostate';
const ShoppingActions = () => {
const dispatch = useRepoDispatch();
const addToCart = (item) => {
dispatch('cart.items', 'add', item);
// Recalculate total after adding item
dispatch('cart.total', 'calculate', getCartItems());
};
const toggleTheme = () => {
dispatch('user.preferences.theme', 'toggle');
};
return (
<div>
<button onClick={() => addToCart({ id: 1, name: 'Book', price: 10 })}>
Add to Cart
</button>
<button onClick={toggleTheme}>
Toggle Theme
</button>
</div>
);
};Note: A reducer must be defined for the specified statePath and type; otherwise, an error will be thrown. If type is null, the default behavior of directly overwriting the state at the given statePath is applied.
External State Updates
RepoState provides a dispatch method that allows you to update state from outside React components. This is perfect for handling external events, API responses, WebSocket messages, timers, and other scenarios where state needs to be updated from non-React code.
Example 4: Using External Dispatch
import RepoState from 'repostate';
// WebSocket message handler
websocket.onmessage = (event) => {
const notification = JSON.parse(event.data);
RepoState.dispatch('notifications.messages', 'add', notification);
RepoState.dispatch('notifications.unread', 'increment');
};
// Timer-based updates
setInterval(() => {
RepoState.dispatch('app.lastSyncTime', null, new Date().toISOString());
}, 30000);
// External API response handler
fetch('/api/user/profile')
.then(response => response.json())
.then(profile => {
RepoState.dispatch('user.profile', 'update', profile);
});
// Browser storage events
window.addEventListener('storage', (event) => {
if (event.key === 'theme') {
RepoState.dispatch('settings.theme', null, event.newValue);
}
});External Dispatch Features:
- ✅ Updates from anywhere - Call from event handlers, timers, API callbacks
- ✅ Automatic re-renders - React components automatically update when external changes occur
- ✅ Same reducer logic - Uses the same validation and reducer system as React components
- ✅ Type safety - Maintains the same error handling and validation as internal dispatches
When to Use External Dispatch:
- WebSocket/SSE connections - Real-time data updates from server
- Background timers - Periodic data refreshes or time-based updates
- Browser events - Storage changes, focus/blur, online/offline status
- External libraries - Integration with third-party services
- Service workers - Push notifications and background sync
- Cross-tab communication - Synchronizing state across browser tabs
Accessing Hooks from RepoState
You can also access the hooks via the RepoState object:
import RepoState from 'repostate';
const YourComponent = () => {
const [user, dispatch] = RepoState.useRepoState('user');
const globalDispatch = RepoState.useRepoDispatch();
return (
<div>
<p>User: {user.name}</p>
<button onClick={() => dispatch(null, { name: 'Doe' })}>Change Name</button>
<button onClick={() => globalDispatch('settings.theme', 'toggle')}>Toggle Theme</button>
</div>
);
};API Reference
RepoState
add<T>(state: T, reducers?: ReducerConfig[]): void: Adds state to the global state tree. Deep merges objects and throws errors on conflicts. Optionally adds reducers for the new state paths.dispatch(statePath: StatePath, type: ActionType, value?: any): void: Updates state from outside React components. Perfect for external events, API responses, and timers. Automatically triggers React re-renders.getSnapshot(): StateObject: Returns a deep clone of the current state.addReducer(statePath: string, type: string, reduceFn: Reducer): void: Adds a reducer function for a specificstatePathandtype.
useRepoState<T>(statePath?: StatePath): UseRepoStateReturn<T>
A React hook that provides access to a specific substate and a function to update that substate.
statePath: A string representing the path to the desired state. IfstatePathisnull,undefined, or'@', it returns the entire state.
Returns an array [subState, dispatchFn]:
subState: T: The current value of the substate atstatePath.dispatchFn(type: ActionType, value?: any): void: A function to dispatch an action to update the state at the specifiedstatePath. Iftypeisnull, the default behavior of directly overwriting the state at the givenstatePathwith givenvalueis applied.
useRepoDispatch(): DispatchFunction
A React hook that returns a dispatch function for updating the state directly.
dispatch(statePath: StatePath, type: ActionType, value?: any): void: Dispatches an action to update the state at the specifiedstatePath. Iftypeisnull, the default behavior of directly overwriting the state at the givenstatePathwith givenvalueis applied.
TypeScript Types
RepoState v2.0.0 exports all necessary types for TypeScript users:
import type {
StateObject,
Reducer,
ReducerConfig,
DispatchFunction,
UseRepoStateReturn,
StatePath,
ActionType
} from 'repostate';
// Type-safe reducer definition
const userReducer: Reducer<UserState, Partial<UserState>> = (state, updates) => ({
...state,
...updates
});
// Type-safe reducer config
const reducerConfig: ReducerConfig[] = [
{ path: 'user', type: 'update', reducer: userReducer }
];License
This project is licensed under the MIT License.
