react-hooks-global-states
v15.0.16
Published
A package to easily handle global state across your React components using hooks.
Maintainers
Readme
react-hooks-global-states 🌟
![]()
Zero setup. Zero complexity. Maximum performance. 🚀
One line of code. Infinite possibilities. ✨
Live Demo • Video Tutorial • CodePen
🎯 The One-Liner
import { createGlobalState } from 'react-hooks-global-states';
export const useCounter = createGlobalState(0);That's it. No providers. No context boilerplate. No configuration files. Just pure, beautiful state management. 🎨
// Use it anywhere, instantly
function Counter() {
const [count, setCount] = useCounter();
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}🚀 Why Developers Love This Library
🎓 Zero Learning Curve
// If you know this...
const [state, setState] = useState(0);
// You know this!
const [state, setState] = useGlobalState();⚡ Blazing Fast
Only components that care about a slice re-render. Surgical precision, maximum performance.
// Only re-renders when name changes
const [name] = useStore((s) => s.user.name);🔗 Chainable Selectors
const useUsers = store.createSelectorHook((s) => s.users);
const useAdmins = useUsers.createSelectorHook((users) => users.filter((u) => u.isAdmin));🎭 Actions (Optional)
const useAuth = createGlobalState(null, {
actions: {
login(credentials) {
return async ({ setState }) => {
const user = await api.login(credentials);
setState(user);
};
},
},
});🎪 Context Mode
const Form = createContext({ name: '', email: '' });
<Form.Provider>
<FormFields />
</Form.Provider>;� Non-Reactive API
Use state anywhere - even outside React components!
// In API interceptors, WebSockets, utils...
const token = useAuth.getState().token;
useAuth.setState({ user: newUser });📦 Installation
npm install react-hooks-global-statesPlatform-specific with built-in storage:
- 🌐 Web:
react-global-state-hooks(localStorage) - 📱 React Native:
react-native-global-state-hooks(AsyncStorage by default, customizable, optional dependency)
🎬 Quick Start
30 Seconds to Global State
import { createGlobalState } from 'react-hooks-global-states';
// 1. Create it (anywhere)
const useTheme = createGlobalState('dark');
// 2. Use it (everywhere)
function ThemeToggle() {
const [theme, setTheme] = useTheme();
return <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>{theme} mode</button>;
}
function ThemedComponent() {
const [theme] = useTheme();
return <div className={theme}>Themed content</div>;
}60 Seconds to Production-Ready
import { createGlobalState } from 'react-hooks-global-states';
const useAuth = createGlobalState(
{ user: null, token: null },
{
// Non-reactive metadata
metadata: { isLoading: false },
// Type-safe actions
actions: {
login(email, password) {
return async ({ setState, setMetadata }) => {
setMetadata({ isLoading: true });
try {
const { user, token } = await api.login(email, password);
setState({ user, token });
return { success: true };
} catch (error) {
return { success: false, error };
} finally {
setMetadata({ isLoading: false });
}
};
},
logout() {
return ({ setState }) => {
setState({ user: null, token: null });
};
},
},
},
);
// Usage
function LoginForm() {
const [auth, actions] = useAuth();
const { isLoading } = useAuth.getMetadata();
const handleSubmit = async (e) => {
e.preventDefault();
const result = await actions.login(email, password);
if (!result.success) toast.error(result.error);
};
return (
<form onSubmit={handleSubmit}>
{/* ... */}
<button disabled={isLoading}>{isLoading ? 'Logging in...' : 'Login'}</button>
</form>
);
}🌟 Core Features Deep Dive
1️⃣ Global State with createGlobalState
Create state that lives outside React's component tree. Perfect for app-wide state!
🎨 The Basics
// Primitives
const useCount = createGlobalState(0);
const useTheme = createGlobalState('light');
const useIsOpen = createGlobalState(false);
// Objects
const useUser = createGlobalState({ name: 'Guest', role: 'viewer' });
// Arrays
const useTodos = createGlobalState([
{ id: 1, text: 'Learn this library', done: true },
{ id: 2, text: 'Build something awesome', done: false },
]);
// With function initialization (useful for testing - allows store.reset())
const useExpensiveState = createGlobalState(() => {
return computeExpensiveInitialValue();
});🎯 Surgical Re-renders with Selectors
The secret sauce: components only re-render when their specific slice changes!
const useStore = createGlobalState({
user: { name: 'John', age: 30, email: '[email protected]' },
theme: 'dark',
notifications: [],
settings: { sound: true, vibrate: false },
});
// This component ONLY re-renders when user.name changes
function UserName() {
const [name] = useStore((state) => state.user.name);
return <h1>{name}</h1>;
}
// Alternative: use.select() when you only need the value (not setState)
function UserNameAlt() {
const name = useStore.select((state) => state.user.name);
return <h1>{name}</h1>;
}
// This ONLY re-renders when theme changes
function ThemeSwitcher() {
const [theme, setStore] = useStore((state) => state.theme);
const toggleTheme = () => {
setStore((s) => ({ ...s, theme: theme === 'dark' ? 'light' : 'dark' }));
};
return <button onClick={toggleTheme}>{theme}</button>;
}
// This ONLY re-renders when notifications array changes
function NotificationCount() {
const [notifications] = useStore((state) => state.notifications);
return <span>{notifications.length}</span>;
}Performance comparison:
| Approach | Re-renders when ANY state changes? | | --------------- | ---------------------------------- | | Context (naive) | ✅ YES (performance killer) | | This library | ❌ NO (only selected slices) |
⚡ Computed Values with Dependencies
Derive values efficiently with automatic recomputation control! - selectors can depend on external state (like useState)!
const useStore = createGlobalState({
todos: [
{ id: 1, text: 'Task 1', completed: false, priority: 'high' },
{ id: 2, text: 'Task 2', completed: true, priority: 'low' },
{ id: 3, text: 'Task 3', completed: false, priority: 'high' },
],
});
function FilteredTodoList() {
// These are regular useState - NOT in the global store!
const [filter, setFilter] = useState('all'); // 'all' | 'active' | 'completed'
const [priorityFilter, setPriorityFilter] = useState('all'); // 'all' | 'high' | 'low'
// 🔥 The magic: selector recomputes when EITHER store OR external state changes!
const [filteredTodos] = useStore(
(state) => {
let todos = state.todos;
// Filter by completion
if (filter === 'active') todos = todos.filter((t) => !t.completed);
if (filter === 'completed') todos = todos.filter((t) => t.completed);
// Filter by priority
if (priorityFilter !== 'all') {
todos = todos.filter((t) => t.priority === priorityFilter);
}
return todos;
},
[filter, priorityFilter], // Recompute when these external dependencies change!
);
return (
<div>
<select value={filter} onChange={(e) => setFilter(e.target.value)}>
<option value="all">All</option>
<option value="active">Active</option>
<option value="completed">Completed</option>
</select>
<select value={priorityFilter} onChange={(e) => setPriorityFilter(e.target.value)}>
<option value="all">All Priorities</option>
<option value="high">High</option>
<option value="low">Low</option>
</select>
<ul>
{filteredTodos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</div>
);
}
// Advanced: use config object with custom equality
function AdvancedFiltered() {
const [filter, setFilter] = useState('all');
const [filteredTodos] = useStore(
(state) =>
state.todos.filter((t) => (filter === 'all' ? true : t.completed === (filter === 'completed'))),
{
dependencies: [filter],
// Custom equality - only recompute if todos array changed
isEqualRoot: (prev, next) => prev.todos === next.todos,
},
);
return <ul>{/* ... */}</ul>;
}🔗 Reusable Selector Hooks (The Game Changer!)
Create hooks from hooks! Chain them! Compose them! This is where it gets fun! 🎉
const useStore = createGlobalState({
users: [
{ id: 1, name: 'Alice', role: 'admin', active: true },
{ id: 2, name: 'Bob', role: 'user', active: true },
{ id: 3, name: 'Charlie', role: 'admin', active: false },
],
currentUserId: 1,
});
// Create a reusable hook for users array
const useUsers = useStore.createSelectorHook((state) => state.users);
// Chain it! Create a hook for active users only
const useActiveUsers = useUsers.createSelectorHook((users) => users.filter((u) => u.active));
// Chain it again! Create a hook for active admins
const useActiveAdmins = useActiveUsers.createSelectorHook((users) => users.filter((u) => u.role === 'admin'));
// Create a hook for the current user
const useCurrentUser = useStore.createSelectorHook((state) => {
return state.users.find((u) => u.id === state.currentUserId);
});
// Now use them anywhere!
function UserStats() {
const [totalUsers] = useUsers();
const [activeUsers] = useActiveUsers();
const [activeAdmins] = useActiveAdmins();
return (
<div>
<p>Total: {totalUsers.length}</p>
<p>Active: {activeUsers.length}</p>
<p>Active Admins: {activeAdmins.length}</p>
</div>
);
}
function CurrentUserProfile() {
const [user] = useCurrentUser();
return <div>{user?.name}</div>;
}
// 🎯 Key insight: All these hooks share the SAME setState!
function AnyComponent() {
const [, setFromUsers] = useUsers();
const [, setFromAdmins] = useActiveAdmins();
const [, setFromStore] = useStore();
console.log(setFromUsers === setFromAdmins); // true!
console.log(setFromUsers === setFromStore); // true!
console.log(setFromUsers === useStore.setState); // true!
}Why this is powerful:
| Feature | Benefit | | -------------------- | ------------------------------------------ | | 🎯 Precision | Each hook subscribes to only what it needs | | 🔗 Composability | Build complex selectors from simple ones | | ♻️ Reusability | Define once, use everywhere | | 🎨 Clean Code | No repetitive selector logic in components | | ⚡ Performance | Automatic memoization and change detection |
🎬 Actions - When You Need Structure
Actions aren't required, but they're awesome for organizing mutations!
const useStore = createGlobalState(
{
todos: new Map(), // Map<id, todo>
filter: 'all',
},
{
actions: {
addTodo(text) {
return ({ setState, getState }) => {
const id = Date.now();
const newTodo = { id, text, completed: false };
setState((s) => ({
...s,
todos: new Map(s.todos).set(id, newTodo),
}));
};
},
toggleTodo(id) {
return ({ setState, getState }) => {
const { todos } = getState();
const todo = todos.get(id);
if (!todo) return;
setState((s) => ({
...s,
todos: new Map(s.todos).set(id, { ...todo, completed: !todo.completed }),
}));
};
},
clearCompleted() {
return ({ setState, getState }) => {
const { todos } = getState();
// Filter out completed todos, create new Map
const newTodos = new Map();
todos.forEach((todo, id) => {
if (!todo.completed) newTodos.set(id, todo);
});
setState((s) => ({ ...s, todos: newTodos }));
// Actions can call other actions!
this.updateStats();
};
},
updateStats() {
return ({ getState }) => {
const { todos } = getState();
const completed = Array.from(todos.values()).filter((t) => t.completed).length;
console.log(`${completed} completed`);
};
},
// Async actions? No problem!
syncWithServer() {
return async ({ setState }) => {
const todosArray = await api.fetchTodos();
const todosMap = new Map(todosArray.map((t) => [t.id, t]));
setState((s) => ({ ...s, todos: todosMap }));
};
},
},
},
);
// When actions are defined, the second element is the actions object!
function TodoApp() {
const [state, actions] = useStore();
return (
<div>
<button onClick={() => actions.addTodo('New task')}>Add</button>
<button onClick={() => actions.clearCompleted()}>Clear</button>
<button onClick={() => actions.syncWithServer()}>Sync</button>
{Array.from(state.todos.values()).map((todo) => (
<div key={todo.id} onClick={() => actions.toggleTodo(todo.id)}>
{todo.text}
</div>
))}
</div>
);
}
// � Don't worry, you can still go rogue with setState!
const handleQuickFix = () => {
useStore.setState((s) => ({ ...s, filter: 'all' }));
};🔌 Non-Reactive API (Use Outside React!)
Access your state from anywhere - even outside components!
const useAuth = createGlobalState({ user: null, token: null });
// ✨ In a utility file
export async function loginUser(email, password) {
try {
const { user, token } = await api.login(email, password);
useAuth.setState({ user, token });
return { success: true };
} catch (error) {
return { success: false, error };
}
}
// ✨ In an API interceptor
axios.interceptors.request.use((config) => {
const { token } = useAuth.getState();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// ✨ In a WebSocket handler
socket.on('user-updated', (user) => {
useAuth.setState((state) => ({ ...state, user }));
});
// ✨ Subscribe to changes (even outside React!)
const unsubscribe = useAuth.subscribe(
// Selector
(state) => state.user,
// Callback
function onChange(newUser) {
console.log('User changed:', user);
analytics.identify(user?.id);
},
);🔭 Observable Fragments (RxJS-style)
Create observable slices of your state for reactive programming!
const useStore = createGlobalState({
count: 0,
user: { name: 'John' },
});
// Create an observable that tracks just the count
const countObservable = useStore.createObservable((state) => state.count);
// Use it outside React
countObservable.subscribe((count) => {
console.log('Count is now:', count);
if (count > 10) {
alert('Count is high!');
}
});
// Observables also have getState
console.log(countObservable.getState()); // Current count value
// Chain observables!
const doubledObservable = countObservable.createObservable((count) => count * 2);📋 Metadata - Non-Reactive Side Info
Store data that doesn't need to trigger re-renders!
const useStore = createGlobalState(
{ items: [] },
{
metadata: {
isLoading: false,
lastFetch: null,
error: null,
retryCount: 0,
},
},
);
// Metadata changes don't trigger re-renders!
useStore.setMetadata({ isLoading: true });
// But you can access it anytime
const meta = useStore.getMetadata();
console.log(meta.isLoading); // true
// Perfect for loading states, error tracking, etc.
async function fetchData() {
useStore.setMetadata({ isLoading: true, error: null });
try {
const items = await api.fetch();
useStore.setState({ items });
useStore.setMetadata({
isLoading: false,
lastFetch: new Date(),
});
} catch (error) {
useStore.setMetadata({
isLoading: false,
error: error.message,
retryCount: useStore.getMetadata().retryCount + 1,
});
}
}2️⃣ Scoped State with createContext
Sometimes you need state scoped to a component tree. That's what createContext is for!
🎪 The Basics
import { createContext } from 'react-hooks-global-states';
// Create a context
const UserFormContext = createContext({
name: '',
email: '',
age: 0,
});
function App() {
return (
<UserFormContext.Provider>
<FormFields />
<FormPreview />
</UserFormContext.Provider>
);
}
function FormFields() {
const [form, setForm] = UserFormContext.use();
return (
<div>
<input value={form.name} onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))} />
<input value={form.email} onChange={(e) => setForm((f) => ({ ...f, email: e.target.value }))} />
</div>
);
}
function FormPreview() {
const [form] = UserFormContext.use();
return (
<div>
Hello {form.name} ({form.email})
</div>
);
}🎁 Provider Variations
const ThemeContext = createContext('light');
function App() {
return (
<>
{/* Default value */}
<ThemeContext.Provider>
<Page /> {/* Gets 'light' */}
</ThemeContext.Provider>
{/* Custom value */}
<ThemeContext.Provider value="dark">
<Page /> {/* Gets 'dark' */}
</ThemeContext.Provider>
{/* Derived from parent (yes, you can nest!) */}
<ThemeContext.Provider value={(parent) => (parent === 'dark' ? 'light' : 'dark')}>
<Page /> {/* Gets opposite of parent */}
</ThemeContext.Provider>
</>
);
}🎯 Context + Selectors = ❤️
Everything that works with createGlobalState works with createContext!
const FormContext = createContext({
personal: { name: '', age: 0 },
contact: { email: '', phone: '' },
preferences: { theme: 'light', notifications: true },
});
// Only re-renders when name changes!
function NameField() {
const [name, setForm] = FormContext.use((state) => state.personal.name);
return (
<input
value={name}
onChange={(e) => setForm((s) => ({ ...s, personal: { ...s.personal, name: e.target.value } }))}
/>
);
}
// Only re-renders when email changes!
function EmailField() {
const [email] = FormContext.use((state) => state.contact.email);
return <div>{email}</div>;
}🎭 Context with Actions
const CounterContext = createContext(0, {
actions: {
increment(amount = 1) {
return ({ setState, getState }) => {
setState(getState() + amount);
};
},
decrement(amount = 1) {
return ({ setState, getState }) => {
setState(getState() - amount);
};
},
reset() {
return ({ setState }) => setState(0);
},
},
});
function Counter() {
const [count, actions] = CounterContext.use();
return (
<div>
<h1>{count}</h1>
<button onClick={() => actions.increment()}>+</button>
<button onClick={() => actions.decrement()}>-</button>
<button onClick={() => actions.reset()}>Reset</button>
</div>
);
}
function App() {
return (
<CounterContext.Provider>
<Counter />
</CounterContext.Provider>
);
}🔗 Reusable Context Selectors
Yes, chainable selectors work here too!
const DataContext = createContext({
users: [
/* ... */
],
posts: [
/* ... */
],
filter: 'all',
});
// Create reusable hooks
const useUsers = DataContext.use.createSelectorHook((s) => s.users);
const useActiveUsers = useUsers.createSelectorHook((u) => u.filter((user) => user.active));
const usePosts = DataContext.use.createSelectorHook((s) => s.posts);
function App() {
return (
<DataContext.Provider>
<UserList />
<PostList />
</DataContext.Provider>
);
}
function UserList() {
const [activeUsers] = useActiveUsers();
return (
<ul>
{activeUsers.map((u) => (
<li key={u.id}>{u.name}</li>
))}
</ul>
);
}🧪 What About Testing?
Do you need to access the context to spy on state changes, inject test data, or manipulate state directly? No problem!
import { renderHook } from '@testing-library/react';
import CounterContext from './CounterContext'; // { count: 0 } with increment() action
describe('CounterContext', () => {
it('should manipulate state from tests', () => {
// Create wrapper with direct access to context
const { wrapper, context } = CounterContext.Provider.makeProviderWrapper();
// Render the hook
const { result } = renderHook(() => CounterContext.use(), { wrapper });
// Read initial state
expect(result.current[0].count).toBe(0);
// Call the action through the wrapper
context.current.actions.increment();
// Verify state updated
expect(result.current[0].count).toBe(1);
});
});🎬 Lifecycle Hooks
React to context lifecycle events!
const DataContext = createContext([], {
callbacks: {
onCreated: (store) => {
console.log('Context created with:', store.getState());
},
onMounted: (store) => {
console.log('Provider mounted!');
// Fetch data on mount
fetchData().then((data) => store.setState(data));
// Return cleanup
return () => {
console.log('Provider unmounting!');
};
},
},
});
// Or per-provider
function App() {
return (
<DataContext.Provider
onCreated={(store) => console.log('Created!')}
onMounted={(store) => {
console.log('Mounted!');
return () => console.log('Cleanup!');
}}
>
<Content />
</DataContext.Provider>
);
}3️⃣ External Actions with actions
Extend any store with additional actions without modifying it! Perfect for separating concerns! 🎯
💪 Direct Binding
import { createGlobalState, actions } from 'react-hooks-global-states';
const useCounter = createGlobalState(0);
// Create actions for the store
const counterActions = actions(useCounter, {
increment(amount = 1) {
return ({ setState, getState }) => {
setState(getState() + amount);
};
},
decrement(amount = 1) {
return ({ setState, getState }) => {
setState(getState() - amount);
};
},
double() {
return ({ setState, getState }) => {
setState(getState() * 2);
};
},
});
// Use them anywhere!
counterActions.increment(5); // count = 5
counterActions.double(); // count = 10
counterActions.decrement(3); // count = 7🎨 Action Templates (Define Before Ready!)
Need actions in contexts or lifecycle hooks (onInit) before the store API is available? Create action templates first, bind them later!
import { actions, InferAPI, createContext } from 'react-hooks-global-states';
type SessionAPI = InferAPI<typeof SessionContext>;
// Create internal actions template
const internalActions = actions<SessionAPI>()({
loadData() {
return async ({ setState }) => {
...
};
},
});
const SessionContext = createContext(
{ user: null, preferences: {}, lastSync: null },
{
callbacks: {
onInit: (api) => {
const { loadData } = internalActions(api);
// Load data on init
loadData();
// Sync every 5 minutes
const interval = setInterval(loadData, 5 * 60 * 1000);
// Cleanup on unmount
return () => clearInterval(interval);
},
},
// public actions
actions: {
logout() {
return (api) => {
const { setState } = api as SessionAPI;
setState({ user: null, preferences: {}, lastSync: null });
};
},
},
},
);
// Internal actions are not expose through the context
const { loadData } = SessionContext.use.actions();
console.log(loadData); // undefined🔄 Actions Calling Actions
Actions can call each other in multiple ways!
const useStore = createGlobalState({ count: 0, history: [] }, {
actions: {
clearHistory() {
return ({ setState }) => {
...
};
},
},
});
const storeActions = actions(useStore, {
logAction(message) {
return ({ setState, getState }) => {
...
};
},
increment(amount = 1) {
return ({ setState, getState }) => {
setState((s) => ({ ...s, count: s.count + amount }));
// Call another action in this group with 'this'
this.logAction(`Incremented by ${amount}`);
};
},
incrementTwice(amount = 1) {
return ({ actions }) => {
// Call actions with 'this'
this.increment(amount);
this.increment(amount);
// Or directly from storeActions
storeActions.logAction(`Incremented twice by ${amount}`);
// Access store's public actions via 'actions' parameter
actions.clearHistory();
};
},
});
storeActions.incrementTwice(5);🎭 Access Store Actions
External actions can call each other with this and access the store's public actions!
const useStore = createGlobalState(
{ count: 0, logs: [] },
{
actions: {
log(message) {
return ({ setState }) => {
...
};
},
},
},
);
const extraActions = actions(useStore, {
addToHistory(message) {
return ({ setState }) => {
...
};
},
incrementAndLog(amount = 1) {
return ({ setState, actions }) => {
setState((s) => ({ ...s, count: s.count + amount }));
// Call sibling actions with 'this'
this.addToHistory(`Incremented by ${amount}`);
// Access store's public actions
actions.log(`Count increased by ${amount}`);
};
},
});
extraActions.incrementAndLog(5);🎪 Works with Context!
import { actions, InferAPI } from 'react-hooks-global-states';
const CounterContext = createContext(0);
type CounterAPI = InferAPI<typeof CounterContext>;
// Define action template outside - reusable domain actions
const counterActionsTemplate = actions<CounterAPI>()({
increment() {
return ({ setState, getState }) => {
setState(getState() + 1);
};
},
decrement() {
return ({ setState, getState }) => {
setState(getState() - 1);
};
},
});
function App() {
return (
<CounterContext.Provider>
<Counter />
</CounterContext.Provider>
);
}
function Counter() {
const api = CounterContext.use.api();
// Bind template to context instance
const { increment, decrement } = useMemo(() => counterActionsTemplate(api), [api]);
return (
<div>
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
</div>
);
}🔥 Advanced Patterns
🏗️ Production Architecture (File Organization)
Real-world large-scale applications need clean separation! Here's how to organize a store with actions in separate files, custom hooks, observables, and a namespace pattern:
File Structure:
src/stores/todos/
├── index.ts // Namespace - bundles everything
├── store.ts // Store definition
├── constants/ // Initial state & metadata
│ ├── initialValue.ts
│ └── metadata.ts
├── types/ // Type definitions
│ ├── TodosAPI.ts
│ └── Todo.ts
├── hooks/ // Custom selector hooks
│ ├── useActiveTodos.ts
│ └── useCompletedTodos.ts
├── observables/ // Observable fragments
│ └── activeTodos$.ts
├── helpers/ // Utility functions
│ └── createTodo.ts
└── actions/
├── index.ts // Export all actions
├── addTodo.ts
├── toggleTodo.ts
└── internal/
└── syncWithServer.tsStore Definition (store.ts):
import { createGlobalState, actions, type InferAPI } from 'react-hooks-global-states';
import { addTodo, toggleTodo, removeTodo } from './actions';
import { syncWithServer } from './actions/internal';
import { initialValue, metadata } from './constants';
type TodosAPI = InferAPI<typeof todosStore>;
// Internal actions template
const internalActions = actions<TodosAPI>()({
syncWithServer,
});
const todosStore = createGlobalState(initialValue, {
metadata,
// Public actions - exposed to consumers
actions: {
addTodo,
toggleTodo,
removeTodo,
},
callbacks: {
onInit: (api) => {
// Bind and extend with internal actions (not exposed publicly)
const { syncWithServer } = internalActions(api);
// Auto-sync every 30 seconds
const interval = setInterval(() => syncWithServer(), 30000);
return () => clearInterval(interval);
},
},
});
// Export types (defined in types/ folder)
export type { TodosAPI } from './types';
export default todosStore;Type Files (types/TodosAPI.ts):
// types/TodosAPI.ts
// prevents circular type references
export type TodosAPI = import('../store').TodosAPI;Action File (actions/addTodo.ts):
import type { TodosAPI } from '../types';
/**
* Adds a new todo and syncs with server
*/
function addTodo(this: TodosAPI['actions'], text: string) {
return async ({ setState }: TodosAPI): Promise<void> => {
const newTodo = {
id: crypto.randomUUID(),
text,
completed: false,
createdAt: new Date(),
};
setState((s) => ({
...s,
todos: [...s.todos, newTodo],
}));
// Call internal action to sync
await this.syncWithServer();
};
}
export default addTodo;Custom Hook (hooks/useActiveTodos.ts):
import todosStore from '../store';
export const useActiveTodos = todosStore.createSelectorHook((state) =>
state.todos.filter((t) => !t.completed),
);Observable (observables/activeTodos$.ts):
import { useActiveTodos } from '../hooks/useActiveTodos';
// Derive from hook - reuses the filter logic
export const activeTodos$ = useActiveTodos.createObservable((s) => s);Namespace Pattern (index.ts):
import store from './store';
import { useActiveTodos, useCompletedTodos } from './hooks';
import { activeTodos$, completedTodos$ } from './observables';
// Bundle everything into a clean namespace
const todos$ = Object.assign(store, {
// Custom hooks
useActiveTodos,
useCompletedTodos,
// Observables
activeTodos$,
completedTodos$,
});
export default todos$;Usage:
import todos$ from './stores/todos';
function TodoApp() {
// Use the namespace
const activeTodos = todos$.useActiveTodos();
// Subscribe to observable outside React
useEffect(() => {
const sub = todos$.activeTodos$.subscribe((todos) => {
console.log('Active todos changed:', todos.length);
});
return () => sub();
}, []);
return (
<div>
<button onClick={() => todos$.actions.addTodo('New task')}>Add</button>
{activeTodos.map((todo) => (
<div key={todo.id}>{todo.text}</div>
))}
</div>
);
}Why this pattern rocks:
- ✅ KISS - Keep It Simple, Stupid! Easy to navigate
- ✅ Type-safe - Everything is strongly typed
- ✅ Public/Private APIs - Internal actions don't pollute public interface
- ✅ Namespace pattern - Everything bundled:
todos$.useActiveTodos(),todos$.activeTodos$ - ✅ Scalable - Easy to find, test, and maintain individual pieces
🎧 Smart Subscriptions
Subscribe to specific slices outside React!
const useStore = createGlobalState({
user: { name: 'John', role: 'admin' },
theme: 'dark',
notifications: [],
});
// Subscribe to just the theme
const unsubTheme = useStore.subscribe(
(state) => state.theme,
(theme) => {
document.body.className = theme;
localStorage.setItem('theme', theme);
},
);
// Subscribe to user role changes
const unsubRole = useStore.subscribe(
(state) => state.user.role,
(role) => {
console.log('User role changed to:', role);
analytics.track('role_changed', { role });
},
);
// Cleanup
unsubTheme();
unsubRole();🎨 uniqueId - Type-Safe Unique IDs
Generate branded unique identifiers with compile-time safety!
🏷️ Basic Usage
import { uniqueId } from 'react-hooks-global-states';
// Simple IDs
const id1 = uniqueId(); // "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
const id2 = uniqueId('user:'); // "user:a1b2c3d4-e5f6-7890-abcd-ef1234567890"
const id3 = uniqueId('session:'); // "session:a1b2c3d4-e5f6-7890-abcd-ef1234567890"🔒 Branded IDs (Type Safety!)
Create ID generators with compile-time type checking!
// Create branded generators
const generateUserId = uniqueId.for('user:');
const generatePostId = uniqueId.for('post:');
type UserId = ReturnType<typeof generateUserId>;
type PostId = ReturnType<typeof generatePostId>;
const userId: UserId = generateUserId(); // ✅ "user:a1b2c3d4-e5f6-7890-abcd-ef1234567890"
const postId: PostId = generatePostId(); // ✅ "post:a1b2c3d4-e5f6-7890-abcd-ef1234567890"
// TypeScript prevents mixing!
const wrong: UserId = generatePostId(); // ❌ Type error!🛡️ Runtime Validation
const generateUserId = uniqueId.for('user:');
const id = generateUserId(); // "user:a1b2c3d4-e5f6-7890-abcd-ef1234567890"
// Check if a string is a valid user ID
if (generateUserId.is(id)) {
console.log('Valid user ID!');
}
generateUserId.is('user:a1b2c3d4-e5f6-7890-abcd-ef1234567890'); // ✅ true
generateUserId.is('post:a1b2c3d4-e5f6-7890-abcd-ef1234567890'); // ❌ false
// Assert (throws if invalid)
generateUserId.assert('user:a1b2c3d4-e5f6-7890-abcd-ef1234567890'); // ✅ OK
generateUserId.assert('post:a1b2c3d4-e5f6-7890-abcd-ef1234567890'); // ❌ Throws error!🎯 Strict Branding
Maximum type safety with symbol branding!
declare const UserBrand: unique symbol;
declare const PostBrand: unique symbol;
const generateUserId = uniqueId.for('user:').strict<typeof UserBrand>();
const generatePostId = uniqueId.for('post:').strict<typeof PostBrand>();
// Even with same prefix, types are incompatible!
const generateUserId2 = uniqueId.for('user:').strict<typeof PostBrand>();
type UserId = ReturnType<typeof generateUserId>;
type UserId2 = ReturnType<typeof generateUserId2>;
const id1: UserId = generateUserId(); // ✅
const id2: UserId = generateUserId2(); // ❌ Different brands!💼 Real-World Example
import { createGlobalState, uniqueId } from 'react-hooks-global-states';
// Create typed ID generators
const generateUserId = uniqueId.for('user:');
const generateTodoId = uniqueId.for('todo:');
type UserId = ReturnType<typeof generateUserId>;
type TodoId = ReturnType<typeof generateTodoId>;
interface User {
id: UserId;
name: string;
}
interface Todo {
id: TodoId;
text: string;
assignedTo: UserId | null;
}
const useApp = createGlobalState(
{
users: [] as User[],
todos: [] as Todo[],
},
{
actions: {
addUser(name: string) {
return ({ setState, getState }) => {
const user: User = {
id: generateUserId(), // Type-safe!
name,
};
setState((s) => ({
...s,
users: [...s.users, user],
}));
};
},
addTodo(text: string, assignedTo: UserId | null = null) {
return ({ setState, getState }) => {
const todo: Todo = {
id: generateTodoId(), // Type-safe!
text,
assignedTo,
};
setState((s) => ({
...s,
todos: [...s.todos, todo],
}));
};
},
assignTodo(todoId: TodoId, userId: UserId) {
return ({ setState, getState }) => {
// TypeScript ensures correct ID types!
setState((s) => ({
...s,
todos: s.todos.map((t) => (t.id === todoId ? { ...t, assignedTo: userId } : t)),
}));
};
},
},
},
);
// Usage
const [, actions] = useApp();
// Create a new user
const userId = generateUserId();
actions.addUser('John');
actions.addTodo('Build feature', userId);🎓 Learning Resources
| Resource | Description | | ------------------------------------------------------------------------------------ | --------------------------------- | | 🎮 Live Demo | Interactive examples | | 🎥 Video Tutorial | Full walkthrough | | 💻 CodePen | Try it online | | 📚 400+ Tests | Check the test suite for patterns |
🌐 Platform-Specific Versions
| Package | Platform | Special Feature |
| -------------------------------------------------------------------------------------------------- | -------------------- | ------------------------ |
| react-hooks-global-states | React / React Native | Core library |
| react-global-state-hooks | Web | localStorage integration |
| react-native-global-state-hooks | React Native | AsyncStorage integration |
🎉 Why Developers Choose This
"I replaced 500 lines of Redux with 50 lines of this. Mind blown." 🤯
- Every developer who tries it
"The useState I always wanted." ❤️
- React developers everywhere
"Finally, state management that doesn't fight me." 🥊
- Tired developers worldwideThe Bottom Line
| What You Get | What You Don't |
| --------------------------- | --------------------- |
| ✅ useState API | ❌ Boilerplate |
| ✅ Surgical re-renders | ❌ Whole-tree updates |
| ✅ Chainable selectors | ❌ Repetitive code |
| ✅ TypeScript inference | ❌ Manual typing |
| ✅ Global + Context | ❌ Either/or choice |
| ✅ Actions (optional) | ❌ Required structure |
| ✅ 30-second learning curve | ❌ Week-long training |
🚀 Get Started Now
npm install react-hooks-global-statesThen in your app:
import { createGlobalState } from 'react-hooks-global-states';
const useTheme = createGlobalState('light');
function App() {
const [theme, setTheme] = useTheme();
return <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>Toggle Theme</button>;
}That's it. You're done. 🎉
