Skip to content

Repository files navigation

@icydotdev/pocket — React Context syntax in 1 line

pocket

Pocket-sized React Context syntax. One call, zero boilerplate.

npm versionnpm downloadslicense

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:functionToggle(){const[theme,setTheme]=useTheme();return(<buttononClick={()=>setTheme(theme==='light' ? 'dark' : 'light')}>{theme}</button>);}

Features

  • One-call ContextcreatePocket(setup) returns [Provider, useValues]. No createContext, no useContext, no manual undefined throw, no hand-written wrapper component
  • Tuple passthrough — Return useState directly and consumers destructure exactly like useState. 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 as useTheme
  • 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.tsximport{createContext,useContext,useState,typeReactNode}from'react';typeThemeCtx={theme: 'light'|'dark';setTheme: (t: 'light'|'dark')=>void;};constThemeContext=createContext<ThemeCtx|undefined>(undefined);exportfunctionThemeProvider({ children }: {children: ReactNode}){const[theme,setTheme]=useState<'light'|'dark'>('light');return(<ThemeContext.Providervalue={{ theme, setTheme }}>{children}</ThemeContext.Provider>);}exportfunctionuseTheme(){constv=useContext(ThemeContext);if(!v)thrownewError('useTheme must be used inside ThemeProvider');returnv;}

After — pocket

// theme.tsimport{useState}from'react';import{createPocket}from'@icydotdev/pocket';exportconst[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';exportconst[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 };});
functionSearch(){const{ filter, setFilter }=useDashboard();return<inputvalue={filter}onChange={(e)=>setFilter(e.target.value)}/>;}

Tuple passthrough

Return a tuple → consumers destructure a tuple. Single-value contexts become one-liners:

exportconst[ThemeProvider,useTheme]=createPocket(()=>useState<'light'|'dark'>('light'),);functionThemeToggle(){const[theme,setTheme]=useTheme();// mirrors useStatereturn(<buttononClick={()=>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 };},);<Counterinitial={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><Counterinitial={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 ^booleanconst[,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:

exportconst[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 ProviderZustand
Fine-grained re-renders / selectorsJotai or use-context-selector
Server Components stateRSC + props

Comparison

createPocketvanilla ContextconstateZustand
Lines of boilerplate~1~20~6~5
Provider requiredyesyesyesno
Tuple passthroughyesyesno (forces object)n/a
Named Provider in DevToolsautomanualmanualn/a
Named error messagesautomanualgenericn/a
Inline setup (no named hook)yesn/anoyes
Selectorsnonoyesyes
Thin layer over Contextyesyesyesno (custom store)
Bundle sizetiny0tinysmall

vs constate

Same factory shape, sharper edges:

  • Tuple passthroughcreatePocket(() => 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 migrate rewrites vanilla createContext files
  • Selector hook via useSyncExternalStore for opt-in fine-grained re-renders
  • Async-state primitive — createPocketAsync(promise) exposing { data, loading, error }
  • CLI scaffolder — npx @icydotdev/pocket add Theme drops 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 test

License

MIT © Sam Kavanagh

About

Pocket-sized React Context syntax.

Resources

Stars

39 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages