atomirx 0.0.7 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +198 -2234
- package/bin/cli.js +90 -0
- package/dist/core/derived.d.ts +2 -2
- package/dist/core/effect.d.ts +3 -2
- package/dist/core/onCreateHook.d.ts +15 -2
- package/dist/core/onErrorHook.d.ts +4 -1
- package/dist/core/pool.d.ts +78 -0
- package/dist/core/pool.test.d.ts +1 -0
- package/dist/core/select-boolean.test.d.ts +1 -0
- package/dist/core/select-pool.test.d.ts +1 -0
- package/dist/core/select.d.ts +278 -86
- package/dist/core/types.d.ts +233 -1
- package/dist/core/withAbort.d.ts +95 -0
- package/dist/core/withReady.d.ts +3 -3
- package/dist/devtools/constants.d.ts +41 -0
- package/dist/devtools/index.cjs +1 -0
- package/dist/devtools/index.d.ts +29 -0
- package/dist/devtools/index.js +429 -0
- package/dist/devtools/registry.d.ts +98 -0
- package/dist/devtools/registry.test.d.ts +1 -0
- package/dist/devtools/setup.d.ts +61 -0
- package/dist/devtools/types.d.ts +311 -0
- package/dist/index-BZEnfIcB.cjs +1 -0
- package/dist/index-BbPZhsDl.js +1653 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +18 -14
- package/dist/onDispatchHook-C8yLzr-o.cjs +1 -0
- package/dist/onDispatchHook-SKbiIUaJ.js +5 -0
- package/dist/onErrorHook-BGGy3tqK.js +38 -0
- package/dist/onErrorHook-DHBASmYw.cjs +1 -0
- package/dist/react/index.cjs +1 -30
- package/dist/react/index.js +206 -791
- package/dist/react/onDispatchHook.d.ts +106 -0
- package/dist/react/useAction.d.ts +4 -1
- package/dist/react-devtools/DevToolsPanel.d.ts +93 -0
- package/dist/react-devtools/EntityDetails.d.ts +10 -0
- package/dist/react-devtools/EntityList.d.ts +15 -0
- package/dist/react-devtools/LogList.d.ts +12 -0
- package/dist/react-devtools/hooks.d.ts +50 -0
- package/dist/react-devtools/index.cjs +1 -0
- package/dist/react-devtools/index.d.ts +31 -0
- package/dist/react-devtools/index.js +1589 -0
- package/dist/react-devtools/styles.d.ts +148 -0
- package/package.json +26 -2
- package/skills/atomirx/SKILL.md +456 -0
- package/skills/atomirx/references/async-patterns.md +188 -0
- package/skills/atomirx/references/atom-patterns.md +238 -0
- package/skills/atomirx/references/deferred-loading.md +191 -0
- package/skills/atomirx/references/derived-patterns.md +428 -0
- package/skills/atomirx/references/effect-patterns.md +426 -0
- package/skills/atomirx/references/error-handling.md +140 -0
- package/skills/atomirx/references/hooks.md +322 -0
- package/skills/atomirx/references/pool-patterns.md +229 -0
- package/skills/atomirx/references/react-integration.md +411 -0
- package/skills/atomirx/references/rules.md +407 -0
- package/skills/atomirx/references/select-context.md +309 -0
- package/skills/atomirx/references/service-template.md +172 -0
- package/skills/atomirx/references/store-template.md +205 -0
- package/skills/atomirx/references/testing-patterns.md +431 -0
- package/coverage/base.css +0 -224
- package/coverage/block-navigation.js +0 -87
- package/coverage/clover.xml +0 -1440
- package/coverage/coverage-final.json +0 -14
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +0 -131
- package/coverage/prettify.css +0 -1
- package/coverage/prettify.js +0 -2
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +0 -210
- package/coverage/src/core/atom.ts.html +0 -889
- package/coverage/src/core/batch.ts.html +0 -223
- package/coverage/src/core/define.ts.html +0 -805
- package/coverage/src/core/emitter.ts.html +0 -919
- package/coverage/src/core/equality.ts.html +0 -631
- package/coverage/src/core/hook.ts.html +0 -460
- package/coverage/src/core/index.html +0 -281
- package/coverage/src/core/isAtom.ts.html +0 -100
- package/coverage/src/core/isPromiseLike.ts.html +0 -133
- package/coverage/src/core/onCreateHook.ts.html +0 -138
- package/coverage/src/core/scheduleNotifyHook.ts.html +0 -94
- package/coverage/src/core/types.ts.html +0 -523
- package/coverage/src/core/withUse.ts.html +0 -253
- package/coverage/src/index.html +0 -116
- package/coverage/src/index.ts.html +0 -106
- package/dist/index-CBVj1kSj.js +0 -1350
- package/dist/index-Cxk9v0um.cjs +0 -1
- package/scripts/publish.js +0 -198
- package/src/core/atom.test.ts +0 -633
- package/src/core/atom.ts +0 -311
- package/src/core/atomState.test.ts +0 -342
- package/src/core/atomState.ts +0 -256
- package/src/core/batch.test.ts +0 -257
- package/src/core/batch.ts +0 -172
- package/src/core/define.test.ts +0 -343
- package/src/core/define.ts +0 -243
- package/src/core/derived.test.ts +0 -1215
- package/src/core/derived.ts +0 -450
- package/src/core/effect.test.ts +0 -802
- package/src/core/effect.ts +0 -188
- package/src/core/emitter.test.ts +0 -364
- package/src/core/emitter.ts +0 -392
- package/src/core/equality.test.ts +0 -392
- package/src/core/equality.ts +0 -182
- package/src/core/getAtomState.ts +0 -69
- package/src/core/hook.test.ts +0 -227
- package/src/core/hook.ts +0 -177
- package/src/core/isAtom.ts +0 -27
- package/src/core/isPromiseLike.test.ts +0 -72
- package/src/core/isPromiseLike.ts +0 -16
- package/src/core/onCreateHook.ts +0 -107
- package/src/core/onErrorHook.test.ts +0 -350
- package/src/core/onErrorHook.ts +0 -52
- package/src/core/promiseCache.test.ts +0 -241
- package/src/core/promiseCache.ts +0 -284
- package/src/core/scheduleNotifyHook.ts +0 -53
- package/src/core/select.ts +0 -729
- package/src/core/selector.test.ts +0 -799
- package/src/core/types.ts +0 -389
- package/src/core/withReady.test.ts +0 -534
- package/src/core/withReady.ts +0 -191
- package/src/core/withUse.test.ts +0 -249
- package/src/core/withUse.ts +0 -56
- package/src/index.test.ts +0 -80
- package/src/index.ts +0 -65
- package/src/react/index.ts +0 -21
- package/src/react/rx.test.tsx +0 -571
- package/src/react/rx.tsx +0 -531
- package/src/react/strictModeTest.tsx +0 -71
- package/src/react/useAction.test.ts +0 -987
- package/src/react/useAction.ts +0 -607
- package/src/react/useSelector.test.ts +0 -182
- package/src/react/useSelector.ts +0 -292
- package/src/react/useStable.test.ts +0 -553
- package/src/react/useStable.ts +0 -288
- package/tsconfig.json +0 -9
- package/v2.md +0 -725
- package/vite.config.ts +0 -39
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { CSSProperties } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* CSS variables for theming.
|
|
4
|
+
* Users can override these via CSS custom properties.
|
|
5
|
+
*/
|
|
6
|
+
export declare const cssVariables: {
|
|
7
|
+
readonly "--atomirx-bg-primary": "#1a1a2e";
|
|
8
|
+
readonly "--atomirx-bg-secondary": "#16213e";
|
|
9
|
+
readonly "--atomirx-bg-tertiary": "#0f3460";
|
|
10
|
+
readonly "--atomirx-bg-hover": "#1f4287";
|
|
11
|
+
readonly "--atomirx-text-primary": "#eaeaea";
|
|
12
|
+
readonly "--atomirx-text-secondary": "#a0a0a0";
|
|
13
|
+
readonly "--atomirx-text-muted": "#666666";
|
|
14
|
+
readonly "--atomirx-border": "#2a2a4a";
|
|
15
|
+
readonly "--atomirx-accent": "#e94560";
|
|
16
|
+
readonly "--atomirx-accent-hover": "#ff6b6b";
|
|
17
|
+
readonly "--atomirx-success": "#4ade80";
|
|
18
|
+
readonly "--atomirx-warning": "#fbbf24";
|
|
19
|
+
readonly "--atomirx-error": "#ef4444";
|
|
20
|
+
readonly "--atomirx-info": "#3b82f6";
|
|
21
|
+
readonly "--atomirx-badge-mutable": "#3b82f6";
|
|
22
|
+
readonly "--atomirx-badge-derived": "#8b5cf6";
|
|
23
|
+
readonly "--atomirx-badge-effect": "#f59e0b";
|
|
24
|
+
readonly "--atomirx-badge-pool": "#10b981";
|
|
25
|
+
readonly "--atomirx-badge-module": "#ec4899";
|
|
26
|
+
readonly "--atomirx-radius": "6px";
|
|
27
|
+
readonly "--atomirx-radius-lg": "8px";
|
|
28
|
+
readonly "--atomirx-font-size": "11px";
|
|
29
|
+
readonly "--atomirx-font-size-sm": "9px";
|
|
30
|
+
readonly "--atomirx-font-mono": "ui-monospace, SFMono-Regular, SF Mono, Menlo, monospace";
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Base container styles with CSS variables.
|
|
34
|
+
*/
|
|
35
|
+
export declare const baseContainerStyle: CSSProperties;
|
|
36
|
+
/**
|
|
37
|
+
* Floating toggle button styles.
|
|
38
|
+
*/
|
|
39
|
+
export declare const floatingButtonStyle: CSSProperties;
|
|
40
|
+
/**
|
|
41
|
+
* Panel container styles by position.
|
|
42
|
+
* Uses non-shorthand border properties to avoid React warnings.
|
|
43
|
+
*/
|
|
44
|
+
export declare const getPanelStyle: (position: "bottom" | "right" | "left", size: number, isOpen: boolean) => CSSProperties;
|
|
45
|
+
/**
|
|
46
|
+
* Tab bar styles.
|
|
47
|
+
*/
|
|
48
|
+
export declare const tabBarStyle: CSSProperties;
|
|
49
|
+
/**
|
|
50
|
+
* Tab button styles.
|
|
51
|
+
*/
|
|
52
|
+
export declare const getTabStyle: (isActive: boolean) => CSSProperties;
|
|
53
|
+
/**
|
|
54
|
+
* Toolbar styles (search, filter, actions).
|
|
55
|
+
*/
|
|
56
|
+
export declare const toolbarStyle: CSSProperties;
|
|
57
|
+
/**
|
|
58
|
+
* Search input styles.
|
|
59
|
+
*/
|
|
60
|
+
export declare const searchInputStyle: CSSProperties;
|
|
61
|
+
/**
|
|
62
|
+
* Filter button group styles.
|
|
63
|
+
*/
|
|
64
|
+
export declare const filterGroupStyle: CSSProperties;
|
|
65
|
+
/**
|
|
66
|
+
* Filter button styles.
|
|
67
|
+
*/
|
|
68
|
+
export declare const getFilterButtonStyle: (isActive: boolean) => CSSProperties;
|
|
69
|
+
/**
|
|
70
|
+
* Entity list container styles.
|
|
71
|
+
*/
|
|
72
|
+
export declare const entityListStyle: CSSProperties;
|
|
73
|
+
/**
|
|
74
|
+
* Entity item styles.
|
|
75
|
+
*/
|
|
76
|
+
export declare const getEntityItemStyle: (isSelected: boolean) => CSSProperties;
|
|
77
|
+
/**
|
|
78
|
+
* Entity type badge styles.
|
|
79
|
+
*/
|
|
80
|
+
export declare const getTypeBadgeStyle: (type: "mutable" | "derived" | "effect" | "pool" | "module") => CSSProperties;
|
|
81
|
+
/**
|
|
82
|
+
* Entity key/name styles.
|
|
83
|
+
*/
|
|
84
|
+
export declare const entityKeyStyle: CSSProperties;
|
|
85
|
+
/**
|
|
86
|
+
* Entity value preview styles.
|
|
87
|
+
*/
|
|
88
|
+
export declare const entityValueStyle: CSSProperties;
|
|
89
|
+
/**
|
|
90
|
+
* Details panel styles.
|
|
91
|
+
*/
|
|
92
|
+
export declare const detailsPanelStyle: CSSProperties;
|
|
93
|
+
/**
|
|
94
|
+
* Details header styles.
|
|
95
|
+
*/
|
|
96
|
+
export declare const detailsHeaderStyle: CSSProperties;
|
|
97
|
+
/**
|
|
98
|
+
* Details section styles.
|
|
99
|
+
*/
|
|
100
|
+
export declare const detailsSectionStyle: CSSProperties;
|
|
101
|
+
/**
|
|
102
|
+
* Details label styles.
|
|
103
|
+
*/
|
|
104
|
+
export declare const detailsLabelStyle: CSSProperties;
|
|
105
|
+
/**
|
|
106
|
+
* Details value styles.
|
|
107
|
+
*/
|
|
108
|
+
export declare const detailsValueStyle: CSSProperties;
|
|
109
|
+
/**
|
|
110
|
+
* Code block styles.
|
|
111
|
+
*/
|
|
112
|
+
export declare const codeBlockStyle: CSSProperties;
|
|
113
|
+
/**
|
|
114
|
+
* History entry styles.
|
|
115
|
+
*/
|
|
116
|
+
export declare const historyEntryStyle: CSSProperties;
|
|
117
|
+
/**
|
|
118
|
+
* History timestamp styles.
|
|
119
|
+
*/
|
|
120
|
+
export declare const historyTimestampStyle: CSSProperties;
|
|
121
|
+
/**
|
|
122
|
+
* Position button styles.
|
|
123
|
+
*/
|
|
124
|
+
export declare const positionButtonStyle: CSSProperties;
|
|
125
|
+
/**
|
|
126
|
+
* Close button styles.
|
|
127
|
+
*/
|
|
128
|
+
export declare const closeButtonStyle: CSSProperties;
|
|
129
|
+
/**
|
|
130
|
+
* Empty state styles.
|
|
131
|
+
*/
|
|
132
|
+
export declare const emptyStateStyle: CSSProperties;
|
|
133
|
+
/**
|
|
134
|
+
* Status indicator styles.
|
|
135
|
+
*/
|
|
136
|
+
export declare const getStatusStyle: (status: "ready" | "loading" | "error") => CSSProperties;
|
|
137
|
+
/**
|
|
138
|
+
* Resize handle styles.
|
|
139
|
+
*/
|
|
140
|
+
export declare const getResizeHandleStyle: (position: "bottom" | "right" | "left") => CSSProperties;
|
|
141
|
+
/**
|
|
142
|
+
* Main content area styles.
|
|
143
|
+
*/
|
|
144
|
+
export declare const mainContentStyle: CSSProperties;
|
|
145
|
+
/**
|
|
146
|
+
* Stat badge styles.
|
|
147
|
+
*/
|
|
148
|
+
export declare const statBadgeStyle: CSSProperties;
|
package/package.json
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
"management",
|
|
8
8
|
"atom",
|
|
9
9
|
"derived",
|
|
10
|
-
"effect"
|
|
10
|
+
"effect",
|
|
11
|
+
"devtools"
|
|
11
12
|
],
|
|
12
13
|
"author": "Gignuyen",
|
|
13
14
|
"license": "MIT",
|
|
@@ -16,7 +17,20 @@
|
|
|
16
17
|
"url": "https://github.com/linq2js/atomirx"
|
|
17
18
|
},
|
|
18
19
|
"homepage": "https://github.com/linq2js/atomirx",
|
|
19
|
-
"version": "0.0
|
|
20
|
+
"version": "0.1.0",
|
|
21
|
+
"bin": {
|
|
22
|
+
"atomirx": "./bin/cli.js"
|
|
23
|
+
},
|
|
24
|
+
"cursor": {
|
|
25
|
+
"skills": [
|
|
26
|
+
"./skills/atomirx"
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"bin",
|
|
32
|
+
"skills"
|
|
33
|
+
],
|
|
20
34
|
"type": "module",
|
|
21
35
|
"main": "./dist/index.cjs",
|
|
22
36
|
"module": "./dist/index.js",
|
|
@@ -31,6 +45,16 @@
|
|
|
31
45
|
"import": "./dist/react/index.js",
|
|
32
46
|
"require": "./dist/react/index.cjs",
|
|
33
47
|
"types": "./dist/react/index.d.ts"
|
|
48
|
+
},
|
|
49
|
+
"./devtools": {
|
|
50
|
+
"import": "./dist/devtools/index.js",
|
|
51
|
+
"require": "./dist/devtools/index.cjs",
|
|
52
|
+
"types": "./dist/devtools/index.d.ts"
|
|
53
|
+
},
|
|
54
|
+
"./react-devtools": {
|
|
55
|
+
"import": "./dist/react-devtools/index.js",
|
|
56
|
+
"require": "./dist/react-devtools/index.cjs",
|
|
57
|
+
"types": "./dist/react-devtools/index.d.ts"
|
|
34
58
|
}
|
|
35
59
|
},
|
|
36
60
|
"scripts": {
|
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: atomirx
|
|
3
|
+
description: Guide for atomirx reactive state management. Use for atom, derived, effect, select, pool, define(), ready(), React hooks (useSelector, rx, useAction, useStable), and debugging reactive flows.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# atomirx State Management
|
|
7
|
+
|
|
8
|
+
## Philosophy: You Don't Care About Async/Sync
|
|
9
|
+
|
|
10
|
+
**Atomirx abstracts away the async/sync distinction.** In reactive contexts, you write sync code regardless of whether atoms contain sync values or Promises.
|
|
11
|
+
|
|
12
|
+
| Context | Your Code | Async Handling |
|
|
13
|
+
| ---------------------------------- | -------------------------------- | -------------------- |
|
|
14
|
+
| `useSelector`, `derived`, `effect` | **Sync** — just `read()` | Suspense handles it |
|
|
15
|
+
| Services | Receive values as **parameters** | Don't read atoms |
|
|
16
|
+
| Outside reactive context | `await .get()` | Explicit when needed |
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
// You don't care if user$ contains sync value or Promise
|
|
20
|
+
const userName$ = derived(({ read }) => read(user$).name);
|
|
21
|
+
|
|
22
|
+
// Same pattern works for any atom
|
|
23
|
+
function MyComponent() {
|
|
24
|
+
const name = useSelector(userName$); // Sync code, Suspense handles async
|
|
25
|
+
return <div>{name}</div>;
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**This is why naming doesn't use `Async$` suffix** — the abstraction makes it irrelevant.
|
|
30
|
+
|
|
31
|
+
## Bootstrap Pattern (DevTools)
|
|
32
|
+
|
|
33
|
+
**DevTools must initialize BEFORE any atoms are created** to properly track all reactive primitives.
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
// main.tsx
|
|
37
|
+
async function main() {
|
|
38
|
+
// 1. Render devtools FIRST (before any atom imports)
|
|
39
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
40
|
+
const { renderDevtools } = await import("atomirx/react-devtools");
|
|
41
|
+
await renderDevtools();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 2. THEN import React and App (which contains atoms)
|
|
45
|
+
const React = await import("react");
|
|
46
|
+
const ReactDOM = await import("react-dom/client");
|
|
47
|
+
const { default: App } = await import("./App");
|
|
48
|
+
|
|
49
|
+
// 3. Render app
|
|
50
|
+
ReactDOM.createRoot(document.getElementById("root")!).render(
|
|
51
|
+
<React.StrictMode>
|
|
52
|
+
<App />
|
|
53
|
+
</React.StrictMode>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
main();
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Why this order matters:**
|
|
61
|
+
|
|
62
|
+
- `onCreateHook` must be set up before atoms are created
|
|
63
|
+
- DevTools uses `onCreateHook` to track atom/derived/effect creation
|
|
64
|
+
- If atoms are imported before devtools, they won't appear in the panel
|
|
65
|
+
|
|
66
|
+
**For apps with bootstrap logic:**
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
// bootstrap.ts
|
|
70
|
+
export async function bootstrap() {
|
|
71
|
+
// Initialize services, fetch config, etc.
|
|
72
|
+
await initializeServices();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// main.tsx
|
|
76
|
+
async function main() {
|
|
77
|
+
// 1. DevTools first
|
|
78
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
79
|
+
const { renderDevtools } = await import("atomirx/react-devtools");
|
|
80
|
+
await renderDevtools();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 2. Bootstrap (may create atoms)
|
|
84
|
+
const { bootstrap } = await import("./bootstrap");
|
|
85
|
+
await bootstrap();
|
|
86
|
+
|
|
87
|
+
// 3. Import and render app
|
|
88
|
+
const React = await import("react");
|
|
89
|
+
const ReactDOM = await import("react-dom/client");
|
|
90
|
+
const { default: App } = await import("./App");
|
|
91
|
+
|
|
92
|
+
ReactDOM.createRoot(document.getElementById("root")!).render(
|
|
93
|
+
<React.StrictMode>
|
|
94
|
+
<App />
|
|
95
|
+
</React.StrictMode>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
main();
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Core Primitives
|
|
103
|
+
|
|
104
|
+
| Primitive | Purpose | Subscription |
|
|
105
|
+
| ------------------ | ------------------------------ | ------------ |
|
|
106
|
+
| `atom<T>(initial)` | Mutable state | No |
|
|
107
|
+
| `derived(fn)` | Computed value | Yes (lazy) |
|
|
108
|
+
| `effect(fn)` | Side effects on changes | Yes (eager) |
|
|
109
|
+
| `select(fn)` | One-time read, no subscription | No |
|
|
110
|
+
| `pool(fn, opts)` | Parameterized atoms with GC | Per-entry |
|
|
111
|
+
| `batch(fn)` | Group updates, single notify | No |
|
|
112
|
+
| `define(fn)` | Lazy singleton factory | No |
|
|
113
|
+
| `onCreateHook` | Track atom/effect creation | No |
|
|
114
|
+
| `onErrorHook` | Global error handling | No |
|
|
115
|
+
|
|
116
|
+
## SelectContext Methods
|
|
117
|
+
|
|
118
|
+
**Works identically in `derived()`, `effect()`, `useSelector()`, `rx()`.** Learn once, use everywhere.
|
|
119
|
+
|
|
120
|
+
| Method | Signature | Behavior |
|
|
121
|
+
| ----------- | ------------------------- | -------------------------------- |
|
|
122
|
+
| `read()` | `read(atom$)` | Read + track dependency |
|
|
123
|
+
| `ready()` | `ready(atom$)` or with fn | Wait for non-null (suspends) |
|
|
124
|
+
| `from()` | `from(pool, params)` | Get ScopedAtom from pool |
|
|
125
|
+
| `track()` | `track(atom$)` | Track without reading |
|
|
126
|
+
| `untrack()` | `untrack(atom$)` or fn | Read/exec without tracking |
|
|
127
|
+
| `safe()` | `safe(() => expr)` | Catch errors, preserve Suspense |
|
|
128
|
+
| `all()` | `all([a$, b$])` | Wait for all (Promise.all) |
|
|
129
|
+
| `any()` | `any({ a: a$, b: b$ })` | First ready (Promise.any) |
|
|
130
|
+
| `race()` | `race({ a: a$, b: b$ })` | First settled (Promise.race) |
|
|
131
|
+
| `settled()` | `settled([a$, b$])` | All results (Promise.allSettled) |
|
|
132
|
+
| `state()` | `state(atom$)` | Get state without throwing |
|
|
133
|
+
| `and()` | `and([cond1, cond2])` | Logical AND, short-circuit |
|
|
134
|
+
| `or()` | `or([cond1, cond2])` | Logical OR, short-circuit |
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
// Same pattern works everywhere
|
|
138
|
+
const pattern = ({ read, all, safe }) => {
|
|
139
|
+
const [user, posts] = all([user$, posts$]);
|
|
140
|
+
const [err, parsed] = safe(() => JSON.parse(read(config$)));
|
|
141
|
+
return { user, posts, config: err ? null : parsed };
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const combined$ = derived(pattern);
|
|
145
|
+
const data = useSelector(pattern);
|
|
146
|
+
effect(pattern);
|
|
147
|
+
{
|
|
148
|
+
rx(pattern);
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## read() vs ready() vs state()
|
|
153
|
+
|
|
154
|
+
| Method | On null/undefined | On loading | Use Case |
|
|
155
|
+
| --------- | ----------------- | -------------- | -------------------- |
|
|
156
|
+
| `read()` | Returns null | Throws Promise | Always need value |
|
|
157
|
+
| `ready()` | Suspends | Throws Promise | Wait for data |
|
|
158
|
+
| `state()` | Returns state obj | Returns state | Manual loading/error |
|
|
159
|
+
|
|
160
|
+
## Key Rules
|
|
161
|
+
|
|
162
|
+
1. **MUST use define()** for all state/logic. Global classes OK, variables MUST be in `define()`.
|
|
163
|
+
2. **MUST use batch()** for multiple atom updates.
|
|
164
|
+
3. **MUST group useSelector calls** into single selector.
|
|
165
|
+
4. **useAction deps: pass atoms, use .get() inside** for auto re-dispatch.
|
|
166
|
+
5. **NEVER try/catch with read()** — breaks Suspense. Use `safe()`.
|
|
167
|
+
6. **MUST co-locate mutations** in store that owns the atom.
|
|
168
|
+
7. **MUST export readonly atoms** via `readonly({ atom$ })`.
|
|
169
|
+
8. **SelectContext is sync only** — NEVER use in setTimeout/Promise.then.
|
|
170
|
+
9. **Services vs Stores** — Services are stateless, Stores have atoms.
|
|
171
|
+
10. **NEVER import service factories** — use `define()`, invoke with `()`.
|
|
172
|
+
11. **Single effect, single workflow** — split multiple workflows.
|
|
173
|
+
12. **MUST define meta.key** for debugging: `{ meta: { key: "store.name" } }`.
|
|
174
|
+
13. **MUST use .override()** for hooks, never assign `.current` directly.
|
|
175
|
+
14. **MUST use useStable()** — NEVER use React's useCallback.
|
|
176
|
+
15. **Use pool** for parameterized state instead of manual Maps.
|
|
177
|
+
|
|
178
|
+
### meta.key (REQUIRED)
|
|
179
|
+
|
|
180
|
+
```tsx
|
|
181
|
+
// ✅ DO: Define meta.key
|
|
182
|
+
const user$ = atom<User | null>(null, { meta: { key: "auth.user" } });
|
|
183
|
+
const isAuth$ = derived(({ read }) => !!read(user$), { meta: { key: "auth.isAuthenticated" } });
|
|
184
|
+
effect(({ read }) => { ... }, { meta: { key: "auth.persistSession" } });
|
|
185
|
+
const userPool = pool((id: string) => fetchUser(id), { gcTime: 60_000, meta: { key: "users" } });
|
|
186
|
+
|
|
187
|
+
// ❌ DON'T: Skip meta.key
|
|
188
|
+
const user$ = atom<User | null>(null);
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### useSelector Grouping
|
|
192
|
+
|
|
193
|
+
```tsx
|
|
194
|
+
// ✅ DO: Single useSelector
|
|
195
|
+
const { user, posts, settings } = useSelector(({ read }) => ({
|
|
196
|
+
user: read(user$),
|
|
197
|
+
posts: read(posts$),
|
|
198
|
+
settings: read(settings$),
|
|
199
|
+
}));
|
|
200
|
+
|
|
201
|
+
// ❌ DON'T: Multiple calls
|
|
202
|
+
const user = useSelector(user$);
|
|
203
|
+
const posts = useSelector(posts$);
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### useAction with Atoms
|
|
207
|
+
|
|
208
|
+
```tsx
|
|
209
|
+
// ✅ DO: Pass atoms to deps, use .get() inside
|
|
210
|
+
const load = useAction(async () => atom1$.get() + (await atom2$.get()), {
|
|
211
|
+
deps: [atom1$, atom2$],
|
|
212
|
+
lazy: false,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// ❌ DON'T: useSelector values in deps
|
|
216
|
+
const { v1, v2 } = useSelector(({ read }) => ({
|
|
217
|
+
v1: read(atom1$),
|
|
218
|
+
v2: read(atom2$),
|
|
219
|
+
}));
|
|
220
|
+
const load = useAction(async () => v1 + v2, { deps: [v1, v2], lazy: false });
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### batch() for Multiple Updates
|
|
224
|
+
|
|
225
|
+
```tsx
|
|
226
|
+
// ✅ DO: Batch
|
|
227
|
+
batch(() => {
|
|
228
|
+
user$.set(newUser);
|
|
229
|
+
settings$.set(newSettings);
|
|
230
|
+
lastUpdated$.set(Date.now());
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// ❌ DON'T: Separate updates
|
|
234
|
+
user$.set(newUser);
|
|
235
|
+
settings$.set(newSettings);
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### useStable() (REQUIRED)
|
|
239
|
+
|
|
240
|
+
**MUST use `useStable()` instead of React's `useCallback`/`useMemo`.**
|
|
241
|
+
|
|
242
|
+
```tsx
|
|
243
|
+
// ❌ FORBIDDEN
|
|
244
|
+
const handleSubmit = useCallback(
|
|
245
|
+
() => auth.register(username),
|
|
246
|
+
[auth, username]
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
// ✅ REQUIRED
|
|
250
|
+
const stable = useStable({
|
|
251
|
+
onSubmit: () => auth.register(username),
|
|
252
|
+
onLogin: () => auth.login(),
|
|
253
|
+
config: { timeout: 5000, retries: 3 },
|
|
254
|
+
columns: [{ key: "name", label: "Name" }],
|
|
255
|
+
});
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### pool() for Parameterized State
|
|
259
|
+
|
|
260
|
+
```tsx
|
|
261
|
+
// ✅ DO: Use pool
|
|
262
|
+
const userPool = pool((id: string) => fetchUser(id), {
|
|
263
|
+
gcTime: 60_000,
|
|
264
|
+
meta: { key: "users" },
|
|
265
|
+
});
|
|
266
|
+
userPool.get("user-1");
|
|
267
|
+
userPool.set("user-1", newUser);
|
|
268
|
+
|
|
269
|
+
// In reactive context use from()
|
|
270
|
+
const userPosts$ = derived(({ read, from }) => {
|
|
271
|
+
const user$ = from(userPool, "user-1");
|
|
272
|
+
return read(user$).posts;
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// ❌ DON'T: Manual Map
|
|
276
|
+
const userCache = new Map<string, MutableAtom<User>>();
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### and()/or() for Boolean Logic
|
|
280
|
+
|
|
281
|
+
```tsx
|
|
282
|
+
// ✅ DO: Use and()/or()
|
|
283
|
+
const canEdit$ = derived(({ and }) => and([isLoggedIn$, hasPermission$]));
|
|
284
|
+
const hasData$ = derived(({ or }) => or([cacheData$, apiData$]));
|
|
285
|
+
|
|
286
|
+
// Lazy evaluation
|
|
287
|
+
const canDelete$ = derived(({ and }) =>
|
|
288
|
+
and([
|
|
289
|
+
isLoggedIn$,
|
|
290
|
+
() => hasDeletePermission$, // Only evaluated if logged in
|
|
291
|
+
])
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
// ❌ DON'T: Manual logic
|
|
295
|
+
const canEdit$ = derived(
|
|
296
|
+
({ read }) => read(isLoggedIn$) && read(hasPermission$)
|
|
297
|
+
);
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### untrack() for Non-Reactive Reads
|
|
301
|
+
|
|
302
|
+
```tsx
|
|
303
|
+
// ✅ DO: Use untrack() when you need to read without re-computing
|
|
304
|
+
const combined$ = derived(({ read, untrack }) => {
|
|
305
|
+
const count = read(count$); // Tracks count$ - re-computes on change
|
|
306
|
+
const config = untrack(config$); // Does NOT track - no re-compute on change
|
|
307
|
+
return count * config.multiplier;
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// Also works with functions for multiple reads
|
|
311
|
+
const snapshot$ = derived(({ read, untrack }) => {
|
|
312
|
+
const liveData = read(liveData$); // Tracked
|
|
313
|
+
const snapshot = untrack(() => {
|
|
314
|
+
// None of these are tracked
|
|
315
|
+
return { a: read(a$), b: read(b$), c: read(c$) };
|
|
316
|
+
});
|
|
317
|
+
return { liveData, snapshot };
|
|
318
|
+
});
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### define() for Services and Stores
|
|
322
|
+
|
|
323
|
+
```tsx
|
|
324
|
+
// ✅ STORE (has atoms)
|
|
325
|
+
export const counterStore = define(() => {
|
|
326
|
+
const count$ = atom(0, { meta: { key: "counter.count" } });
|
|
327
|
+
return {
|
|
328
|
+
...readonly({ count$ }),
|
|
329
|
+
increment: () => count$.set((x) => x + 1),
|
|
330
|
+
};
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// ✅ SERVICE (stateless)
|
|
334
|
+
export const storageService = define(
|
|
335
|
+
(): StorageService => ({
|
|
336
|
+
get: (key) => localStorage.getItem(key),
|
|
337
|
+
set: (key, val) => localStorage.setItem(key, val),
|
|
338
|
+
})
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
// ❌ FORBIDDEN: Factory pattern
|
|
342
|
+
import { getAuthService } from "@/services/auth";
|
|
343
|
+
const auth = getAuthService(); // WRONG
|
|
344
|
+
|
|
345
|
+
// ✅ REQUIRED: Module invocation
|
|
346
|
+
import { authService } from "@/services/auth.service";
|
|
347
|
+
const auth = authService(); // Correct
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### Hooks (.override() REQUIRED)
|
|
351
|
+
|
|
352
|
+
```tsx
|
|
353
|
+
// ❌ FORBIDDEN: Direct assignment
|
|
354
|
+
onCreateHook.current = (info) => { ... };
|
|
355
|
+
|
|
356
|
+
// ✅ REQUIRED: Use .override()
|
|
357
|
+
onCreateHook.override((prev) => (info) => {
|
|
358
|
+
prev?.(info);
|
|
359
|
+
console.log(`Created ${info.type}: ${info.key}`);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
onErrorHook.override((prev) => (info) => {
|
|
363
|
+
prev?.(info);
|
|
364
|
+
Sentry.captureException(info.error);
|
|
365
|
+
});
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
## Finding Things
|
|
369
|
+
|
|
370
|
+
| To Find | Search Pattern |
|
|
371
|
+
| ------------------ | ---------------------------------------------------------- |
|
|
372
|
+
| Atom definitions | `atom<` or `atom(` |
|
|
373
|
+
| Derived atoms | `derived((` |
|
|
374
|
+
| Effects | `effect((` |
|
|
375
|
+
| Pools | `pool((` |
|
|
376
|
+
| Stores | `define(() =>` in `*.store.ts` |
|
|
377
|
+
| Services | `define(() =>` in `*.service.ts` |
|
|
378
|
+
| Atom usages | `read(`, `ready(`, `all([`, `any({`, `race({`, `settled([` |
|
|
379
|
+
| Non-reactive reads | `untrack(` |
|
|
380
|
+
| Pool usages | `from(poolName,` |
|
|
381
|
+
| Mutations | Find store owner, check return statement |
|
|
382
|
+
| Hook setup | `onCreateHook.override`, `onErrorHook.override` |
|
|
383
|
+
|
|
384
|
+
## Debugging "Why doesn't X update?"
|
|
385
|
+
|
|
386
|
+
1. Find atom being set → search `.set(`
|
|
387
|
+
2. Find subscribers → search `read(atomName$)`, `ready(atomName$)`
|
|
388
|
+
3. Check derived is subscribed → used in `useSelector`?
|
|
389
|
+
4. Check effect cleanup → doesn't prevent re-run?
|
|
390
|
+
5. For pools → check if entry was GC'd, verify `from()` usage
|
|
391
|
+
|
|
392
|
+
## Common Issues
|
|
393
|
+
|
|
394
|
+
| Symptom | Likely Cause | Fix |
|
|
395
|
+
| ---------------------- | ---------------------------- | --------------------------------- |
|
|
396
|
+
| Derived never updates | No active subscription | Use `useSelector` in component |
|
|
397
|
+
| Effect runs infinitely | Setting atom it reads | Use `select()` for non-reactive |
|
|
398
|
+
| ready() never resolves | Value never becomes non-null | Check data flow |
|
|
399
|
+
| Stale closure | Reading atom in callback | Use `.get()` in callbacks |
|
|
400
|
+
| Suspense not working | try/catch around read() | Use `safe()` instead |
|
|
401
|
+
| Hook not firing | Direct `.current` assign | Use `.override()` instead |
|
|
402
|
+
| Missing hook calls | Hook chain broken | Always call `prev?.(info)` |
|
|
403
|
+
| Pool entry missing | GC'd before access | Increase gcTime |
|
|
404
|
+
| ScopedAtom error | Used outside context | Only use from() inside derived |
|
|
405
|
+
| Too many re-computes | Tracking unnecessary deps | Use `untrack()` for config |
|
|
406
|
+
| DevTools missing atoms | Atoms created before hook | Use bootstrap pattern (see above) |
|
|
407
|
+
|
|
408
|
+
## Naming Conventions
|
|
409
|
+
|
|
410
|
+
| Type | Variable | File | Contains |
|
|
411
|
+
| ----------- | ------------- | ----------------- | ----------------------- |
|
|
412
|
+
| **Service** | `authService` | `auth.service.ts` | Pure functions only |
|
|
413
|
+
| **Store** | `authStore` | `auth.store.ts` | Atoms, derived, effects |
|
|
414
|
+
|
|
415
|
+
| Type | Suffix | Example |
|
|
416
|
+
| -------------------- | --------- | ------------------------------ |
|
|
417
|
+
| Atom (sync or async) | `$` | `count$`, `user$`, `products$` |
|
|
418
|
+
| Derived | `$` | `doubled$`, `userName$` |
|
|
419
|
+
| Pool | `Pool` | `userPool`, `productPool` |
|
|
420
|
+
| Service | `Service` | `authService` (NO atoms) |
|
|
421
|
+
| Store | `Store` | `authStore` (HAS atoms) |
|
|
422
|
+
| Actions | verb-led | `navigateTo`, `invalidate` |
|
|
423
|
+
|
|
424
|
+
**Why no `Async$`?** Atomirx abstracts async/sync — you don't care in SelectContext (Suspense handles it).
|
|
425
|
+
|
|
426
|
+
### File Structure
|
|
427
|
+
|
|
428
|
+
```
|
|
429
|
+
src/
|
|
430
|
+
├── services/ # Stateless
|
|
431
|
+
│ ├── auth/
|
|
432
|
+
│ │ └── auth.service.ts
|
|
433
|
+
│ └── crypto/
|
|
434
|
+
│ └── crypto.service.ts
|
|
435
|
+
└── stores/ # Stateful
|
|
436
|
+
├── auth.store.ts
|
|
437
|
+
├── todos.store.ts
|
|
438
|
+
└── sync.store.ts
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
## References
|
|
442
|
+
|
|
443
|
+
- [Rules & Best Practices](references/rules.md)
|
|
444
|
+
- [Pool Patterns](references/pool-patterns.md)
|
|
445
|
+
- [Select Context](references/select-context.md)
|
|
446
|
+
- [Deferred Loading](references/deferred-loading.md)
|
|
447
|
+
- [React Integration](references/react-integration.md)
|
|
448
|
+
- [Error Handling](references/error-handling.md)
|
|
449
|
+
- [Async Patterns](references/async-patterns.md)
|
|
450
|
+
- [Atom Patterns](references/atom-patterns.md)
|
|
451
|
+
- [Derived Patterns](references/derived-patterns.md)
|
|
452
|
+
- [Effect Patterns](references/effect-patterns.md)
|
|
453
|
+
- [Hooks](references/hooks.md)
|
|
454
|
+
- [Testing Patterns](references/testing-patterns.md)
|
|
455
|
+
- [Store Template](references/store-template.md)
|
|
456
|
+
- [Service Template](references/service-template.md)
|