# ⚙ react-automat > Zero-dependency higher-order state container for React `PureComponent`. State lives in module scope (independent of mounting). Components read state synchronously in `constructor` and subscribe in `componentDidMount`. No Context, hooks, or HOCs. [![Live Interactive Demo](https://img.shields.io/badge/Live%20Demo-sebgreen.net%2Fautomat-6366f1?style=flat-square)](https://sebgreen.net/automat/) [![License: 0BSD](https://img.shields.io/badge/License-0BSD-blue.svg?style=flat-square)](LICENSE) ```bash npm install react-automat ``` --- ## 1. Complete Type Signatures ```ts import type { PureComponent } from 'react'; export interface AutomatOptions { name?: string | null; // window.__AUTOMATS__ key & Automat.get(name) persist?: boolean; // false = window (default), true = IndexedDB url?: string | null; // Auto-triggers fetcher(url) on init fetcher?: (url: string, automat?: Automat) => Promise; } export type Selector = keyof T | (keyof T)[] | ((state: T) => S | null | undefined); export interface ScopedSlice { readonly state: S; subscribe(target: PureComponent | ((slice: S) => void)): () => void; } export interface BlobResource { blob: Blob; url: string; size: number; type: string; loadedAt: string; revoke: () => void; // Auto-called on setDirty() / dispose() to release memory } export interface SSEOptions { onMessage?: (data: any, event: string, automat: Automat) => void; events?: Record) => void>; onError?: (err: any, automat: Automat) => void; } export class Automat> { constructor(initialState?: T | null, actions?: A, options?: AutomatOptions); readonly state: T; getState(): T; setState(partial: Partial | ((prev: T) => Partial)): T; readonly actions: A; readonly isReady: boolean; readonly isDirty: boolean; readonly ready: Promise; // Re-awaitable after setDirty() readonly name: string | null; readonly persist: boolean; readonly url: string | null; readonly subscriberCount: number; subscribe(target: PureComponent | ((state: T, partial?: Partial) => void), selector?: Selector): () => void; select(selector: Selector): ScopedSlice; unsubscribe(target: any): void; subscribeTo( upstream: Automat, transform: (upstreamState: U, currentState: T) => Partial | null | undefined, options?: { cascadeDirty?: boolean | ((upstream: U, current: T) => boolean) } ): this; invalidateWith(upstream: Automat, filterFn?: (upstream: U, current: T) => boolean): this; onDirty(fn: (automat: this) => void): () => void; setDirty(): Promise; // Resets state to null/default, revokes blobs, cascades, defers if unmounted reload(): Promise; // Silent background re-fetch; preserves state & isReady=true refresh(): Promise; // Alias for reload() read(): T; // For Suspense: throws ready promise if unready; triggers deferred load if dirty clearPersistence(): Promise; dispose(): void; // Unsubs all, revokes blobs, deletes from window registry static get(name: string): Automat | undefined; } // Fetchers & SSE export function fetchJson(url: string, options?: RequestInit): Promise; export function fetchBlob(url: string, options?: RequestInit): Promise; export const fetchImageBlob: typeof fetchBlob; export const fetchPdfBlob: typeof fetchBlob; export function connectSSE(automat: Automat, url: string, options?: SSEOptions): { eventSource: EventSource | null; close: () => void }; export function createSSEFetcher(sseUrl: string, baseFetcher?: Function, sseOptions?: SSEOptions): (url: string, automat?: any) => Promise; ``` ## 2. Canonical Usage Patterns ### A. PureComponent Subscription ```jsx import { PureComponent } from 'react'; import { Automat } from 'react-automat'; const store = new Automat({ count: 0 }, { inc: () => store.setState({ count: store.state.count + 1 }) }); class Counter extends PureComponent { constructor(props) { super(props); this.state = store.state; } // Sync read, never stale componentDidMount() { this.unsub = store.subscribe(this); } // Auto-calls this.setState(partial) componentWillUnmount() { this.unsub(); } render() { return ; } } ``` ### B. Fine-Grained Selectors (Shallow-guarded, 0 renders if slice unchanged) ```js store.subscribe(this, 'count'); // Single key store.subscribe(this, ['theme', 'locale']); // Multi key store.subscribe(this, (s) => ({ items: s.list.length }));// Projection function ``` ### C. Unconnected Component via Window (Zero imports of store file) ```js const store = Automat.get('cart'); // or window.__AUTOMATS__?.get('cart') this.state = { count: store?.state?.items?.length || 0 }; this.unsub = store?.subscribe(this, (s) => ({ count: s.items.length })); ``` ### D. Backend API: React Suspense vs Standard ```jsx const user = new Automat(null, {}, { url: '/api/user' }); // Suspense: throws promise while unready const Profile = () =>
{user.read().name}
; // Standard: check isReady class ProfileStandard extends PureComponent { constructor(props) { super(props); this.state = { u: user.state }; } componentDidMount() { this.unsub = user.subscribe(this, (u) => ({ u })); } componentWillUnmount() { this.unsub(); } render() { return !user.isReady ? :
{this.state.u.name}
; } } ``` ### E. Cascading Invalidation, Blobs & Memory Freeing ```js const all = new Automat(null, {}, { name: 'photos/all', url: '/api/photos' }); const p1 = new Automat(null, {}, { name: 'photo/1', url: '/api/photos/1', fetcher: fetchImageBlob }); p1.invalidateWith(all); // p1 cascades setDirty() when all.setDirty() is called all.setDirty(); // p1 auto-revokes held Blob URL, resets state=null, defers reload if unmounted ``` ### F. SSE Remote Trigger ```js const live = new Automat({}, { onSSETrigger(data, event, store) { if (event === 'invalidate') store.setDirty(); if (event === 'reload') store.reload(); } }); connectSSE(live, '/api/stream'); ``` ## 3. Decision Matrix: `setDirty()` vs `reload()` | Method | `isReady` | State during fetch | Suspense | Unmounted | Use Case | |---|---|---|---|---|---| | `setDirty()` | `false` | Resets to `null`/init | Throws fallback | Deferred until mount/read | Cache bust, logout, hard reset | | `reload()` | `true` | Preserved | Never suspends | Immediate fetch | Polling, pull-to-refresh, sync | ## 4. Invariant Rules - Read `store.state` synchronously in `constructor(props)` — never in render bodies. - Subscribe in `componentDidMount()` & store the returned `unsub`; unsubscribe in `componentWillUnmount()`. - Never mutate `store.state` directly (`store.state.x = 1` ❌); use `store.setState({ x })` or actions. - Pass selectors to `subscribe(this, selector)` on multi-key stores to skip redundant renders. - Never wrap components in Context Providers, HOCs, or custom hooks.