diff --git a/AI_CONTEXT.md b/AI_CONTEXT.md index cbeb468..39f7aa6 100644 --- a/AI_CONTEXT.md +++ b/AI_CONTEXT.md @@ -1,44 +1,45 @@ -# React State Custom - AI Context & Best Practices +# React State Custom - AI Context -This document is designed to help AI assistants (Gemini, ChatGPT, Claude, Copilot) understand the preferred patterns and API usage for `react-state-custom`. +**Target Audience:** AI Assistants (Gemini, ChatGPT, Claude, Copilot) +**Goal:** Generate idiomatic, high-performance code using `react-state-custom`. -## ๐Ÿง  Mental Model +--- + +## ๐Ÿง  Mental Model: "It's Just a Hook" + +`react-state-custom` turns standard React hooks into shared global stores. -`react-state-custom` is a **hook-first** state management library. -- **State is just a hook**: You define state logic using standard React hooks (`useState`, `useEffect`, `useMemo`). -- **Stores are headless components**: The library runs your hook in a hidden component (`Root`) and publishes the results to an event-driven context. -- **Consumption is selective**: Components subscribe only to the specific fields they need, preventing unnecessary re-renders. +1. **Define Logic**: Write a hook (`useState`, `useEffect`, `useMemo`). +2. **Share Logic**: Wrap it with `createStore`. +3. **Consume Logic**: Use the generated hook in any component. + +**Key Difference:** Unlike Redux or Zustand, there is no external store object. The "store" is literally a React component running your hook in the background, managed by `AutoRootCtx`. + +--- -## ๐Ÿ† The "Golden Path" (Preferred API) +## ๐Ÿ† The Golden Path -Always prefer `createStore` over the lower-level `createRootCtx` / `createAutoCtx` primitives unless specifically requested. +**Always** follow this pattern unless explicitly asked for low-level primitives. -### 1. Define State (The Hook) -Write a standard React hook. It receives `params` (props) and `preState` (warm-start data). +### 1. The Store (Features/State) ```typescript -// state.ts +// features/counterState.ts import { useState } from 'react'; +import { createStore } from 'react-state-custom'; -export const useCounterState = ({ initial = 0 }: { initial?: number }) => { +// 1. Define the hook (Standard React) +const useCounterState = ({ initial = 0 }: { initial?: number }) => { const [count, setCount] = useState(initial); const increment = () => setCount(c => c + 1); return { count, increment }; }; -``` - -### 2. Create Store (The Factory) -Use `createStore` to generate the store hooks. - -```typescript -// state.ts -import { createStore } from 'react-state-custom'; +// 2. Export the store (One line) export const { useStore: useCounterStore } = createStore('counter', useCounterState); ``` -### 3. Mount Context (The Root) -Mount `` **once** near the top of the app. +### 2. The Root (App Entry) ```tsx // App.tsx @@ -47,22 +48,22 @@ import { AutoRootCtx } from 'react-state-custom'; export default function App() { return ( <> - + {/* ๐Ÿ‘ˆ Must be at the top */} ); } ``` -### 4. Consume State (The Component) -Use the generated `useStore` hook. It returns a proxy that tracks usage. +### 3. The Consumer (Components) ```tsx -// Counter.tsx -import { useCounterStore } from './state'; +// components/Counter.tsx +import { useCounterStore } from '../features/counterState'; export function Counter() { - // โšก๏ธ Only re-renders when 'count' changes + // 3. Use the hook + // โšก๏ธ Automatic subscription: re-renders ONLY when 'count' changes. const { count, increment } = useCounterStore({ initial: 10 }); return ; @@ -71,37 +72,41 @@ export function Counter() { --- -## โœ… Do's and โŒ Don'ts +## โœ… Best Practices -### โœ… DO -- **DO** use `createStore` for 95% of use cases. -- **DO** use `AutoRootCtx` to manage store lifecycles automatically. -- **DO** keep store parameters simple (primitives like strings/numbers) to ensure stable context IDs. -- **DO** destructure `useStore` results immediately during render (e.g., `const { data } = useStore(...)`). -- **DO** use `preState` in your hook if you need to persist state across hot-reloads or auto-remounts. +- **Params must be primitives**: Store parameters (`{ id: '123' }`) are serialized to create unique store instances. Avoid passing objects or callbacks as params. +- **Destructure immediately**: `const { data } = useStore(...)`. The returned object is a proxy that tracks usage during render. +- **No Providers**: Never manually wrap components in providers. `AutoRootCtx` handles everything. +- **Keep it simple**: Don't use `createRootCtx` or `createAutoCtx` directly. `createStore` is the only API you usually need. -### โŒ DON'T -- **DON'T** use `createRootCtx` and `createAutoCtx` manually unless you are building a custom abstraction. -- **DON'T** pass objects or arrays as store parameters (e.g., `useStore({ user: { id: 1 } })`). Pass IDs instead (`useStore({ userId: 1 })`). -- **DON'T** store the proxy returned by `useStore` in a `useRef` or `useEffect`. It is designed for render-phase tracking only. -- **DON'T** manually create `Context` objects via `new Context()` unless you are building a low-level primitive. +## ๐Ÿ› ๏ธ Common Patterns -## ๐Ÿ›  API Signatures (Simplified) +### Async Data (Data Fetching) ```typescript -// The main factory function -function createStore( - name: string, - useFn: (params: Params, preState: Partial) => State, - timeToClean?: number // Optional: keep-alive time in ms -): { - useStore: (params: Params) => State; // Returns a reactive proxy - useCtxState: (params: Params) => Context; // Returns the raw context -} +const useUserState = ({ userId }) => { + const [data, setData] = useState(null); + + useEffect(() => { + fetchUser(userId).then(setData); + }, [userId]); + + return { data, isLoading: !data }; +}; +``` + +### Derived State -// The global manager -function AutoRootCtx(props: { - Wrapper?: React.ComponentType; // e.g. ErrorBoundary - debugging?: boolean; -}): JSX.Element; +Since stores are just hooks, you can use `useMemo` for derived data. + +```typescript +const useCartState = () => { + const [items, setItems] = useState([]); + + const total = useMemo(() => + items.reduce((sum, item) => sum + item.price, 0) + , [items]); + + return { items, total }; +}; ``` diff --git a/API_DOCUMENTATION.md b/API_DOCUMENTATION.md index 5283be7..8f150e4 100644 --- a/API_DOCUMENTATION.md +++ b/API_DOCUMENTATION.md @@ -1,390 +1,67 @@ -# React State Custom - API Reference +# API Reference -a hook-first state management toolkit for React 19 applications. Every export surfaces typed utilities for wiring headless state containers, publishing updates, and consuming them with fine-grained subscriptions. +Complete documentation for the `react-state-custom` API. -> First time here? Skim the โ€œQuick Start (2 minutes)โ€ section in `README.md` before diving into the API surface. - -๐ŸŽฎ **[Try the Live Demo โ†’](https://vothanhdat.github.io/react-state-custom/)** - ---- - -## Contents - -- [React State Custom - API Reference](#react-state-custom---api-reference) - - [Contents](#contents) - - [Core Context System](#core-context-system) - - [`Context`](#context) - - [`getContext`](#getcontext) - - [`useDataContext`](#usedatacontext) - - [Publishing Hooks](#publishing-hooks) - - [`useDataSource`](#usedatasource) - - [`useDataSourceMultiple`](#usedatasourcemultiple) - - [Subscription Hooks](#subscription-hooks) - - [`useDataSubscribe`](#usedatasubscribe) - - [`useDataSubscribeMultiple`](#usedatasubscribemultiple) - - [`useDataSubscribeMultipleWithDebounce`](#usedatasubscribemultiplewithdebounce) - - [`useDataSubscribeWithTransform`](#usedatasubscribewithtransform) - - [`useQuickSubscribe`](#usequicksubscribe) - - [Store Factory](#store-factory) - - [`createStore`](#createstore) - - [Root Context Factory](#root-context-factory) - - [`createRootCtx`](#createrootctx) - - [Auto Context System](#auto-context-system) - - [`AutoRootCtx`](#autorootctx) - - [`createAutoCtx`](#createautoctx) - - [Developer Tools](#developer-tools) - - [`DevToolContainer`](#devtoolcontainer) - - [`DataViewComponent`](#dataviewcomponent) - - [Utility Hooks](#utility-hooks) - - [`useArrayChangeId`](#usearraychangeid) - - [Types](#types) - - [`ParamsToIdRecord`](#paramstoidrecord) - - [Usage Patterns](#usage-patterns) - - [Basic Context Wiring](#basic-context-wiring) - - [Headless Root + Auto Context](#headless-root--auto-context) - - [Live Examples](#live-examples) - ---- - -## Core Context System - -### `Context` - -EventTarget-backed data store that tracks the latest values for a shape `D` and notifies listeners on change. - -```ts -class Context extends EventTarget { - constructor(name: string) - name: string - data: Partial - registry: Set - useCounter: number - publish(key: keyof D, value: D[keyof D] | undefined): void - subscribe(key: keyof D, listener: (value: D[keyof D] | undefined) => void): () => void - subscribeAll(listener: (key: keyof D, snapshot: Partial) => void): () => void -} -``` - -- Publishes only when `value != data[key]` (loose inequality for shallow detection). -- `subscribe` immediately invokes the listener with the current value if present. -- `subscribeAll` fires for every key update via an internal `@--change-event` broadcast. -- `registry` collects active publishers for duplicate detection (see `useDataSource*`). - -```ts -interface UserState { name: string; age: number } -const users = new Context("users") -const stop = users.subscribe("name", value => console.log("name โ†’", value)) -users.publish("name", "Ada Lovelace") -stop() -``` - -### `getContext` - -Memoized `Context` factory keyed by name. - -```ts -function getContext(name: string): Context -``` - -- Returns the same instance for repeated calls with identical arguments. -- Internally memoized via JSON-stringified arguments and pruned when `useCounter` stays at zero for ~100 ms after unmount. - -```ts -const sessionCtx = getContext("session") -const sameInstance = getContext("session") -console.assert(sessionCtx === sameInstance) -``` - -### `useDataContext` - -React hook returning a typed `Context` by name. - -```ts -function useDataContext(name?: string): Context -``` - -- Memoizes `getContext(name)` and increments `ctx.useCounter` while mounted. -- Automatically evicts unused contexts shortly after the last consumer unmounts. - -```tsx -function SessionProvider({ children }: { children: React.ReactNode }) { - const ctx = useDataContext<{ token?: string }>("session") - useDataSource(ctx, "token", readSessionToken()) - return <>{children} -} -``` - ---- - -## Publishing Hooks - -### `useDataSource` - -Publishes a single key whenever `value` changes. - -```ts -function useDataSource( - ctx: Context | undefined, - key: K, - value: D[K] | undefined -): void -``` - -- Publishes inside an effect to avoid React render side effects. -- Skips publication when `ctx` is undefined or `value == ctx.data[key]`. -- Registers `key` in `ctx.registry`; duplicate publishers log an error with a captured stack trace. - -```tsx -function UserSource({ userId }: { userId: string }) { - const ctx = useDataContext("user") - const profile = useUserProfile(userId) - useDataSource(ctx, "profile", profile) - return null -} -``` - -### `useDataSourceMultiple` - -Batch publisher for multiple `[key, value]` tuples. - -```ts -function useDataSourceMultiple( - ctx: Context | undefined, - ...entries: { -readonly [P in keyof T]: [T[P], D[T[P]]] } -): void -``` - -- Shallow-compares each pair and publishes only the changed ones. -- Dependencies collapse to a hash from [`useArrayChangeId`](#usearraychangeid); pass stable tuples to avoid extra flushes. -- Runs the same duplicate-source safety check as `useDataSource`. - -```tsx -useDataSourceMultiple(ctx, - ["user", user], - ["theme", theme], - ["isLoading", loading], -) -``` +> **Note:** For most applications, you only need `createStore` and `AutoRootCtx`. The other APIs are lower-level primitives used internally or for advanced customization. --- -## Subscription Hooks - -### `useDataSubscribe` - -Subscribes to a single key with optional debounce. - -```ts -function useDataSubscribe( - ctx: Context | undefined, - key: K, - debounceTime?: number -): D[K] | undefined -``` - -- Uses React state to trigger re-renders; initial state mirrors `ctx?.data[key]`. -- `debounceTime > 0` wraps updates in `debounce` with `.cancel()` support on cleanup. -- Returns the live value (`ctx?.data[key]`) so reads stay current even if the hook is mid-debounce. - -```tsx -const searchQuery = useDataSubscribe(ctx, "query", 250) -``` - -### `useDataSubscribeMultiple` - -Observes several keys and re-renders when any value differs from the previous snapshot. - -```ts -function useDataSubscribeMultiple( - ctx: Context | undefined, - ...keys: K -): { [P in K[number]]: D[P] | undefined } -``` - -- Aggregates values into an object keyed by the provided names. -- Internally debounces change detection to the next macrotask (1 ms) to coalesce bursts. -- Automatically unsubscribes all listeners on unmount. - -```tsx -const { user, isLoading } = useDataSubscribeMultiple(ctx, "user", "isLoading") -``` - -### `useDataSubscribeMultipleWithDebounce` - -Like `useDataSubscribeMultiple`, but allows specifying a debounce window. - -```ts -function useDataSubscribeMultipleWithDebounce( - ctx: Context | undefined, - debounceTime?: number, - ...keys: K -): { [i in keyof K]: D[K[i]] | undefined } -``` - -- Default debounce is 50 ms. -- Returns an array keyed by index (matches the order of `keys`). - -```tsx -const [query, filters] = useDataSubscribeMultipleWithDebounce(ctx, 200, "query", "filters") -``` - -### `useDataSubscribeWithTransform` - -Subscribes to one key and recomputes derived data lazily. - -```ts -function useDataSubscribeWithTransform( - ctx: Context | undefined, - key: K, - transform: (value: D[K] | undefined) => E -): E -``` - -- Memoizes `transform(ctx?.data[key])` and only triggers the internal `setState` when the transformed value actually changes (`!=`). -- Handy for formatting or computing derived values without extra selectors. - -```tsx -const stats = useDataSubscribeWithTransform(ctx, "profile", profile => ({ - postCount: profile?.posts?.length ?? 0, - joinedAt: profile?.createdAt ? new Date(profile.createdAt) : null, -})) -``` - -### `useQuickSubscribe` - -Proxy-based subscription that tracks which properties you read during render and subscribes to those keys only. - -```ts -function useQuickSubscribe( - ctx: Context | undefined -): { [P in keyof D]?: D[P] | undefined } -``` - -- Wraps `ctx?.data` in a `Proxy`; the first read cycle records accessed keys. -- After render, subscribes to the accessed set and cleans up unused subscriptions automatically. -- Subsequent renders reuse the same proxy; avoid storing it outside render scope. - -```tsx -const { total, items } = useQuickSubscribe(cartCtx) -``` - -> โš ๏ธ The proxy intentionally throws when accessed outside of render. Always destructure the fields you need synchronously during render and avoid passing the proxy to refs, effects, or callbacks. - ---- - -## Store Factory +## โšก Primary API ### `createStore` -The all-in-one helper that combines `createRootCtx` and `createAutoCtx` into a single call. This is the recommended way to create stores in most applications. +The main entry point. Converts a standard React hook into a shared, auto-managed store. -```ts -function createStore>( +```typescript +function createStore( name: string, - useFn: (params: U, preState: Partial) => V, + useFn: (params: Params, preState: Partial) => State, timeToClean?: number, - AttatchedComponent?: React.FC + AttachedComponent?: React.FC ): { - useCtxState(params: U): Context - useStore(params: U): { [P in keyof V]?: V[P] | undefined } + useStore(params: Params): State; + useCtxState(params: Params): Context; } ``` -- **`name`**: Unique namespace for this store. -- **`useFn`**: The hook that defines your state logic. -- **`timeToClean`**: Optional delay (ms) before unmounting the store after the last subscriber leaves. -- **`AttatchedComponent`**: Optional component to render alongside the store (useful for side effects). +#### Arguments +- **`name`** *(string)*: A unique namespace for this store (e.g., `'user'`, `'cart'`). +- **`useFn`** *(function)*: Your custom hook. Receives `params` and optional `preState`. +- **`timeToClean`** *(number, optional)*: Time in milliseconds to keep the store alive after the last subscriber unmounts. Defaults to `0`. +- **`AttachedComponent`** *(Component, optional)*: A React component that renders alongside the store root. Useful for side effects (like data fetching or logging) that should run exactly once per store instance. -Returns an object with: -- **`useStore`**: A hook that subscribes to the store and returns a proxy for reading state. -- **`useCtxState`**: A hook that returns the raw `Context` object (for advanced usage). +#### Returns +- **`useStore`**: The consumer hook. Call this in your components to read state. It returns a **proxy** that automatically tracks which properties you access to optimize re-renders. +- **`useCtxState`**: Returns the raw `Context` object. Useful for advanced integrations. +#### Example ```tsx -// 1. Define store -const { useStore } = createStore('counter', () => { - const [count, setCount] = useState(0); +const useCounter = ({ initial }) => { + const [count, setCount] = useState(initial); return { count, setCount }; -}); +}; -// 2. Use in component -function Counter() { - const { count, setCount } = useStore({}); - return ; -} +export const { useStore } = createStore('counter', useCounter); ``` --- -## Root Context Factory - -### `createRootCtx` - -Creates a headless Root component that runs your hook `useFn(props, preState)` exactly once per parameter set and publishes its return shape into a derived context namespace. `preState` is the previously published data for that context name (if any), useful for rehydrating state after an auto-unmount/remount cycle. - -```ts -import { ParamsToIdRecord } from 'react-state-custom' - -function createRootCtx>( - name: string, - useFn: (params: U, preState: Partial) => V -): { - name: string - getCtxName(params: U): string - useRootState(params: U): V - Root: React.FC - useCtxState(params: U): Context - useCtxStateStrict(params: U): Context -} -``` - -Key behaviors: - -- Context name = `name` plus serialized, sorted props (for example `user-state-userId-123`). -- `useFn` receives the last published state (if any) as `preState`, so you can warm start when a Root remounts. -- The generated `Root` publishes every key returned by `useFn` via `useDataSourceMultiple`. -- Duplicate `Root` mounts with the same resolved name throw (stack trace captured at mount). -- Props passed to `Root`/`useCtxState` must be primitives (string/number/boolean/bigint/null/undefined); `paramsToId` rejects objects so context IDs remain deterministic. -- `useCtxStateStrict` throws if the `Root` is missing; `useCtxState` schedules a delayed `console.error` instead (1 s). - -```tsx -const { Root: UserRoot, useCtxState } = createRootCtx("user", useUserState) - -function UserProvider({ userId, children }: { userId: string; children: React.ReactNode }) { - return ( - <> - - {children} - - ) -} - -function UserName({ userId }: { userId: string }) { - const ctx = useCtxState({ userId }) - const { profile } = useDataSubscribeMultiple(ctx, "profile") - return {profile?.name} -} -``` - ---- - -## Auto Context System - ### `AutoRootCtx` -Global manager that renders requested Root instances lazily. +The global manager component. Must be mounted once at the top of your application. -```ts +```typescript function AutoRootCtx(props: { - Wrapper?: React.FC - debugging?: boolean + Wrapper?: React.FC<{ children: React.ReactNode }>; + debugging?: boolean; }): JSX.Element ``` -- Mount exactly once near your app root. -- Stores a registry of active roots in the special context `auto-ctx`, publishing `subscribe` and `state` for internal coordination. -- Each subscription entry tracks usage count and optional delayed teardown (`keepUntil`). -- `Wrapper` lets you provide an ErrorBoundary-like component; defaults to `Fragment`. Set `debugging` to render ephemeral state snapshots for inspection. +#### Props +- **`Wrapper`** *(Component, optional)*: A wrapper component for each store instance (e.g., an ErrorBoundary). Defaults to `React.Fragment`. +- **`debugging`** *(boolean, optional)*: If `true`, renders a raw view of the state in the DOM for debugging. +#### Example ```tsx function App() { return ( @@ -392,229 +69,110 @@ function App() { - ) -} -``` - -### `createAutoCtx` - -Connects a `createRootCtx` factory to `AutoRootCtx`, returning a consumer hook that ensures the corresponding Root is mounted on demand. - -```ts -function createAutoCtx>( - rootCtx: ReturnType>, - unmountDelayMs?: number, - AttatchedComponent?: React.FC -): { - useCtxState(params: U): Context - useStore(params: U): { [P in keyof V]?: V[P] | undefined } + ); } ``` -- Subscribes to the global `auto-ctx` context and asks `AutoRootCtx` to mount the root. -- `unmountDelayMs` (default 0) keeps instances alive briefly after the last subscriber disconnects, smoothing mount/unmount thrash. -- `AttatchedComponent` (optional) is a React component that receives the same params as the state hook. It renders alongside each auto-mounted root instance, useful for side effects, portals, or UI that should live alongside the state. -- Consumers simply call `useCtxState(params)` or `useStore(params)`; no manual Root mounting required. - -```tsx -const { useCtxState: useUserCtx, useStore: useUserStore } = createAutoCtx(createRootCtx("user", useUserState), 200) - -function UserCard({ userId }: { userId: string }) { - // Quick access via useStore - const { profile } = useUserStore({ userId }) - - // Or manual context access - const ctx = useUserCtx({ userId }) - // ... - return {profile?.name} -} -``` - -**With AttatchedComponent:** - -```tsx -// A component that logs when a user context is active -const UserLogger: React.FC<{ userId: string }> = ({ userId }) => { - useEffect(() => { - console.log(`User ${userId} context mounted`) - return () => console.log(`User ${userId} context unmounted`) - }, [userId]) - return null -} - -const { useCtxState: useUserCtx } = createAutoCtx( - createRootCtx("user", useUserState), - 200, - UserLogger -) -``` - --- -## Developer Tools +## ๐Ÿ› ๏ธ Developer Tools ### `DevToolContainer` -Popup inspector that renders the current data snapshot for every context. +A floating inspector to visualize all active stores and their state in real-time. -```ts +```typescript function DevToolContainer(props: { - toggleButton?: string - Component?: DataViewComponent - style?: React.CSSProperties - children?: React.ReactNode + toggleButton?: string; + Component?: DataViewComponent; + style?: React.CSSProperties; + children?: React.ReactNode; }): JSX.Element ``` -- Togglable overlay; pass `Component` to customize how values render (defaults to JSON). -- Docks to the bottom of the viewport and exposes resizable panes (via `@uiw/react-split`) so you can balance app viewport and inspector real estate on the fly. -- Import `react-state-custom/dist/react-state-custom.css` to get the required styles. -- Provide `children` to override the floating toggle button label (defaults to "Toggle Dev Tool"). -- Works best alongside `` so all contexts are represented. +#### Props +- **`toggleButton`** *(string, optional)*: Text for the toggle button. +- **`Component`** *(Component, optional)*: Custom renderer for state values. Defaults to a JSON tree view. +- **`children`** *(ReactNode, optional)*: Custom trigger button content. +#### Example ```tsx -import "react-state-custom/dist/react-state-custom.css" +import { DevToolContainer } from 'react-state-custom'; +import 'react-state-custom/dist/react-state-custom.css'; -function DevShell() { - return ( - <> - - - - - ) -} -``` - -### `DataViewComponent` - -Type for custom dev tool renderers. - -```ts -type DataViewComponent = React.FC<{ name: string; value: any }> -``` - -```tsx -const ObjectViewer: DataViewComponent = ({ name, value }) => ( -
-

{name}

-
{JSON.stringify(value, null, 2)}
-
-) + ``` --- -## Utility Hooks +## โš™๏ธ Advanced API (Primitives) -### `useArrayChangeId` +These APIs are used internally by `createStore`. You generally don't need them unless you're building custom abstractions. -Shallow change detector for arrays. Returns a random string that flips whenever the tracked array differs from the last render. - -```ts -function useArrayChangeId(values: any[]): string -``` +### `createRootCtx` -- Compares length and each element via `!=`. -- Useful for collapsing complex tuple dependencies into a single stable key. +Creates a headless "Root" component that runs a hook and publishes its result to a context. -```tsx -const changeKey = useArrayChangeId(entries.flat()) -useEffect(() => { - // expensive work runs only when the tuple content changes -}, [changeKey]) +```typescript +function createRootCtx( + name: string, + useFn: (params: Params, preState: Partial) => State +): { + Root: React.FC; + useCtxState(params: Params): Context; + // ...other internal helpers +} ``` ---- - -## Types - -### `ParamsToIdRecord` +### `createAutoCtx` -Type constraint for parameters passed to `createRootCtx` and `createAutoCtx`. Only primitive values are allowed to ensure deterministic context naming. +Connects a `createRootCtx` result to the `AutoRootCtx` system for automatic mounting. -```ts -type ParamsToIdRecord = Record +```typescript +function createAutoCtx( + rootCtx: ReturnType, + timeToClean?: number, + AttachedComponent?: React.FC +): Result ``` -- Keys must be strings -- Values must be primitives: `string`, `number`, `bigint`, `boolean`, `null`, or `undefined` -- Objects, arrays, and functions are **not allowed** and will throw at runtime via `paramsToId` +### `useDataContext` -```tsx -// โœ… Valid params -const validParams = { userId: "123", count: 42, active: true } +Hook to retrieve a `Context` instance by name. -// โŒ Invalid params (will throw) -const invalidParams = { user: { id: 123 } } // Objects not allowed -const invalidParams2 = { callback: () => {} } // Functions not allowed +```typescript +function useDataContext(name: string): Context ``` -This constraint ensures that context names remain deterministic and stable across renders, preventing accidental context duplication or collision. - ---- +### `useDataSource` / `useDataSourceMultiple` -## Usage Patterns +Hooks to publish values from a component into a context. -### Basic Context Wiring +```typescript +useDataSource(ctx, 'key', value); +useDataSourceMultiple(ctx, ['key1', value1], ['key2', value2]); +``` -```tsx -interface AppState { - user?: User - theme: "light" | "dark" -} +### `useDataSubscribe` / `useDataSubscribeMultiple` -function AppRoot() { - const ctx = useDataContext("app-state") - useDataSourceMultiple(ctx, - ["user", useCurrentUser()], - ["theme", useTheme()], - ) - return -} +Hooks to manually subscribe to specific context keys. -function NavBar() { - const ctx = useDataContext("app-state") - const { user, theme } = useDataSubscribeMultiple(ctx, "user", "theme") - return
Welcome {user?.name}
-} +```typescript +const value = useDataSubscribe(ctx, 'key'); +const { key1, key2 } = useDataSubscribeMultiple(ctx, 'key1', 'key2'); ``` -### Headless Root + Auto Context +--- -```tsx -function useCartState({ cartId }: { cartId: string }) { - const [items, setItems] = useState([]) - const total = useMemo(() => items.reduce((sum, item) => sum + item.price * item.qty, 0), [items]) - return { items, total, setItems } -} +## ๐Ÿงฉ Types -const cartRoot = createRootCtx("cart", useCartState) -const { useCtxState: useCartCtx } = createAutoCtx(cartRoot, 250) +### `ParamsToIdRecord` -function CartPanel({ cartId }: { cartId: string }) { - const ctx = useCartCtx({ cartId }) - const { items, total } = useQuickSubscribe(ctx) - return ( - - ) -} +Constraint for store parameters. All params must be primitive values to ensure deterministic IDs. -function Shell() { - return ( - <> - - - - ) -} +```typescript +type ParamsToIdRecord = Record< + string, + string | number | bigint | boolean | null | undefined +>; ``` - ---- - -## Live Examples - -Explore full working demos (counter, todo list, form, timer, cart) at the **[Live Demo](https://vothanhdat.github.io/react-state-custom/)**. diff --git a/README.md b/README.md index 2f5facb..c2325eb 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # React State Custom -**Simple. Powerful. TypeScript-first.** +**The "It's Just a Hook" State Manager for React.** -Turn any React hook into global state. No boilerplate, no complexityโ€”just pure, performant state management. +Turn any React hook into a global store. Zero boilerplate. Full type safety. Automatic lifecycle management. [![Demo](https://img.shields.io/badge/Demo-Live-blue?style=flat-square)](https://vothanhdat.github.io/react-state-custom/) [![npm version](https://img.shields.io/npm/v/react-state-custom?style=flat-square)](https://www.npmjs.com/package/react-state-custom) @@ -14,522 +14,154 @@ npm install react-state-custom ๐ŸŽฎ **[Try the Live Demo โ†’](https://vothanhdat.github.io/react-state-custom/)** -## Quick Start (2 minutes) +--- -If you already know how to write a component with `useState`, you're moments away from sharing that state everywhere. +## โšก The 30-Second Pitch -1. **Write a plain hook** โ€“ encapuslate data fetching, derived values, and actions inside a normal React hook. -2. **Create a store** โ€“ `createStore('feature', useFeatureState)` creates a shared store and returns a `useStore` hook. -3. **Mount `` once** โ€“ drop it near the top of your tree (wrap it with your own `ErrorBoundary` if desired). -4. **Consume anywhere** โ€“ call the generated `useStore` hook to access data and actions. +Stop writing reducers, actions, and manual providers. If you can write a React hook, you've already written your store. ```tsx -import { useState, useMemo } from 'react' -import { createStore, AutoRootCtx } from 'react-state-custom' - -const useFeatureState = ({ featureId }: { featureId: string }) => { - const [value, setValue] = useState(0) - const double = useMemo(() => value * 2, [value]) - return { value, double, increment: () => setValue(v => v + 1) } -} - -export const { useStore: useFeatureStore } = createStore('feature', useFeatureState) - -function AppShell() { - return ( - <> - - - - ) -} - -function FeatureMeter({ featureId }: { featureId: string }) { - const { value, double, increment } = useFeatureStore({ featureId }) - return ( -
- {value} - {double} - -
- ) +// 1. Write a standard hook (your store logic) +const useCountState = ({ initial = 0 }) => { + const [count, setCount] = useState(initial) + const increment = () => setCount(c => c + 1) + return { count, increment } } -``` - -Thatโ€™s the entire workflowโ€”no reducers, actions, or provider trees. - -## Why React State Custom? - -**Zero Boilerplate** โ€ข **Type-Safe** โ€ข **Selective Re-renders** โ€ข **Hook-Based** โ€ข **~10KB Bundle** - -React State Custom lets you write state management code that feels naturalโ€”because it **is** just React hooks. Use the same hooks you already know (`useState`, `useEffect`, etc.) to create powerful, shared global state without learning new paradigms. - -### When `useState` + `useEffect` Fall Short - -Even though React hooks are flexible, they start to hurt once state crosses component boundaries: -- **Prop drilling & manual providers** โ€“ every time state needs to be shared, you create a context, memoize values, and remember to wrap trees. -- **Coarse-grained re-renders** โ€“ updating one property forces every subscriber of that context to render, even if they don't consume the changed field. -- **Lifecycle bookkeeping** โ€“ you manually manage instance lifetimes, clean up effects, and guard against components mounting before providers. -- **Zero visibility** โ€“ there's no built-in way to inspect shared state, throttle noisy updates, or keep debugging breadcrumbs. +// 2. Create a store +export const { useStore } = createStore('counter', useCountState) -React State Custom keeps your favorite hooks but layers on automatic context lifecycles, selective subscriptions, and built-in tooling so you can stay productive as your app grows. - -## โšก Quick Example - -### Without React State Custom (manual context plumbing) - -```typescript -const CounterContext = createContext<{ - count: number; - increment: () => void; - decrement: () => void; -} | null>(null); - -function CounterProvider({ children }: { children: React.ReactNode }) { - const [count, setCount] = useState(0); - const value = useMemo( - () => ({ - count, - increment: () => setCount(c => c + 1), - decrement: () => setCount(c => c - 1), - }), - [count] - ); - - return {children}; -} - -function useCounter() { - const ctx = useContext(CounterContext); - if (!ctx) throw new Error('CounterProvider missing'); - return ctx; -} -``` - -Every consumer re-renders whenever anything in `value` changes, you have to remember to wrap parts of the tree with `CounterProvider`, and tearing this pattern down for parameterized instances gets messy fast. - -### With React State Custom (hook-first store) - -### With React State Custom (hook-first store) - -```typescript -import { useState } from 'react'; -import { createStore, AutoRootCtx } from 'react-state-custom'; - -// 1. Write your state logic using familiar React hooks -function useCounterState() { - const [count, setCount] = useState(0); - const increment = () => setCount(c => c + 1); - const decrement = () => setCount(c => c - 1); - - return { count, increment, decrement }; -} - -// 2. Create shared store (one line!) -const { useStore } = createStore('counter', useCounterState); - -// 3. Add AutoRootCtx to your app root (mount it once near the top of your tree) +// 3. Setup (mount once at root) & Use anywhere function App() { return ( <> - + {/* ๐Ÿ‘ˆ The magic that manages your stores */} - ); + ) } -// 4. Use anywhere in your app function Counter() { - const { count, increment, decrement } = useStore({}); - - return ( -
-

{count}

- - -
- ); + const { count, increment } = useStore({ initial: 10 }) + return } ``` -> โ„น๏ธ `AutoRootCtx` accepts optional `Wrapper` and `debugging` props. Pass an ErrorBoundary-like component through `Wrapper` to isolate failures, or set `debugging` to `true` to render raw state snapshots in the DOM (handy alongside React DevTools when tracking updates). - -`useStore` keeps `Counter` focused on `count`, so even if this context grows with more fields later, the component only re-renders when `count` changes. +**That's it.** No `Provider` wrapping per store. No complex setup. Just hooks. -**That's it!** No reducers, no actions, no providers to wrapโ€”just hooks. +--- -## Core Concepts in Plain English +## ๐Ÿš€ Why React State Custom? -- **Contexts on demand** โ€“ `Context` extends `EventTarget`, so every state update is just an event dispatch. `getContext` memoizes instances per name and `useDataContext` automatically bumps a counter so unused contexts self-evict shortly after the last consumer unmounts. -- **Publishers** โ€“ `useDataSource` and `useDataSourceMultiple` publish inside effects to keep renders pure. A registry guards against duplicate publishers fighting over the same key so you get actionable errors instead of stale data. -- **Subscribers** โ€“ `useDataSubscribe*` hooks cover single, multiple, debounced, and transformed reads. `useQuickSubscribe` proxies the backing data object so each component subscribes only to the properties it touches. -- **Root factories** โ€“ `createRootCtx` runs your headless hook exactly once per parameter set, publishes every returned key, and throws if two roots try to mount with the same resolved name. Your hook receives `(props, preState)` so it can rehydrate from the last published values when a root remounts. Parameters are serialized via `paramsToId`, so stick to primitive props (string/number/boolean/bigint/null/undefined) to keep IDs deterministic. -- **Composable Stores** โ€“ Because stores are just hooks, you can subscribe to one store *inside* the logic of another. This enables powerful reactive chains where a derived store automatically updates whenever its upstream dependencies change. -- **Auto orchestration** โ€“ Mount `` once and wire each root through `createAutoCtx`. The auto root listens for subscription requests, mounts/destroys the corresponding root on demand, and optionally keeps them alive for a configurable `timeToClean` window to smooth thrashing. -- **Dev tooling** โ€“ `DevToolContainer` watches the memoized context cache, flashes updates in place, and lets you plug in custom renderers so you can diff state right beside your UI. +Most state libraries force you to learn a new way to write logic (reducers, atoms, proxies). **React State Custom** lets you use the React skills you already have. -## Core Building Blocks (copy & paste ready) +### ๐Ÿ’Ž Zero Boilerplate +Define state with `useState`, `useEffect`, `useMemo`. No new syntax to learn. -Familiarity beats theory, so here are the primitives youโ€™ll reach for most often: +### ๐ŸŽฏ Selective Re-renders +Components only re-render when the specific data they use changes. Performance is built-in. -### 1. Context โ€“ event-driven store -```typescript -const ctx = useDataContext('my-state'); -``` +### ๐Ÿ”„ Automatic Lifecycle +Stores are created when needed and destroyed when unused. No more manual cleanup or memory leaks. -### 2. Data source โ€“ publish values -```typescript -useDataSource(ctx, 'count', count); -``` +### ๐Ÿ›ก๏ธ TypeScript First +Full type inference out of the box. Your IDE knows exactly what's in your store. -### 3. Subscribers โ€“ pick exact fields -```typescript -const count = useDataSubscribe(ctx, 'count'); -const { count, name } = useDataSubscribeMultiple(ctx, 'count', 'name'); -``` +--- -### 4. Root context โ€“ run your hook once -```typescript -const { Root, useCtxState } = createRootCtx('my-state', useMyState); -``` +## ๐Ÿ› ๏ธ Quick Start -### 5. Auto context โ€“ mount roots for you -```typescript -const { useCtxState } = createAutoCtx(rootContext); -``` +### 1. Define Your State +Write a hook that returns the data and actions you want to share. -### 6. Store factory โ€“ all in one ```typescript -const { useStore } = createStore('my-state', useMyState); -``` - -### 6. Store factory โ€“ all in one -```typescript -const { useStore } = createStore('my-state', useMyState); -``` +// features/userState.ts +import { useState, useEffect } from 'react' -## ๐ŸŽฏ Key Features - -### 1. **Just React Hooks** -Use `useState`, `useEffect`, `useMemo`, and any other React hooks you already know. No new concepts to learn. - -```typescript -function useUserState({ userId }: { userId: string }) { - const [user, setUser] = useState(null); - const [loading, setLoading] = useState(true); +export const useUserState = ({ userId }: { userId: string }) => { + const [user, setUser] = useState(null) useEffect(() => { - fetchUser(userId).then(setUser).finally(() => setLoading(false)); - }, [userId]); - - return { user, loading }; + fetchUser(userId).then(setUser) + }, [userId]) + + return { user, isLoading: !user } } ``` -### 2. **Selective Re-renders** -Components only re-render when the **specific data they subscribe to** changesโ€”not when anything in the state changes. +### 2. Create the Store +Use `createStore` to generate a hook for your components. ```typescript -// Only re-renders when 'user' changes, not when 'loading' changes -const { user } = useDataSubscribeMultiple(ctx, 'user'); +import { createStore } from 'react-state-custom' +import { useUserState } from './features/userState' -// Or subscribe to multiple fields -const { user, loading } = useDataSubscribeMultiple(ctx, 'user', 'loading'); +export const { useStore: useUserStore } = createStore('user', useUserState) ``` -> โš ๏ธ `useQuickSubscribe` proxies are only readable during render. Destructure the properties you need immediately and avoid storing the proxy in refs, effects, or callbacks. - -### 3. **Automatic Context Management** -With `AutoRootCtx`, state contexts are automatically created and destroyed as needed. Mount it once near your application root, optionally providing a `Wrapper` (for error boundaries) or enabling `debugging` to render live state snapshots in the DOMโ€”useful context when pairing with React DevTools. No manual provider management required. - -### 4. **TypeScript First** -Full type inference and type safety throughout. Your IDE knows exactly what's in your state. - -### 5. **Tiny Bundle Size** -~10KB gzipped. No dependencies except React. - -## ๐Ÿ†š Comparison with Hooks, Redux & Zustand - -| Feature | React State Custom | Plain Hooks (Context) | Redux | Zustand | -|---------|-------------------|-----------------------|-------|---------| -| **Bundle Size** | ~10KB | 0KB (just React) | ~50KB (with toolkit) | ~1KB | -| **Learning Curve** | โœ… Minimal (just hooks) | โš ๏ธ Familiar APIs, but patterns are DIY | โŒ High (actions, reducers, middleware) | โœ… Low | -| **Boilerplate** | โœ… None | โŒ Manual providers + prop drilling | โŒ Heavy | โœ… Minimal | -| **Type Safety** | โœ… Full inference | โš ๏ธ Custom per-context typing | โš ๏ธ Requires setup | โœ… Good | -| **Selective Re-renders** | โœ… Built-in | โŒ Context update = every consumer renders | โš ๏ธ Requires selectors | โœ… Built-in | -| **DevTools** | โœ… Built-in UI | โŒ None | โœ… Redux DevTools | โœ… DevTools support | -| **Async Support** | โœ… Native (hooks) | โœ… Native (hooks) | โš ๏ธ Requires middleware | โœ… Native | -| **Context Composition** | โœ… Automatic | โŒ Manual provider trees | โŒ Manual | โš ๏ธ Manual store combination | - -### When to Use React State Custom - -โœ… **Choose React State Custom if you:** -- Want to use React hooks for state management without learning new patterns -- Need fine-grained control over component re-renders -- Prefer minimal boilerplate and configuration -- Want automatic context lifecycle management -- Need multiple independent state contexts that don't interfere - -โŒ **Consider Redux if you:** -- Need powerful time-travel debugging (Redux DevTools) -- Have a very large team that benefits from strict architectural patterns -- Already have significant Redux investment - -โŒ **Consider Zustand if you:** -- Want the absolute smallest bundle size -- Need a simple global store without context isolation -- Don't need automatic context lifecycle management - -## ๐Ÿ”ฅ Real-World Example: User Authentication - -```typescript -// authState.ts -function useAuthState() { - const [user, setUser] = useState(null); - const [loading, setLoading] = useState(true); - - useEffect(() => { - // Check authentication on mount - checkAuth().then(setUser).finally(() => setLoading(false)); - }, []); - - const login = async (email: string, password: string) => { - setLoading(true); - try { - const user = await authService.login(email, password); - setUser(user); - } finally { - setLoading(false); - } - }; - - const logout = async () => { - await authService.logout(); - setUser(null); - }; - - return { user, loading, login, logout }; -} - -export const { useStore: useAuthStore } = createStore('auth', useAuthState); +### 3. Mount the Root (Once) +Add `` to your app's root. This component manages all your stores automatically. +```tsx // App.tsx -function App() { - return ( - <> - - -
- - - - ); -} +import { AutoRootCtx } from 'react-state-custom' -// Header.tsx - Only re-renders when user changes -function Header() { - const { user, logout } = useAuthStore({}); - - return ( -
- {user ? ( - <> - Welcome, {user.name} - - - ) : ( - Login - )} -
- ); -} - -// ProtectedRoute.tsx - Only re-renders when loading or user changes -function ProtectedRoute({ children }) { - const ctx = useAuthStore.useCtxState({}); - const { user, loading } = useDataSubscribeMultiple(ctx, 'user', 'loading'); - - if (loading) return ; - if (!user) return ; - - return children; -} -``` - -**Compare with Redux:** -```typescript -// Redux requires: action types, action creators, reducers, thunks/sagas -// React State Custom: just write a hook! โœจ -``` - -## ๐Ÿš€ Advanced Features - -Once you have a store running, layer in these power-ups as needed. - -### Developer Tools -Visual debugging component to inspect all your context data in real-time: - -```typescript -import { DevToolContainer } from 'react-state-custom'; -import 'react-state-custom/dist/react-state-custom.css'; - -function App() { +export default function App() { return ( <> - - ); + ) } ``` -The toggle reveals a bottom-docked inspector that now uses resizable split panes powered by `@uiw/react-split`. Drag the gutter to adjust how much space the context list or detail view occupies while keeping your application visible above. - -**Custom data viewer with rich object visualization:** -```typescript -import { DataViewComponent } from 'react-state-custom'; -import { ObjectView } from 'react-obj-view'; -import 'react-obj-view/dist/react-obj-view.css'; // Required for ObjectView styling - -const CustomDataView: DataViewComponent = ({ name, value }) => { - return ; -}; - - -``` - -Pass `children` to `DevToolContainer` to customize the floating toggle button label (for example `State Inspector`), and import `react-state-custom/dist/react-state-custom.css` once to pick up the overlay styles. - -### Parameterized Contexts -Create multiple instances of the same state with different parameters: - -```typescript -function useUserState({ userId }: { userId: string }) { - // State logic here -} - -const { useStore: useUserStore } = createStore('user', useUserState); +--- -// Different instances for different users -function UserProfile({ userId }) { - const { user } = useUserStore({ userId }); // Automatic instance per userId - return
{user?.name}
; -} -``` +## ๐Ÿ†š Comparison -> Need to avoid rapid mount/unmount churn? Pass a second argument to `createStore` (for example `createStore('user', useUserState, 200)`) to keep instances alive for a few extra milliseconds before disposal. +| Feature | React State Custom | Redux | Context API | Zustand | +|:---|:---:|:---:|:---:|:---:| +| **Paradigm** | Just Hooks ๐Ÿช | Actions/Reducers | Providers | Store Object | +| **Boilerplate** | ๐ŸŸข None | ๐Ÿ”ด High | ๐ŸŸก Medium | ๐ŸŸข Low | +| **Auto Lifecycle** | โœ… Yes | โŒ No | โŒ No | โŒ No | +| **Selective Renders** | โœ… Automatic | โš ๏ธ Selectors | โŒ Manual | โœ… Selectors | +| **Learning Curve** | ๐ŸŸข Low | ๐Ÿ”ด High | ๐ŸŸก Medium | ๐ŸŸข Low | -> โš ๏ธ The props you pass to `createStore`/`useStore` must be composed of primitive values (string, number, boolean, bigint, null, or undefined). Objects are rejected so context names stay deterministicโ€”pass IDs instead of raw objects. +--- -### Debounced Subscriptions -Optimize performance for frequently changing values: +## ๐Ÿงฉ Advanced Features -```typescript -// Re-render at most once per 300ms -const searchQuery = useDataSubscribe(ctx, 'searchQuery', 300); -``` +### ๐Ÿ”Œ Developer Tools +Inspect your state in real-time with the built-in DevTools. -### Transformed Subscriptions -Transform data before using it: +```tsx +import { DevToolContainer } from 'react-state-custom' +import 'react-state-custom/dist/react-state-custom.css' -```typescript -const userStats = useDataSubscribeWithTransform( - ctx, - 'user', - (user) => ({ - fullName: `${user?.firstName} ${user?.lastName}`, - isAdmin: user?.role === 'admin' - }) -); + ``` -### Composing Stores (Derived State) -Since stores are just hooks, you can subscribe to one store *inside* another. This allows you to build reactive dependency chains where a downstream store automatically updates when an upstream store changes. +### ๐Ÿ†” Parameterized Stores +Create multiple independent instances of the same store by passing different parameters. -```typescript -// 1. Upstream Store -const { useStore: useUserStore } = createStore('user', () => { - const [role, setRole] = useState('guest'); - return { role, setRole }; -}); - -// 2. Downstream Store (depends on User) -const useDashboardStore = () => { - // Subscribe to the upstream store - const { role } = useUserStore({}); - - // Derive state based on the upstream value - const permissions = useMemo(() => { - return role === 'admin' ? ['read', 'write', 'delete'] : ['read']; - }, [role]); - - return { permissions }; -}; - -const { useStore: useDashboardStore } = createStore('dashboard', useDashboardStore); +```tsx +// Creates a unique store for each ID +const { count } = useStore({ id: 'counter-1' }) +const { count } = useStore({ id: 'counter-2' }) ``` -## ๐ŸŽฎ Live Examples - -Explore interactive examples in the **[Live Demo](https://vothanhdat.github.io/react-state-custom/)**: - -- **Counter** - Basic state management with increment, decrement, and reset -- **Todo List** - Multiple independent lists with scoped contexts -- **Form Validation** - Real-time validation with error handling -- **Timer** - Side effects and cleanup with millisecond precision -- **Shopping Cart** - Complex state with derived values (total, itemCount) - -Each example includes live code editing with syntax highlighting, powered by Sandpack! - -## ๐Ÿ“– Documentation - -For complete API documentation, examples, and advanced patterns, see: -- **[API_DOCUMENTATION.md](./API_DOCUMENTATION.md)** - Complete API reference -- **[AI_CONTEXT.md](./AI_CONTEXT.md)** - Context for AI assistants (Gemini, ChatGPT, etc.) -- **[Live Demo](https://vothanhdat.github.io/react-state-custom/)** - Interactive examples +### โšก๏ธ Derived State +Compose stores just like hooks. -## ๐Ÿ› ๏ธ Development - -```bash -# Install dependencies -yarn install - -# Run development UI with example selector -yarn dev - -# Run interactive playground with live code editing -yarn dev:playground - -# Build library -yarn build - -# Build demo site -yarn build:demo - -# Preview demo locally -yarn preview +```tsx +const useCartTotal = () => { + const { items } = useCartStore({}) + return items.reduce((total, item) => total + item.price, 0) +} ``` -### Development Modes - -**`yarn dev`** - Starts a clean development UI with an interactive example selector. Great for: -- Testing all examples in one place -- Quick switching between different examples -- Visual debugging with DevTool component - -**`yarn dev:playground`** - Starts the Sandpack-powered playground with live code editing. Perfect for: -- Creating interactive demos -- Live code editing and experimentation -- Sharing editable examples - -## ๐ŸŽ“ Learning Path - -1. **Follow the Quick Start** โ€“ build one shared store end-to-end. -2. **Layer on subscriptions** โ€“ swap `useQuickSubscribe` for the more specific `useDataSubscribe*` hooks where it makes sense. -3. **Optimize when needed** โ€“ introduce debounced/transform subscriptions and `createAutoCtx` grace periods to smooth noisy stores. -4. **Scale up** โ€“ add parameterized contexts (one store per ID) and wire the DevTool overlay for visibility. +--- ## ๐Ÿ“ฆ Installation @@ -537,18 +169,13 @@ yarn preview npm install react-state-custom # or yarn add react-state-custom -# or -pnpm add react-state-custom ``` -## ๐Ÿค Contributing +## ๐Ÿ“– Documentation -Contributions are welcome! Please feel free to submit a Pull Request. +- **[API Reference](./API_DOCUMENTATION.md)** - Full API documentation. +- **[Live Demo](https://vothanhdat.github.io/react-state-custom/)** - Interactive examples. ## ๐Ÿ“„ License -MIT License - feel free to use in any project. - ---- - -**Made with โค๏ธ for developers who love React hooks** +MIT ยฉ Vo Thanh Dat