pocket-state
Version:
tiny global store
247 lines (179 loc) • 5.51 kB
Markdown
# pocket-state
A lightweight, typed, and framework-agnostic state management library.
Supports **selectors**, **middleware**, and **Immer-style updates**.
Works seamlessly **inside React** with hooks or **outside React** with a simple API.
## ✨ Features
- ⚡ Minimal API – Simple and powerful.
- 🎯 Selectors – Subscribe to slices of state (store-level and hook-level).
- 🌀 Immer support – Mutate drafts safely with `setImmer`.
- 🔌 Framework-agnostic – Works in plain TS/JS and React.
- 🛠 Middleware – Logging, persistence, batching, devtools bridges, etc.
- 🔔 Event Emitter – Subscribe to store and custom events.
- ⚖️ EqualityFn – Custom comparator (shallow by default, can use deep/custom).
- ✅ TypeScript-first – Fully type-safe.
## 📦 Installation
```bash
npm install pocket-state
# or
yarn add pocket-state
# or
pnpm add pocket-state
```
## 🚀 Usage
### 1) Create a Store
```ts
import {createStore} from 'pocket-state';
interface Counter {
count: number;
flag: boolean;
}
export const counterStore = createStore<Counter>({
count: 0,
flag: false,
});
```
### 2) Read & Write
```ts
// Read full state
console.log(counterStore.getValue()); // { count: 0, flag: false }
// Read by key
console.log(counterStore.getValue('count')); // 0
// Update via partial
counterStore.setValue({flag: true});
// Update via function
counterStore.setValue(s => ({count: s.count + 1}));
// Async update
counterStore.setValue(async s => {
const delta = await Promise.resolve(2);
return {count: s.count + delta};
});
```
### 3) Immer Updates
📦 Install immer
```bash
npm install immer
# or
yarn add immer
# or
pnpm add immer
```
```ts
counterStore.setImmer(draft => {
draft.count++;
draft.flag = !draft.flag;
});
```
### 4) Reset & Dirty Check
```ts
counterStore.reset(); // reset to initial
counterStore.reset({count: 10}); // reset with override
console.log(counterStore.isDirty()); // true/false
```
### 5) Subscriptions (Outside React)
```ts
// Entire state
const unsub = counterStore.subscribe(state => {
console.log('New state:', state);
});
// With selector
const unsubCount = counterStore.subscribe(
s => s.count,
count => console.log('Count changed:', count),
);
unsub();
unsubCount();
```
## 🎣 Custom Hooks with `createHook`
A utility to generate custom, type-safe hooks for your stores —
allowing you to access the store API without re-renders,
or subscribe to selected state slices with precise control.
```ts
import {createHook} from 'pocket-state';
import {counterStore} from './counterStore';
// Generate a custom hook
const useCounter = createHook(counterStore);
// Access only store API (no re-render)
const {reset, setValue} = useCounter();
// Access selected state (with API), triggers re-render on changes
const {value: count, reset} = useCounter(state => state.count);
```
### 🧩 **React Example**
```tsx
import React from 'react';
import {Text, Button, View} from 'react-native';
import {createStore, createHook} from 'pocket-state';
// 1. Create your store
interface Counter {
count: number;
}
const counterStore = createStore<Counter>({count: 0});
// 2. Create the custom hook
const useCounter = createHook(counterStore);
// 3. Use inside a React Native component
export function CounterComponent() {
// Only gets API, no re-render
const {reset} = useCounter();
// Gets selected value + API, re-renders when count changes
const {value: count, reset: reset2} = useCounter(state => state.count);
return (
<View>
<Text>Count: {count}</Text>
<Button
title="Inc"
onPress={() => counterStore.setValue(s => ({count: s.count + 1}))}
/>
<Button title="Reset" onPress={reset} />
</View>
);
}
```
**Advantages:**
- **No accidental re-renders** when accessing only API methods.
- **Type-safe selectors** and API usage, with full IDE support.
- **Simple migration path** for those coming from Zustand or similar libraries.
## ⚖️ EqualityFn
By default, `createStore` uses a shallow equality function to detect changes.
You can override this by providing a custom comparator as the 3rd argument:
```ts
import {createStore} from 'pocket-state';
import deepEqual from 'fast-deep-equal';
interface State {
count: number;
nested: {a: number; b: number};
}
// shallow equality (default)
const store1 = createStore<State>({count: 0, nested: {a: 1, b: 2}});
// deep equality
const store2 = createStore<State>(
{count: 0, nested: {a: 1, b: 2}},
[],
deepEqual,
);
```
This is useful when working with nested state objects and you want to avoid unnecessary re-renders.
## 🧩 API Reference
### `Store<T>`
- `getValue(): T` and `getValue(key: K): T[K]`
- `setValue(patch | (state) => patch | Promise<patch>)`
- `setImmer((draft) => void)`
- `reset(next?: T | Partial<T>)`
- `subscribe(listener)` and `subscribe(selector, listener)`
- `isDirty()`
- `getInitialValue()`
- `getNumberOfSubscriber()`
### `useStore(store, selector?)` (React)
Lightweight hook built on `useSyncExternalStore` that subscribes to your store and returns the selected slice.
### `createHook(store)` (React)
Generates a custom hook for your store.
- `hook()` → store API only, **no re-render**.
- `hook(selector)` → `{ value, ...api }`, re-renders when selected state changes.
## 📜 License
MIT
keywords: pocket-state, state-management, react, react-native, typescript, hooks, store, equalityFn