@icydotdev/pocket
v1.0.7
Published
Pocket-sized React Context syntax. One call, zero ceremony.
Maintainers
Readme
Pocket-sized React Context syntax. One call, zero boilerplate.
Quick Start · Features · Usage · FAQ · Comparison · Contributing
Stop declaring contexts. Stop building Provider components. Stop wiring up useContext and throwing on undefined. pocket collapses React Context boilerplate into one call — and stays a thin layer over the Context API. No store, no atoms, no selectors, no magic. If you know React Context, you already know how this behaves.
Quick Start
npm i @icydotdev/pocket// 1. One call returns a typed Provider AND a hook to consume it:
const [ThemeProvider, useTheme] = createPocket(() => useState('light'));
// 2. Drop the Provider anywhere — same as vanilla Context:
<ThemeProvider>
<Toggle />
</ThemeProvider>;
// 3. Consume from any descendant — also same as vanilla Context:
function Toggle() {
const [theme, setTheme] = useTheme();
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
{theme}
</button>
);
}Features
- One-call Context —
createPocket(setup)returns[Provider, useValues]. NocreateContext, nouseContext, no manualundefinedthrow, no hand-written wrapper component - Tuple passthrough — Return
useStatedirectly and consumers destructure exactly likeuseState. Single-value contexts become one-liners - Provider props — The setup function takes whatever props you pass to the Provider. Per-instance config for free
- Per-instance state — Each
<Provider/>mount runs its own setup. Same Context, independent state per mount. Nesting works like normal Context - Named errors — Pass a name once and get descriptive error messages:
"Theme consumer called outside <Theme.Provider>"instead of a generic throw - React DevTools labels — Provider shows as
<Theme.Provider>in the tree, consumer hook registers asuseTheme - Inline setup — Pass an arrow function directly. No separate
function useCounter()definition required first - Full TypeScript inference — Types inferred from the setup function's return. Zero annotations
- Thin layer over Context — Same render semantics, same SSR story, same mental model. If you need fine-grained re-renders, reach for Zustand or Jotai — this stays close to vanilla Context on purpose
- Tiny bundle — Single tree-shakeable export, zero runtime dependencies
Before / After
The classic 20-line createContext + useContext + ThemeProvider + useTheme boilerplate collapses to one line.
Before — vanilla Context
// theme-context.tsx
import { createContext, useContext, useState, type ReactNode } from 'react';
type ThemeCtx = {
theme: 'light' | 'dark';
setTheme: (t: 'light' | 'dark') => void;
};
const ThemeContext = createContext<ThemeCtx | undefined>(undefined);
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const v = useContext(ThemeContext);
if (!v) throw new Error('useTheme must be used inside ThemeProvider');
return v;
}After — pocket
// theme.ts
import { useState } from 'react';
import { createPocket } from '@icydotdev/pocket';
export const [ThemeProvider, useTheme] = createPocket(() =>
useState<'light' | 'dark'>('light'),
);~22 lines of vanilla Context → 1. Consumers destructure [theme, setTheme] exactly like useState.
Usage
Basic
createPocket takes a setup function. It runs inside the Provider on every render — call any hooks you want (useState, useEffect, useReducer, custom hooks). Return whatever consumers should see.
import { useState, useEffect } from 'react';
import { createPocket } from '@icydotdev/pocket';
export const [DashboardProvider, useDashboard] = createPocket(() => {
const [filter, setFilter] = useState('');
const [sortBy, setSortBy] = useState<'date' | 'name'>('date');
useEffect(() => {
document.title = `Dashboard — ${filter || 'all'}`;
}, [filter]);
return { filter, setFilter, sortBy, setSortBy };
});function Search() {
const { filter, setFilter } = useDashboard();
return <input value={filter} onChange={(e) => setFilter(e.target.value)} />;
}Tuple passthrough
Return a tuple → consumers destructure a tuple. Single-value contexts become one-liners:
export const [ThemeProvider, useTheme] = createPocket(() =>
useState<'light' | 'dark'>('light'),
);
function ThemeToggle() {
const [theme, setTheme] = useTheme(); // mirrors useState
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
{theme}
</button>
);
}Provider props
The setup function can take props. The Provider forwards everything except children:
const [Counter, useCounter] = createPocket(
({ initial = 0 }: { initial?: number }) => {
const [n, setN] = useState(initial);
return { n, setN };
},
);
<Counter initial={42}>
<Display />
</Counter>;Per-instance state
Multiple <Provider/> mounts each run their own setup → independent state. Same Context, different values per mount.
<Counter><Display /></Counter>
<Counter initial={100}><Display /></Counter>TypeScript
Types inferred from the setup function's return. Don't annotate.
const [, useValues] = createPocket(() => ({ a: 1, b: 'x', c: true }));
const { a, b, c } = useValues();
// ^number ^string ^boolean
const [, useTheme] = createPocket(() => useState<'light' | 'dark'>('light'));
const [theme, setTheme] = useTheme();
// ^'light'|'dark' ^Dispatch<SetStateAction<...>>Name your pocket (recommended)
Pass a name as the first argument. Two upgrades for free:
export const [ThemeProvider, useTheme] = createPocket('Theme', () => {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
return { theme, setTheme };
});1. Better error messages. When a consumer calls outside the Provider:
Error: Theme consumer called outside <Theme.Provider>.
Wrap the calling component in <Theme.Provider> or check your tree.vs unnamed:
Error: Pocket consumer called outside <Pocket.Provider>.
Wrap the calling component in <Pocket.Provider> or check your tree.2. React DevTools labels. The Provider shows as <Theme.Provider> in the component tree, not <ContextProvider>. The consumer hook registers as useTheme.
Skip the name and everything still works: the default is 'Pocket'.
When NOT to use it
| You need... | Use this instead |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| Cross-component state with no Provider | Zustand |
| Fine-grained re-renders / selectors | Jotai or use-context-selector |
| Server Components state | RSC + props |
Comparison
| | createPocket | vanilla Context | constate | Zustand |
| ---------------------------- | -------------- | --------------- | ------------------------------------------------ | ----------------- |
| Lines of boilerplate | ~1 | ~20 | ~6 | ~5 |
| Provider required | yes | yes | yes | no |
| Tuple passthrough | yes | yes | no (forces object) | n/a |
| Named Provider in DevTools | auto | manual | manual | n/a |
| Named error messages | auto | manual | generic | n/a |
| Inline setup (no named hook) | yes | n/a | no | yes |
| Selectors | no | no | yes | yes |
| Thin layer over Context | yes | yes | yes | no (custom store) |
| Bundle size | tiny | 0 | tiny | small |
vs constate
Same factory shape, sharper edges:
- Tuple passthrough —
createPocket(() => useState(...))works. Constate forces an object wrap. - Auto-named Provider + errors —
'Theme'once, get<Theme.Provider>in DevTools and named errors for free. - Inline setup — pass an arrow directly, no
function useCounter() {...}definition required first.
vs Zustand / Jotai
pocket stays close to vanilla Context on purpose — same render semantics, same mental model, same SSR story. If you need fine-grained re-renders, reach for Zustand/Jotai. If you want Context without the ceremony, this is it.
FAQ
Is it a hook?
createPocket is a module-level factory. The useValues it returns IS a hook — call it at the top of a component. eslint-plugin-react-hooks enforces rules-of-hooks correctly.
Can the setup function call hooks?
Yes. It runs inside the Provider on each render, so anything legal in a custom hook is legal there.
Multiple Providers?
Each <Provider/> mount runs its own setup → independent state. Nesting works like normal Context: inner overrides outer.
Why no value prop on the Provider?
Values come from the setup function. The Provider accepts any props the setup function declares, plus children.
Why no selectors?
By design. pocket is a thin layer over Context — same render semantics. If you need selector-based re-render avoidance, use use-context-selector on top, or pick Jotai/Zustand.
Does it support React Server Components?
Not directly. Like all React Context, pocket needs "use client". RSC has its own state-passing model (props, server actions).
Roadmap
- DevTools panel — small inspector showing active pockets + current values
- Codemod —
npx @icydotdev/pocket migraterewrites vanillacreateContextfiles - Selector hook via
useSyncExternalStorefor opt-in fine-grained re-renders - Async-state primitive —
createPocketAsync(promise)exposing{ data, loading, error } - CLI scaffolder —
npx @icydotdev/pocket add Themedrops a typed Theme context into your project
Contributing
Contributions welcome. Open an issue or PR.
git clone https://github.com/icydotdev/pocket.git
cd pocket
npm install
npm testLicense
MIT © Sam Kavanagh
