+ );
+ }
+}
+```
+
+### E. Backend API: React Suspense vs Standard
```jsx
const user = new Automat(null, {}, { url: '/api/user' });
@@ -137,64 +266,51 @@ 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}
; }
+ render() {
+ if (!user.isReady) return
Loading...
;
+ return
{this.state.u.name}
;
+ }
}
```
-### E. Cascading Invalidation, Blobs & Memory Freeing
+### F. Cascading Invalidation & Memory Freeing with Blobs
```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
+const photoHub = new Automat(null, {}, { url: '/api/photos' });
+const photo1 = new Automat(null, {}, { url: '/api/photos/1', fetcher: fetchImageBlob });
-all.setDirty(); // p1 auto-revokes held Blob URL, resets state=null, defers reload if unmounted
+// Automatically cascades setDirty when hub is dirtied
+photo1.invalidateWith(photoHub);
+
+// When photoHub.setDirty() is triggered:
+// 1. photo1 calls URL.revokeObjectURL(photo1.state.url) to free browser image memory
+// 2. photo1 resets to null/initial state
+// 3. If photo1 is mounted, re-fetches immediately; if unmounted, defers fetch!
```
-### 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');
-```
+---
-### G. Combine Automats
-```js
-const app = Automat.combine({
- auth: authAutomat,
- company: companyAutomat,
-});
+## 3. Core Invariants
-app.state.auth; // authAutomat.state
-app.state.company; // companyAutomat.state
-app.actions.auth.login(); // authAutomat.actions.login()
-app.actions.company.rename('Acme');
+1. **Lifecycle-Independent**: `Automat` instances live outside the React tree (typically in module scope), persisting state across component mounts and unmounts.
+2. **Synchronous Constructor Reads**: Components initialize with `this.state = myAutomat.state;` directly in `constructor(props)`.
+3. **Automatic Shallow Merges**: When subscribed with `subscribe(this)`, `automat.setState(partial)` calls `component.setState(partial)`, preserving any component-local state.
+4. **Fine-Grained Selectors**: Passing a selector to `subscribe(this, selector)` runs shallow equality checks; updates to unrelated fields skip `setState` and avoid re-renders.
+5. **Zero Wrappers**: No hooks, HOCs, Context Providers, or `connect()`.
+6. **Optional Persistence**: Automats can specify a `name` and persist in the `window` object (`persist: false`) for session memory across unmounts/HMR, or in `IndexedDB` (`persist: true`) to survive full page reloads.
-// Notified when either child changes; selectors can isolate a branch.
-const unsubscribe = app.subscribe(this, (state) => ({
- user: state.auth.user,
- companyName: state.company.name,
-}));
-```
+---
-The aggregate is derived: child updates automatically update the matching key. Its
-`isReady`, `isDirty`, `ready`, `read()`, `setDirty()`, and `reload()` operations cover
-all children. Calling `app.dispose()` only removes aggregate subscriptions; it does
-not dispose the child automats.
+## Best Practices
-## 3. Decision Matrix: `setDirty()` vs `reload()`
+### β DO
+- Define `Automat` instances in module scope.
+- Read `automat.state` or `automat.getData()` directly in `constructor(props)`.
+- Subscribe in `componentDidMount()` and unsubscribe in `componentWillUnmount()`.
+- Use selectors (`'key'`, `['keys']`, or function) on multi-field automats to avoid unnecessary re-renders.
+- Return `null` in `subscribeTo` transforms when an update should be skipped.
-| 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.
+### β DON'T
+- Do not instantiate `Automat` inside React component lifecycle or render methods.
+- Do not mutate state directly (`automat.state.count = 1`); use `setState()` or actions.
+- Do not wrap components in React Context providers or HOCs.
+- Do not forget to unsubscribe in `componentWillUnmount()`.
diff --git a/src/App.jsx b/src/App.jsx
index 63843c0..86dc36e 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -10,6 +10,10 @@ import DynamicAutomatSubscriber from './examples/components/DynamicAutomatSubscr
import CartManager from './examples/components/CartManager.jsx';
import CartBadgeDisplay from './examples/components/CartBadgeDisplay.jsx';
import UnconnectedPartialSubscriber from './examples/components/UnconnectedPartialSubscriber.jsx';
+import AsyncApiPrimaryView from './examples/components/AsyncApiPrimaryView.jsx';
+import AsyncApiCollapsibleView from './examples/components/AsyncApiCollapsibleView.jsx';
+import MultiApiIndividualControls from './examples/components/MultiApiIndividualControls.jsx';
+import MultiApiCombinedDashboard from './examples/components/MultiApiCombinedDashboard.jsx';
import ApiBoundExample from './examples/components/ApiBoundExample.jsx';
import CascadingPhotosExample from './examples/components/CascadingPhotosExample.jsx';
import CombinedAutomatExample from './examples/components/CombinedAutomatExample.jsx';
@@ -26,31 +30,35 @@ class App extends PureComponent {
v0.1.0
- Higher-order state container for React{' '}
+ Higher-order observable state container for React{' '}
PureComponent
State lives independently of mounting β zero prop drilling Β· Optional Window and IndexedDB persistence.
-
- {/* ββ Example 1: Counter ββββββββββββββββββββββββββββββββββ */}
+ {/* ββ Notification Bar βββββββββββββββββββββββββββββββββββββ */}
+
+
+ {/* ββ Main Content βββββββββββββββββββββββββββββββββββββββββ */}
+
+ {/* ββ Example 1: Basic Counter ββββββββββββββββββββββββββββ */}
01
-
Counter
+
Multi-Subscriber Sync & Local State Isolation
- Two independent PureComponents
- synchronized by a single counterAutomat.
- Neither knows the other exists β they both subscribe to the same state container.
+ Two completely separate components subscribe to the same counterAutomat.
+ The controller button also maintains local UI state (lastPressed, animation classes).
+ Neither component passes props to the other.
@@ -63,85 +71,51 @@ class App extends PureComponent {
- {/* ββ Example 2: Cascade βββββββββββββββββββββββββββββββββ */}
+ {/* ββ Example 2: Cascading State ββββββββββββββββββββββββββ */}
02
-
Notification Cascade
+
Cascading Automats (Reactive Transformation)
- subscribeTo() connects two automats into a
- reactive pipeline. Clicking triggers in CascadeControls mutates{' '}
- counterAutomat (upstream). This automatically invokes the{' '}
- subscribeTo() transform callback, updating{' '}
- notificationAutomat (downstream), which then re-renders{' '}
- NotificationBar without any prop drilling or shared component parent.
+ notificationAutomat derives its state from{' '}
+ counterAutomat using subscribeTo.
+ Every state change cascades automatically without manual event dispatching.
- βΉοΈ How subscribeTo(upstreamAutomat, transform) Works
-
-
-
- upstreamState: The newly emitted state from the upstream automat (e.g. {`{ count: 1 }`}).
-
-
- myState: The current state of this downstream automat right before updating (e.g. {`{ messages: [...] }`}). Acts as an accumulator to prepend events and cap history.
-
-
- Return Value: The returned partial state object is automatically applied via this.setState(partial), notifying downstream UI subscribers.
-
-
- Conditional Filtering: Return null or undefined to selectively skip updates and avoid triggering renders.
-
-
+ notificationAutomat
-
-
-
-
+
- {/* ββ Example 3: API Auto-Sync βββββββββββββββββββββββββββ */}
+ {/* ββ Example 3: Backend Synchronization ββββββββββββββββββ */}
03
-
API-Backed Auto-Sync Counter (POST)
+
Backend Synchronization & Optimistic Updates
- syncCounterAutomat manages an indexed counter that automatically
- synchronizes its mutations to the backend via HTTP POST /api/counter.
- State updates are applied optimistically for instant UI response, followed by asynchronous
- background synchronization and status tracking without any component-level fetch or lifecycle glue.
+ State changes trigger asynchronous backend persistence via a custom fetch handler in the Automatβs actions.
+ A polling subscriber independently monitors the server-side state without polling the Automat itself.
-
- User Click
+
+ SyncCounterControlsβ
- Optimistic setState()
+ syncCounterAutomat
+ β
+ Backend APIβ
- POST /api/counter
- β
- Backend Database Updated
- β
- Sync Status 200 OK
+ SyncBackendMonitor
Dynamic Slot Allocation & Unmount-Safe State Recall
- The left side is a standard indexAutomat counter (like Example 1).
- On the right side, that counterβs value is used to dynamically lookup, instantiate, and resubscribe to
- a different Automat stored in window.automats = new Map().
- Stepping through indexes unhooks from the old Automat, subscribes to the new one, and instantly recalls its preserved state.
+ Demonstrates how state lives outside the React component tree.
+ Changing the active index resubscribes the display component to a different named Automat.
+ Previous slot state is never destroyed on unmount β return to any slot at any time to find its clicks and notes intact.
-
- Shared Index: N
+
+ IndexSelectorβ
- window.automats.get(N)
+ indexAutomat.state.indexβ
- Unsub Old / Sub New
+ Unsub Old Slot / Sub New SlotβRecall State for Slot #N
@@ -211,12 +184,12 @@ class App extends PureComponent {
1. CartBadgeDisplay subscribes only to list length (items.length) β item quantity and coupon edits cause zero re-renders.
- 2. UnconnectedPartialSubscriber has zero imports of the cart store, resolving it strictly via the named window object (Automat.get('cart')), and subscribes only to ['couponCode'].
+ 2. UnconnectedPartialSubscriber has zero imports of the cart store, resolving it strictly via the named window object (Automat.get('cart')), and subscribes only to ['couponCode'].
cartAutomat.setState()β
- window.__AUTOMATS__.get('cart')
+ window.__AUTOMATS__.get('cart')βshallowEqual(lastSlice, nextSlice)β
@@ -235,15 +208,86 @@ class App extends PureComponent {
- {/* ββ Example 6: Backend API Binding & Suspense βββββββ */}
+ {/* ββ Example 6: Async Fetch & Constructor Cache ββββββββ */}
06
-
Backend API Binding, React Suspense & Dirty Invalidation
+
Async API Fetch & Constructor Cache (Instant Re-Mount)
- An automat can bind directly to a backend URL (url: '/api/profile').
+ An automat fetching asynchronous data via a simulated delayed API. When the primary component mounts,
+ calling profileAsyncAutomat.getData() in its constructor returns{' '}
+ {`{ status: 'pending' }`}, rendering a loading state (1st render), then re-rendering
+ when the fetch finishes (2nd render). When a second component mounts (or when unmounted and remounted via the collapsible toggle),
+ the data is already loaded in memory β the constructor receives the resolved payload synchronously, rendering in{' '}
+ 1 render with zero 2nd render.
+
+
+ Mount 1 Constructor
+ β
+ status: 'pending' (Render #1)
+ β
+ API Resolves
+ β
+ Render #2 (Data)
+ |
+ Mount 2 (Collapsible)
+ β
+ Constructor Has Data (1 Render Only)
+
+ Three automats in an orchestrated graph: two with delayed APIs (userStatsAutomat at ~700ms and{' '}
+ systemMetricsAutomat at ~1300ms), and one combined automat created with{' '}
+ Automat.combine(). When accessed while upstreams are idle, it triggers both to load in parallel,
+ merging results when both finish. However, if both have already been accessed earlier, the combined automat resolves{' '}
+ instantly in the component constructor with zero delay and a single render.
+
+
+
+
+
+ {/* ββ Example 8: Backend API Binding & Suspense βββββββ */}
+
+
+
+ 08
+
Backend API Binding, React Suspense & Dirty Invalidation
+
+
+ An automat can bind directly to a backend URL (url: '/api/profile').
Supports await automat.ready, React Suspense via automat.read(),
and manual invalidation via automat.setDirty().
When dirty, state resets to default or null. If mounted, it reloads immediately. If all subscribers are unmounted,
@@ -265,12 +309,12 @@ class App extends PureComponent {
- Automat.combine() creates one derived application state from independent automats.
+ Automat.combine() with an object map creates one derived application state from independent automats.
Updates retain their namespace, actions remain grouped by child, and readiness operations cover every child.