Skip to content

icydotdev/pocket

Repository files navigation

@icydotdev/pocket — React Context syntax in 1 line

pocket

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

npm version npm downloads license

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 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.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 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

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors