@melodyleonard/react-context-toolkit
v0.1.0
Published
Typed React Context store creator and provider composer
Maintainers
Readme
React Context Toolkit
Typed, minimal helpers for shaping React Context into ergonomic, composable stores — plus a simple way to nest multiple providers without noise.
Why
- You like Context for simple app state but want ergonomic hooks and strong typing
- You often end up stacking providers (Theme, Auth, Feature flags, etc.) and want a tidy, declarative composition
- You need optional persistence (local/session/custom) without pulling in a heavy state manager
Key features
- makeStore<S, A>: create a typed Context store and hooks
- Returns { Provider, useStore, useDispatch }
- Strong generics and zero any
- Optional persistence with a pluggable StorageAdapter
- composeProviders([...Providers]): turn N providers into one
- Clean App wrappers with a single composed provider
- SSR-safe persistence: localStorage only used when window is available
Installation
# npm
npm install @melodyleonard/react-context-toolkit
# yarn
yarn add @melodyleonard/react-context-toolkit
# pnpm
pnpm add @melodyleonard/react-context-toolkitCompatibility
- React 18+ (works with React 19)
- TypeScript recommended (strongly typed generics)
- Works in Vite, CRA, Next.js, etc.
Quick start
- Create a store (Theme)
import { makeStore } from '@melodyleonard/react-context-toolkit';
type ThemeState = { mode: 'light' | 'dark' };
type ThemeAction = { type: 'TOGGLE_THEME' };
const initial_state: ThemeState = { mode: 'light' };
export const {
useStore: useThemeStore,
useDispatch: useThemeDispatch,
Provider: ThemeProvider,
} = makeStore<ThemeState, ThemeAction>(
'theme',
initial_state,
(state, action) => {
switch (action.type) {
case 'TOGGLE_THEME':
return { mode: state.mode === 'light' ? 'dark' : 'light' };
default:
return state;
}
},
{ persist: true },
);- Create a store (Auth)
import { makeStore } from '@melodyleonard/react-context-toolkit';
type AuthState = { user: string | null };
type AuthAction = { type: 'LOGIN'; payload: string } | { type: 'LOGOUT' };
const initial_state: AuthState = { user: null };
export const {
useStore: useAuthStore,
useDispatch: useAuthDispatch,
Provider: AuthProvider,
} = makeStore<AuthState, AuthAction>(
'auth',
initial_state,
(state, action) => {
switch (action.type) {
case 'LOGIN':
return { user: action.payload };
case 'LOGOUT':
return { user: null };
default:
return state;
}
},
{ persist: true },
);- Compose providers once
// src/Providers.tsx
import { composeProviders } from '@melodyleonard/react-context-toolkit';
import { ThemeProvider } from './store/themeStore';
import { AuthProvider } from './store/authStore';
const Providers = composeProviders([ThemeProvider, AuthProvider]);
export default Providers;- Use in App
// src/App.tsx
import Providers from './Providers';
import ExampleComponent from './components/ExampleComponent';
export default function App() {
return (
<Providers>
<ExampleComponent />
</Providers>
);
}- Use the hooks anywhere
// src/components/ExampleComponent.tsx
import { useThemeStore, useThemeDispatch } from '../store/themeStore';
import { useAuthStore, useAuthDispatch } from '../store/authStore';
export default function ExampleComponent() {
const theme = useThemeStore();
const themeDispatch = useThemeDispatch();
const auth = useAuthStore();
const authDispatch = useAuthDispatch();
return (
<div style={{
background: theme.mode === 'light' ? '#fff' : '#333',
color: theme.mode === 'light' ? '#000' : '#fff',
}}>
<p>Theme: {theme.mode}</p>
<button onClick={() => themeDispatch({ type: 'TOGGLE_THEME' })}>Toggle Theme</button>
<p>User: {auth.user ?? 'Not logged in'}</p>
<button onClick={() => authDispatch({ type: 'LOGIN', payload: '[email protected]' })}>Login</button>
<button onClick={() => authDispatch({ type: 'LOGOUT' })}>Logout</button>
</div>
);
}Persistence (StorageAdapter)
You can plug in any storage mechanism. By default, localStorage is used when available (SSR-safe).
import type { StorageAdapter } from '@melodyleonard/react-context-toolkit';
const sessionAdapter: StorageAdapter<MyState> = {
get: (key) => {
try {
const raw = sessionStorage.getItem(key);
return raw ? (JSON.parse(raw) as MyState) : null;
} catch {
return null;
}
},
set: (key, value) => {
try {
sessionStorage.setItem(key, JSON.stringify(value));
} catch {
// ignore write errors
}
},
};
const { Provider } = makeStore<MyState, MyAction>(
'my-store',
initial,
reducer,
{ persist: { adapter: sessionAdapter } },
);SSR notes
- Persistence writes happen in an effect — no localStorage access during SSR
- Use a custom adapter to read/write from cookies or server/session storage when needed
TypeScript overview
- S represents your store state shape
- A represents your union of actions
- useStore() returns S
- useDispatch() returns React.Dispatch
FAQ
- Why Context, not Redux? Context is great for small/medium app state. This toolkit keeps it typed, minimal, and ergonomic. Use Redux or other tools if you need advanced devtools or middlewares.
- Can I share one store across routes/components? Yes — it’s a provider, so scope it wherever you need it shared.
- How do I avoid stale state across tabs? Use a storage adapter that syncs via storage events or add a small cross-tab sync.
Roadmap
- CLI to scaffold stores and re-compose providers automatically
- Optional devtools (action/state logging)
- More adapters (AsyncStorage for React Native)
License MIT
