@violetflux/kerros 0.1.9 → 0.2.1
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.de.md +7 -10
- package/README.es.md +7 -10
- package/README.fr.md +7 -10
- package/README.ja.md +7 -10
- package/README.ko.md +7 -10
- package/README.md +43 -11
- package/README.zh-CN.md +43 -11
- package/dist/index.cjs +79 -30
- package/dist/index.d.cts +30 -7
- package/dist/index.d.mts +30 -7
- package/dist/index.mjs +80 -31
- package/package.json +23 -7
- package/skills/kerros/SKILL.md +22 -18
- package/skills/kerros/agents/openai.yaml +2 -2
package/README.de.md
CHANGED
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
<a href="https://github.com/violetflux/kerros/blob/main/README.es.md">Español</a>
|
|
13
13
|
</p>
|
|
14
14
|
|
|
15
|
-
Kerros lässt React-State dort, wo er natürlich hingehört: in Hooks und unter Providern.
|
|
15
|
+
Kerros lässt React-State dort, wo er natürlich hingehört: in Hooks und unter Providern. Automatisches Property-Tracking verhindert standardmäßig unnötige Renders; explizite Selektoren bleiben für abgeleitete Werte und Hotspots verfügbar.
|
|
16
16
|
|
|
17
17
|
- Ein Store ist ein normaler React Hook
|
|
18
|
-
-
|
|
19
|
-
-
|
|
18
|
+
- `useStore()` verfolgt gelesene Properties automatisch
|
|
19
|
+
- Explizite Selektoren sind eine fortgeschrittene Optimierung
|
|
20
20
|
- Jeder Provider besitzt eine isolierte Store-Instanz
|
|
21
21
|
- Stores lassen sich über einseitige Abhängigkeiten komponieren
|
|
22
22
|
- Unterstützt React 17, 18 und 19
|
|
@@ -46,14 +46,11 @@ Im Store Hook können weiterhin `useState`, `useReducer`, Context, SDK Hooks und
|
|
|
46
46
|
|
|
47
47
|
Definiere den Initializer als benannten Hook auf Modulebene, zum Beispiel `useCounterModel`. Anonyme Initializer funktionieren weiterhin zur Laufzeit, werden vom React Compiler im `infer`-Modus aber nicht automatisch als Hooks kompiliert.
|
|
48
48
|
|
|
49
|
-
## Provider einbinden und Werte
|
|
49
|
+
## Provider einbinden und Werte lesen
|
|
50
50
|
|
|
51
51
|
```tsx
|
|
52
52
|
function Counter() {
|
|
53
|
-
const { count, setCount } = useCounter(
|
|
54
|
-
count: s.count,
|
|
55
|
-
setCount: s.setCount,
|
|
56
|
-
}))
|
|
53
|
+
const { count, setCount } = useCounter()
|
|
57
54
|
return <button onClick={() => setCount(count + 1)}>{count}</button>
|
|
58
55
|
}
|
|
59
56
|
|
|
@@ -62,7 +59,7 @@ function App() {
|
|
|
62
59
|
}
|
|
63
60
|
```
|
|
64
61
|
|
|
65
|
-
|
|
62
|
+
Kerros verfolgt die beim Rendern gelesenen Properties. Änderungen an ungelesenen Feldern rendern `Counter` nicht neu; ein Deep-Vergleich des gesamten Stores findet nicht statt.
|
|
66
63
|
|
|
67
64
|
## Installation
|
|
68
65
|
|
|
@@ -89,7 +86,7 @@ function createStore<TStore, TProps = Record<never, never>>(
|
|
|
89
86
|
const [useStream, StreamProvider] = bindStore<Stream>('Stream')
|
|
90
87
|
```
|
|
91
88
|
|
|
92
|
-
Der zurückgegebene Store Hook
|
|
89
|
+
Der zurückgegebene Store Hook verwendet ohne Argument automatisches Tracking. Explizite Objekt-Selektoren sind für abgeleitete Werte und gemessene Hotspots verfügbar. Außerhalb des passenden Providers wird ein verständlicher Fehler ausgelöst.
|
|
93
90
|
|
|
94
91
|
`bindStore` ist eine fortgeschrittene Integration ausschließlich für einen bestehenden Headless External Store. Für normalen Hook-Zustand bleibt `createStore` die richtige Wahl. Der Context enthält nur die ursprüngliche Store-Instanz; Verbraucher verwenden `getSnapshot` und `subscribe` direkt.
|
|
95
92
|
|
package/README.es.md
CHANGED
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
Español
|
|
13
13
|
</p>
|
|
14
14
|
|
|
15
|
-
Kerros mantiene el estado de React donde pertenece
|
|
15
|
+
Kerros mantiene el estado de React donde pertenece: dentro de Hooks y bajo Providers. El seguimiento automático de propiedades evita renderizados innecesarios por defecto; los selectores explícitos siguen disponibles para valores derivados y puntos críticos.
|
|
16
16
|
|
|
17
17
|
- Un Store es un Hook de React normal
|
|
18
|
-
-
|
|
19
|
-
- Los
|
|
18
|
+
- `useStore()` sigue automáticamente las propiedades leídas
|
|
19
|
+
- Los selectores explícitos son una optimización avanzada
|
|
20
20
|
- Cada Provider posee una instancia de Store aislada
|
|
21
21
|
- Los Stores se componen mediante dependencias unidireccionales
|
|
22
22
|
- Compatible con React 17, 18 y 19
|
|
@@ -46,14 +46,11 @@ El Hook del Store puede seguir usando `useState`, `useReducer`, Context, Hooks d
|
|
|
46
46
|
|
|
47
47
|
Define el initializer como un Hook con nombre en el nivel superior, por ejemplo `useCounterModel`. Los initializers anónimos siguen funcionando en runtime, pero React Compiler no los compila automáticamente como Hooks en modo `infer`.
|
|
48
48
|
|
|
49
|
-
## Montar el Provider y
|
|
49
|
+
## Montar el Provider y leer valores
|
|
50
50
|
|
|
51
51
|
```tsx
|
|
52
52
|
function Counter() {
|
|
53
|
-
const { count, setCount } = useCounter(
|
|
54
|
-
count: s.count,
|
|
55
|
-
setCount: s.setCount,
|
|
56
|
-
}))
|
|
53
|
+
const { count, setCount } = useCounter()
|
|
57
54
|
return <button onClick={() => setCount(count + 1)}>{count}</button>
|
|
58
55
|
}
|
|
59
56
|
|
|
@@ -62,7 +59,7 @@ function App() {
|
|
|
62
59
|
}
|
|
63
60
|
```
|
|
64
61
|
|
|
65
|
-
|
|
62
|
+
Kerros sigue las propiedades leídas durante el renderizado. Cambiar un campo no leído no vuelve a renderizar `Counter`, sin comparar profundamente todo el Store.
|
|
66
63
|
|
|
67
64
|
## Instalación
|
|
68
65
|
|
|
@@ -89,7 +86,7 @@ function createStore<TStore, TProps = Record<never, never>>(
|
|
|
89
86
|
const [useStream, StreamProvider] = bindStore<Stream>('Stream')
|
|
90
87
|
```
|
|
91
88
|
|
|
92
|
-
|
|
89
|
+
Sin argumentos, el Hook devuelto usa seguimiento automático. Los selectores de objeto explícitos quedan para valores derivados y puntos críticos medidos. Usarlo fuera de su Provider produce un error claro.
|
|
93
90
|
|
|
94
91
|
`bindStore` es una integración avanzada solo para un Headless External Store existente. Para el estado normal de Hooks, usa `createStore`. El Context solo contiene la instancia original; los consumidores utilizan directamente `getSnapshot` y `subscribe`.
|
|
95
92
|
|
package/README.fr.md
CHANGED
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
<a href="https://github.com/violetflux/kerros/blob/main/README.es.md">Español</a>
|
|
13
13
|
</p>
|
|
14
14
|
|
|
15
|
-
Kerros conserve l'état React là où il se trouve naturellement : dans les Hooks et sous les Providers.
|
|
15
|
+
Kerros conserve l'état React là où il se trouve naturellement : dans les Hooks et sous les Providers. Le suivi automatique des propriétés évite par défaut les rendus inutiles ; les sélecteurs explicites restent disponibles pour les valeurs dérivées et les points chauds.
|
|
16
16
|
|
|
17
17
|
- Un Store est un Hook React ordinaire
|
|
18
|
-
-
|
|
19
|
-
- Les
|
|
18
|
+
- `useStore()` suit automatiquement les propriétés lues
|
|
19
|
+
- Les sélecteurs explicites sont une optimisation avancée
|
|
20
20
|
- Chaque Provider possède une instance de Store isolée
|
|
21
21
|
- Les Stores se composent grâce à des dépendances unidirectionnelles
|
|
22
22
|
- Compatible avec React 17, 18 et 19
|
|
@@ -46,14 +46,11 @@ Le Hook du Store peut continuer à utiliser `useState`, `useReducer`, Context, d
|
|
|
46
46
|
|
|
47
47
|
Définissez l'initializer comme un Hook nommé au niveau du module, par exemple `useCounterModel`. Les initializers anonymes fonctionnent toujours à l'exécution, mais React Compiler ne les compile pas automatiquement comme Hooks en mode `infer`.
|
|
48
48
|
|
|
49
|
-
## Monter le Provider et
|
|
49
|
+
## Monter le Provider et lire les valeurs
|
|
50
50
|
|
|
51
51
|
```tsx
|
|
52
52
|
function Counter() {
|
|
53
|
-
const { count, setCount } = useCounter(
|
|
54
|
-
count: s.count,
|
|
55
|
-
setCount: s.setCount,
|
|
56
|
-
}))
|
|
53
|
+
const { count, setCount } = useCounter()
|
|
57
54
|
return <button onClick={() => setCount(count + 1)}>{count}</button>
|
|
58
55
|
}
|
|
59
56
|
|
|
@@ -62,7 +59,7 @@ function App() {
|
|
|
62
59
|
}
|
|
63
60
|
```
|
|
64
61
|
|
|
65
|
-
|
|
62
|
+
Kerros suit les propriétés lues pendant le rendu. La modification d'un champ non lu ne provoque pas un nouveau rendu de `Counter`, sans comparaison profonde du Store complet.
|
|
66
63
|
|
|
67
64
|
## Installation
|
|
68
65
|
|
|
@@ -89,7 +86,7 @@ function createStore<TStore, TProps = Record<never, never>>(
|
|
|
89
86
|
const [useStream, StreamProvider] = bindStore<Stream>('Stream')
|
|
90
87
|
```
|
|
91
88
|
|
|
92
|
-
|
|
89
|
+
Sans argument, le Hook retourné active le suivi automatique. Les sélecteurs d'objet explicites servent aux valeurs dérivées et aux points chauds mesurés. Son utilisation hors du Provider lève une erreur claire.
|
|
93
90
|
|
|
94
91
|
`bindStore` est une intégration avancée réservée à un Headless External Store existant. Pour un état Hook ordinaire, utilisez `createStore`. Le Context ne contient que l'instance d'origine ; les consommateurs utilisent directement `getSnapshot` et `subscribe`.
|
|
95
92
|
|
package/README.ja.md
CHANGED
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
<a href="https://github.com/violetflux/kerros/blob/main/README.es.md">Español</a>
|
|
13
13
|
</p>
|
|
14
14
|
|
|
15
|
-
Kerros は React の状態を Hook の中、Provider
|
|
15
|
+
Kerros は React の状態を Hook の中、Provider の下という自然な場所に保ちます。デフォルトの自動プロパティ追跡が不要な再レンダーを防ぎ、明示的 selector は派生値や計測済みホットスポットで利用できます。
|
|
16
16
|
|
|
17
17
|
- Store は通常の React Hook
|
|
18
|
-
-
|
|
19
|
-
-
|
|
18
|
+
- `useStore()` は読み取ったプロパティを自動追跡
|
|
19
|
+
- 明示的 selector は高度な最適化として利用
|
|
20
20
|
- Provider ごとに独立した Store インスタンス
|
|
21
21
|
- 一方向の依存関係で Store を合成可能
|
|
22
22
|
- React 17、18、19 をサポート
|
|
@@ -46,14 +46,11 @@ Store Hook 内では `useState`、`useReducer`、Context、SDK Hook、カスタ
|
|
|
46
46
|
|
|
47
47
|
initializer は `useCounterModel` のようなモジュール直下の名前付き Hook として定義してください。匿名 initializer も実行時には動作しますが、React Compiler の `infer` モードでは Hook として自動コンパイルされません。
|
|
48
48
|
|
|
49
|
-
## Provider
|
|
49
|
+
## Provider を配置して値を読み取る
|
|
50
50
|
|
|
51
51
|
```tsx
|
|
52
52
|
function Counter() {
|
|
53
|
-
const { count, setCount } = useCounter(
|
|
54
|
-
count: s.count,
|
|
55
|
-
setCount: s.setCount,
|
|
56
|
-
}))
|
|
53
|
+
const { count, setCount } = useCounter()
|
|
57
54
|
|
|
58
55
|
return <button onClick={() => setCount(count + 1)}>{count}</button>
|
|
59
56
|
}
|
|
@@ -63,7 +60,7 @@ function App() {
|
|
|
63
60
|
}
|
|
64
61
|
```
|
|
65
62
|
|
|
66
|
-
|
|
63
|
+
Kerros はレンダー中に読み取ったプロパティを追跡します。未読フィールドの変更では `Counter` は再レンダーされず、Store 全体の深い比較も行いません。
|
|
67
64
|
|
|
68
65
|
## インストール
|
|
69
66
|
|
|
@@ -90,7 +87,7 @@ function createStore<TStore, TProps = Record<never, never>>(
|
|
|
90
87
|
const [useStream, StreamProvider] = bindStore<Stream>('Stream')
|
|
91
88
|
```
|
|
92
89
|
|
|
93
|
-
返される Store Hook
|
|
90
|
+
返される Store Hook は引数なしで自動追跡を使います。明示的なオブジェクト selector は派生値や計測済みホットスポット向けです。対応する Provider の外では明確なエラーを送出します。
|
|
94
91
|
|
|
95
92
|
高度な連携として、既存の Headless External Store にだけ `bindStore` を使います。通常の Hook 状態には `createStore` を使ってください。Context は元の Store インスタンスだけを保持し、コンシューマーは `getSnapshot` と `subscribe` を直接利用します。
|
|
96
93
|
|
package/README.ko.md
CHANGED
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
<a href="https://github.com/violetflux/kerros/blob/main/README.es.md">Español</a>
|
|
13
13
|
</p>
|
|
14
14
|
|
|
15
|
-
Kerros는 React 상태를 Hook 안과 Provider 아래라는 자연스러운 위치에 둡니다.
|
|
15
|
+
Kerros는 React 상태를 Hook 안과 Provider 아래라는 자연스러운 위치에 둡니다. 기본 자동 속성 추적이 불필요한 렌더링을 막고, 명시적 selector는 파생 값과 측정된 핫스팟에 사용할 수 있습니다.
|
|
16
16
|
|
|
17
17
|
- Store는 평범한 React Hook
|
|
18
|
-
-
|
|
19
|
-
-
|
|
18
|
+
- `useStore()`는 읽은 속성을 자동으로 추적
|
|
19
|
+
- 명시적 selector는 고급 최적화에 사용
|
|
20
20
|
- Provider마다 격리된 Store 인스턴스
|
|
21
21
|
- 단방향 의존성을 통한 Store 조합
|
|
22
22
|
- React 17, 18, 19 지원
|
|
@@ -46,14 +46,11 @@ Store Hook 안에서 `useState`, `useReducer`, Context, SDK Hook, 사용자 Hook
|
|
|
46
46
|
|
|
47
47
|
initializer는 `useCounterModel`처럼 모듈 최상위의 이름 있는 Hook으로 정의하세요. 익명 initializer도 런타임에서는 동작하지만 React Compiler의 `infer` 모드에서는 Hook으로 자동 컴파일되지 않습니다.
|
|
48
48
|
|
|
49
|
-
## Provider를 마운트하고
|
|
49
|
+
## Provider를 마운트하고 값 읽기
|
|
50
50
|
|
|
51
51
|
```tsx
|
|
52
52
|
function Counter() {
|
|
53
|
-
const { count, setCount } = useCounter(
|
|
54
|
-
count: s.count,
|
|
55
|
-
setCount: s.setCount,
|
|
56
|
-
}))
|
|
53
|
+
const { count, setCount } = useCounter()
|
|
57
54
|
|
|
58
55
|
return <button onClick={() => setCount(count + 1)}>{count}</button>
|
|
59
56
|
}
|
|
@@ -63,7 +60,7 @@ function App() {
|
|
|
63
60
|
}
|
|
64
61
|
```
|
|
65
62
|
|
|
66
|
-
|
|
63
|
+
Kerros는 렌더링 중 읽은 속성을 추적합니다. 읽지 않은 필드가 바뀌어도 `Counter`는 다시 렌더링되지 않으며 전체 Store를 깊게 비교하지 않습니다.
|
|
67
64
|
|
|
68
65
|
## 설치
|
|
69
66
|
|
|
@@ -90,7 +87,7 @@ function createStore<TStore, TProps = Record<never, never>>(
|
|
|
90
87
|
const [useStream, StreamProvider] = bindStore<Stream>('Stream')
|
|
91
88
|
```
|
|
92
89
|
|
|
93
|
-
반환된 Store Hook
|
|
90
|
+
반환된 Store Hook은 인자 없이 자동 추적을 사용합니다. 명시적 객체 selector는 파생 값과 측정된 핫스팟을 위한 고급 경로입니다. Provider 밖에서 호출하면 명확한 오류가 발생합니다.
|
|
94
91
|
|
|
95
92
|
고급 통합이 필요한 기존 Headless External Store에만 `bindStore`를 사용하세요. 일반 Hook 상태에는 `createStore`를 사용합니다. Context는 원래 Store 인스턴스만 보관하고 소비자는 `getSnapshot`과 `subscribe`를 직접 사용합니다.
|
|
96
93
|
|
package/README.md
CHANGED
|
@@ -88,14 +88,11 @@ function App() {
|
|
|
88
88
|
|
|
89
89
|
### Use the Store
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
Read the Store directly. Kerros automatically tracks the properties read while this component renders:
|
|
92
92
|
|
|
93
93
|
```tsx
|
|
94
94
|
function TaskList() {
|
|
95
|
-
const { tasks, finishTask } = useTask(
|
|
96
|
-
tasks: s.tasks,
|
|
97
|
-
finishTask: s.finishTask,
|
|
98
|
-
}))
|
|
95
|
+
const { tasks, finishTask } = useTask()
|
|
99
96
|
|
|
100
97
|
return (
|
|
101
98
|
<ul>
|
|
@@ -110,7 +107,7 @@ function TaskList() {
|
|
|
110
107
|
}
|
|
111
108
|
```
|
|
112
109
|
|
|
113
|
-
|
|
110
|
+
Changing an unread field does not rerender `TaskList`. Property tracking follows object, array, and nested reads; it does not perform deep equality over the whole Store.
|
|
114
111
|
|
|
115
112
|
## Install
|
|
116
113
|
|
|
@@ -128,7 +125,7 @@ React 17, React 18, and React 19 are supported.
|
|
|
128
125
|
- **Almost nothing new to learn** — reuse the React knowledge you already have; if you can write a custom Hook, you can write a Store
|
|
129
126
|
- **Designed for flexible refactoring** — Stores and components use the same Hook API, so local state can become shared state with very little work
|
|
130
127
|
- **Local and application-wide state** — Provider placement determines the Store scope, balancing flexibility with simplicity
|
|
131
|
-
- **Avoid Context-wide rerenders** — Context carries a stable container and components rerender only when
|
|
128
|
+
- **Avoid Context-wide rerenders** — Context carries a stable container and components rerender only when an observed value changes
|
|
132
129
|
- **TypeScript support** — Store and selector types are inferred without duplicate declarations
|
|
133
130
|
|
|
134
131
|
## From state management to state sharing
|
|
@@ -139,9 +136,25 @@ Kerros focuses on a smaller and more direct problem. It does not invent a new da
|
|
|
139
136
|
|
|
140
137
|
Passing `value` and `onChange` through layer after layer damages component boundaries. Moving everything into one global Store does not automatically make an application scalable or maintainable either.
|
|
141
138
|
|
|
142
|
-
Sharing frequently changing state through React Context directly also causes repeated work: every Context value change rerenders all consumers. Kerros keeps Provider scoping and multiple instances, but Context carries only a stable container.
|
|
139
|
+
Sharing frequently changing state through React Context directly also causes repeated work: every Context value change rerenders all consumers. Kerros keeps Provider scoping and multiple instances, but Context carries only a stable container. Automatic tracking observes render-time reads, so unrelated Store updates do not rerender a component.
|
|
143
140
|
|
|
144
|
-
Kerros stays simple, lightweight, and reliable. Write local state as an ordinary Hook, share it only when necessary, use a Provider to set its scope, and
|
|
141
|
+
Kerros stays simple, lightweight, and reliable. Write local state as an ordinary Hook, share it only when necessary, use a Provider to set its scope, and let automatic tracking observe what each component reads.
|
|
142
|
+
|
|
143
|
+
## Subscription modes
|
|
144
|
+
|
|
145
|
+
The selector-free form is the default and usually the best starting point:
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
const { count, setCount } = useCounter()
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
- `useStore()` automatically tracks object, array, and nested properties read during render.
|
|
152
|
+
- `useStore(selector)` is the advanced path for derived values and measured hot spots. Its returned object's top-level fields are shallowly compared with `Object.is`.
|
|
153
|
+
- `createStore(model, { tracking: false })` and `bindStore({ tracking: false })` make selector-free reads compare the complete Store at the top level instead.
|
|
154
|
+
|
|
155
|
+
Primitive snapshots use `Object.is`. `Map`, `Set`, class instances, and other non-plain objects are treated as atomic references. Store and external Store snapshots must be immutable: publish a new reference for every observable change.
|
|
156
|
+
|
|
157
|
+
Do not save, return, spread, serialize, or pass the complete selector-free result around. Read properties immediately, normally by destructuring. Effects and `useEffectEvent` may perform imperative reads from `useInstance()`, but rendered state must use the subscribed Store Hook; never expose an Effect Event as a public Store action.
|
|
145
158
|
|
|
146
159
|
## Multiple instances
|
|
147
160
|
|
|
@@ -167,7 +180,7 @@ A Store may call another Store directly. For example, a task Store can read the
|
|
|
167
180
|
|
|
168
181
|
```tsx
|
|
169
182
|
function useTaskModel() {
|
|
170
|
-
const { user } = useAccount(
|
|
183
|
+
const { user } = useAccount()
|
|
171
184
|
const [tasks, setTasks] = useState<Task[]>([])
|
|
172
185
|
|
|
173
186
|
const addTask = (title: string) => {
|
|
@@ -227,12 +240,13 @@ const [useCounter, CounterProvider] = createStore(useCounterModel)
|
|
|
227
240
|
```ts
|
|
228
241
|
function createStore<TStore, TProps = Record<never, never>>(
|
|
229
242
|
useModel: (props: TProps) => TStore,
|
|
243
|
+
options?: { tracking?: boolean },
|
|
230
244
|
): readonly [StoreHook<TStore>, StoreProvider<TProps>]
|
|
231
245
|
```
|
|
232
246
|
|
|
233
247
|
- `useModel` follows the Rules of Hooks
|
|
234
248
|
- Provider props, excluding `children`, are passed to `useModel`
|
|
235
|
-
- the returned Store Hook
|
|
249
|
+
- the returned Store Hook accepts either no argument for automatic tracking or an object-returning selector
|
|
236
250
|
- using the Store Hook outside its matching Provider throws a clear error
|
|
237
251
|
- Provider instances work with Strict Mode and server rendering
|
|
238
252
|
|
|
@@ -258,6 +272,24 @@ If the state begins in `useState`, `useReducer`, an SDK Hook, or another custom
|
|
|
258
272
|
|
|
259
273
|
Kerros uses the official `use-sync-external-store` shim for React 17 and prefers React's native implementation in React 18 and 19. React Compiler is optional.
|
|
260
274
|
|
|
275
|
+
## ESLint guardrails
|
|
276
|
+
|
|
277
|
+
Install the separate type-aware plugin for the safest default usage:
|
|
278
|
+
|
|
279
|
+
```sh
|
|
280
|
+
npm install --save-dev @violetflux/eslint-plugin-kerros @typescript-eslint/parser
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
```js
|
|
284
|
+
import kerros from '@violetflux/eslint-plugin-kerros'
|
|
285
|
+
|
|
286
|
+
export default [kerros.configs.recommendedTypeChecked]
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
`recommendedTypeChecked` enables all 17 rules as errors and uses TypeScript `projectService`. Very large repositories may use `kerros.configs.fastTypeChecked`, which keeps type-aware Store recognition but disables the most expensive whole-program and deep analyses. See the [measured ESLint benchmark](https://github.com/violetflux/kerros/blob/main/benchmarks/eslint/RESULTS.md); the fast profile is a tradeoff, not an untyped fallback. The plugin analyzes complete TS/TSX files, not incomplete Markdown snippets.
|
|
290
|
+
|
|
291
|
+
For maintainers, npm Trusted Publisher entries must be configured for both `@violetflux/kerros` and `@violetflux/eslint-plugin-kerros`. That npm-side configuration is the only release step outside this repository; CI checks and publishes the runtime first, then the plugin.
|
|
292
|
+
|
|
261
293
|
## Documentation
|
|
262
294
|
|
|
263
295
|
- [Introduction](https://violetflux.github.io/kerros/guide/introduction)
|
package/README.zh-CN.md
CHANGED
|
@@ -80,14 +80,11 @@ function App() {
|
|
|
80
80
|
|
|
81
81
|
### 使用 Store
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
直接读取 Store。Kerros 会自动追踪组件渲染期间访问的属性:
|
|
84
84
|
|
|
85
85
|
```tsx
|
|
86
86
|
function TaskList() {
|
|
87
|
-
const { tasks, finishTask } = useTask(
|
|
88
|
-
tasks: s.tasks,
|
|
89
|
-
finishTask: s.finishTask,
|
|
90
|
-
}))
|
|
87
|
+
const { tasks, finishTask } = useTask()
|
|
91
88
|
|
|
92
89
|
return (
|
|
93
90
|
<ul>
|
|
@@ -102,7 +99,7 @@ function TaskList() {
|
|
|
102
99
|
}
|
|
103
100
|
```
|
|
104
101
|
|
|
105
|
-
|
|
102
|
+
没有读取的字段发生变化时,`TaskList` 不会重渲染。自动追踪支持对象、数组和深层属性访问,不会对完整 Store 做深比较。
|
|
106
103
|
|
|
107
104
|
## 安装
|
|
108
105
|
|
|
@@ -120,7 +117,7 @@ Kerros 会浅比较 selector 返回对象的顶层字段。只要这些选中字
|
|
|
120
117
|
- **几乎没有学习成本**:直接复用已有的 React 知识,你怎么写 custom Hook,就可以怎么写 Store
|
|
121
118
|
- **为灵活重构而设计**:Store 和组件使用同一套 Hook API,可以近乎零成本地把组件局部状态转换成组件间共享状态
|
|
122
119
|
- **同时支持局部状态和全局状态**:Provider 决定 Store 的作用域,在灵活和简单之间取得平衡
|
|
123
|
-
- **解决 Context 的重复渲染问题**:Context
|
|
120
|
+
- **解决 Context 的重复渲染问题**:Context 只传递稳定容器,组件观察到的值不变时不会重渲染
|
|
124
121
|
- **优秀的 TypeScript 支持**:Store 和 selector 类型自动推断,不需要重复声明
|
|
125
122
|
|
|
126
123
|
## 从状态管理到状态共享
|
|
@@ -131,9 +128,25 @@ Kerros 想解决的问题更小,也更直接。它不发明新的数据结构
|
|
|
131
128
|
|
|
132
129
|
层层传递 `value`、`onChange` 会逐渐破坏组件边界;粗暴地把数据全部塞进一个全局 Store,也不会自动让应用获得更好的扩展性和可维护性。
|
|
133
130
|
|
|
134
|
-
直接用 React Context 共享变化频繁的状态也会带来重复渲染:Context value 每次变化,所有消费者都会更新。Kerros 保留 Provider 的作用域和多实例能力,但 Context
|
|
131
|
+
直接用 React Context 共享变化频繁的状态也会带来重复渲染:Context value 每次变化,所有消费者都会更新。Kerros 保留 Provider 的作用域和多实例能力,但 Context 只传递稳定容器;自动追踪根据渲染期间的读取建立订阅,无关 Store 更新不会触发组件重渲染。
|
|
135
132
|
|
|
136
|
-
Kerros 简单、轻量、可靠。先把状态写成普通 Hook,需要共享时再交给 `createStore`;Provider
|
|
133
|
+
Kerros 简单、轻量、可靠。先把状态写成普通 Hook,需要共享时再交给 `createStore`;Provider 决定状态共享到哪里,自动追踪决定每个组件订阅什么。
|
|
134
|
+
|
|
135
|
+
## 三种订阅模式
|
|
136
|
+
|
|
137
|
+
不传 selector 是默认用法,也是多数场景的起点:
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
const { count, setCount } = useCounter()
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
- `useStore()`:自动追踪渲染期间读取的对象、数组和深层属性。
|
|
144
|
+
- `useStore(selector)`:用于高级派生值和经过测量的性能热点;Kerros 用 `Object.is` 浅比较 selector 返回对象的顶层字段。
|
|
145
|
+
- `createStore(model, { tracking: false })` 或 `bindStore({ tracking: false })`:关闭自动追踪,无 selector 读取改为完整 Store 顶层浅比较。
|
|
146
|
+
|
|
147
|
+
基础类型快照使用 `Object.is`。`Map`、`Set`、类实例及其他非普通对象按整体引用处理。Store 和 External Store 快照必须保持不可变:每次可观察变化都发布新引用。
|
|
148
|
+
|
|
149
|
+
不要保存、返回、展开、序列化或传递无 selector 的完整结果;应立即读取属性,通常直接解构。Effect 与 `useEffectEvent` 可以通过 `useInstance()` 做命令式读取,但参与渲染的状态必须使用订阅 Hook;也不要把 Effect Event 暴露成公共 Store action。
|
|
137
150
|
|
|
138
151
|
## 多个实例
|
|
139
152
|
|
|
@@ -159,7 +172,7 @@ Kerros 简单、轻量、可靠。先把状态写成普通 Hook,需要共享
|
|
|
159
172
|
|
|
160
173
|
```tsx
|
|
161
174
|
function useTaskModel() {
|
|
162
|
-
const { user } = useAccount(
|
|
175
|
+
const { user } = useAccount()
|
|
163
176
|
const [tasks, setTasks] = useState<Task[]>([])
|
|
164
177
|
|
|
165
178
|
const addTask = (title: string) => {
|
|
@@ -219,12 +232,13 @@ const [useCounter, CounterProvider] = createStore(useCounterModel)
|
|
|
219
232
|
```ts
|
|
220
233
|
function createStore<TStore, TProps = Record<never, never>>(
|
|
221
234
|
useModel: (props: TProps) => TStore,
|
|
235
|
+
options?: { tracking?: boolean },
|
|
222
236
|
): readonly [StoreHook<TStore>, StoreProvider<TProps>]
|
|
223
237
|
```
|
|
224
238
|
|
|
225
239
|
- `useModel` 必须遵守 Hooks 规则
|
|
226
240
|
- 除 `children` 外的 Provider props 会传给 `useModel`
|
|
227
|
-
- Store Hook
|
|
241
|
+
- Store Hook 可不传参数使用自动追踪,也可传入返回对象的 selector
|
|
228
242
|
- 在对应 Provider 外调用会抛出明确错误
|
|
229
243
|
- 支持 Strict Mode、服务端渲染和 Provider 多实例
|
|
230
244
|
|
|
@@ -250,6 +264,24 @@ Provider 的 Context 只保存原 Store 实例,组件直接订阅它;Kerros
|
|
|
250
264
|
|
|
251
265
|
React 17 使用官方 `use-sync-external-store` shim;React 18 和 19 可用时优先使用 React 原生实现。React Compiler 不是必需项。
|
|
252
266
|
|
|
267
|
+
## ESLint 防护规则
|
|
268
|
+
|
|
269
|
+
建议安装独立的类型感知插件,并默认使用最严格配置:
|
|
270
|
+
|
|
271
|
+
```sh
|
|
272
|
+
npm install --save-dev @violetflux/eslint-plugin-kerros @typescript-eslint/parser
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
```js
|
|
276
|
+
import kerros from '@violetflux/eslint-plugin-kerros'
|
|
277
|
+
|
|
278
|
+
export default [kerros.configs.recommendedTypeChecked]
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
`recommendedTypeChecked` 把全部 17 条规则设为 error,并启用 TypeScript `projectService`。超大型仓库可改用 `kerros.configs.fastTypeChecked`:它仍然通过类型识别真实 Kerros Hook,只关闭最昂贵的全程序与深层分析。请参考[真实 ESLint 压测](https://github.com/violetflux/kerros/blob/main/benchmarks/eslint/RESULTS.md);fast 是性能取舍,不是不可靠的命名降级。插件首版只分析完整 TS/TSX 文件,不分析不完整 Markdown 代码块。
|
|
282
|
+
|
|
283
|
+
维护者还需要分别为 `@violetflux/kerros` 和 `@violetflux/eslint-plugin-kerros` 配置 npm Trusted Publisher。这是唯一的仓库外发布步骤;仓库内工作流会先检查并发布运行库,再发布插件。
|
|
284
|
+
|
|
253
285
|
## 文档
|
|
254
286
|
|
|
255
287
|
- [介绍](https://violetflux.github.io/kerros/zh/guide/introduction)
|
package/dist/index.cjs
CHANGED
|
@@ -1,14 +1,79 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
let react = require("react");
|
|
3
|
+
let proxy_compare = require("proxy-compare");
|
|
3
4
|
let use_sync_external_store_shim_with_selector = require("use-sync-external-store/shim/with-selector");
|
|
5
|
+
//#region src/tracking.ts
|
|
6
|
+
const useStoreLayoutEffect$1 = typeof window === "undefined" ? react.useEffect : react.useLayoutEffect;
|
|
7
|
+
/**
|
|
8
|
+
* Subscribe through either an explicit selector, shallow snapshots, or render access tracking
|
|
9
|
+
*/
|
|
10
|
+
function useStoreValue(store, selector, tracking) {
|
|
11
|
+
const committedTracking = (0, react.useRef)(void 0);
|
|
12
|
+
const [proxyCache] = (0, react.useState)(() => /* @__PURE__ */ new WeakMap());
|
|
13
|
+
const [calibration, calibrate] = (0, react.useReducer)((previous, snapshot) => ({
|
|
14
|
+
snapshot,
|
|
15
|
+
version: (previous?.version ?? 0) + 1
|
|
16
|
+
}), void 0);
|
|
17
|
+
const selectSnapshot = (0, react.useCallback)((snapshot) => {
|
|
18
|
+
const currentSnapshot = calibration && Object.is(calibration.snapshot, snapshot) ? calibration.snapshot : snapshot;
|
|
19
|
+
return selector ? selector(currentSnapshot) : currentSnapshot;
|
|
20
|
+
}, [calibration, selector]);
|
|
21
|
+
const compareSelections = (0, react.useCallback)((previous, next) => {
|
|
22
|
+
if (selector || !tracking) return shallowEqual(previous, next);
|
|
23
|
+
const committed = committedTracking.current;
|
|
24
|
+
if (!committed) return Object.is(previous, next);
|
|
25
|
+
return !(0, proxy_compare.isChanged)(committed.snapshot, next, committed.affected, /* @__PURE__ */ new WeakMap());
|
|
26
|
+
}, [selector, tracking]);
|
|
27
|
+
const snapshot = (0, use_sync_external_store_shim_with_selector.useSyncExternalStoreWithSelector)(store.subscribe, store.getSnapshot, store.getSnapshot, selectSnapshot, compareSelections);
|
|
28
|
+
const affected = /* @__PURE__ */ new WeakMap();
|
|
29
|
+
const shouldTrack = !selector && tracking;
|
|
30
|
+
const value = shouldTrack ? (0, proxy_compare.createProxy)(snapshot, affected, proxyCache) : snapshot;
|
|
31
|
+
useStoreLayoutEffect$1(() => {
|
|
32
|
+
if (shouldTrack) {
|
|
33
|
+
const renderedSnapshot = snapshot;
|
|
34
|
+
committedTracking.current = {
|
|
35
|
+
affected,
|
|
36
|
+
snapshot: renderedSnapshot
|
|
37
|
+
};
|
|
38
|
+
const currentSnapshot = store.getSnapshot();
|
|
39
|
+
if ((0, proxy_compare.isChanged)(renderedSnapshot, currentSnapshot, affected, /* @__PURE__ */ new WeakMap())) calibrate(currentSnapshot);
|
|
40
|
+
}
|
|
41
|
+
}, [
|
|
42
|
+
affected,
|
|
43
|
+
shouldTrack,
|
|
44
|
+
snapshot,
|
|
45
|
+
store
|
|
46
|
+
]);
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Compare values by identity first and enumerable top-level fields second
|
|
51
|
+
*/
|
|
52
|
+
function shallowEqual(left, right) {
|
|
53
|
+
if (Object.is(left, right)) return true;
|
|
54
|
+
if (!isShallowComparable(left) || !isShallowComparable(right)) return false;
|
|
55
|
+
const leftKeys = Object.keys(left);
|
|
56
|
+
if (leftKeys.length !== Object.keys(right).length) return false;
|
|
57
|
+
return leftKeys.every((key) => Object.prototype.hasOwnProperty.call(right, key) && Object.is(left[key], right[key]));
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Narrow values before enumerable field comparison
|
|
61
|
+
*/
|
|
62
|
+
function isShallowComparable(value) {
|
|
63
|
+
if (typeof value !== "object" || value === null) return false;
|
|
64
|
+
const prototype = Object.getPrototypeOf(value);
|
|
65
|
+
return prototype === Object.prototype || prototype === Array.prototype;
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
4
68
|
//#region src/index.tsx
|
|
5
69
|
const useStoreLayoutEffect = typeof window === "undefined" ? react.useEffect : react.useLayoutEffect;
|
|
6
70
|
/**
|
|
7
|
-
* Create a
|
|
71
|
+
* Create a React Store with automatic tracking and explicit selector support
|
|
8
72
|
*/
|
|
9
|
-
function createStore(useModel) {
|
|
73
|
+
function createStore(useModel, options) {
|
|
10
74
|
const StoreContext = (0, react.createContext)(void 0);
|
|
11
75
|
const storeName = useModel.name || "KerrosStore";
|
|
76
|
+
const tracking = options?.tracking ?? true;
|
|
12
77
|
/** Run the model Hook and publish its committed snapshot */
|
|
13
78
|
const StoreProvider = (props) => {
|
|
14
79
|
const { children, ...storeProps } = props;
|
|
@@ -20,29 +85,28 @@ function createStore(useModel) {
|
|
|
20
85
|
StoreProvider.displayName = `${storeName}Provider`;
|
|
21
86
|
StoreContext.displayName = `${storeName}Context`;
|
|
22
87
|
/** Select Store fields through the stable Provider container */
|
|
23
|
-
const useStore = (selector) => {
|
|
24
|
-
return
|
|
25
|
-
};
|
|
88
|
+
const useStore = ((selector) => {
|
|
89
|
+
return useStoreValue(useStoreContext(StoreContext), selector, tracking);
|
|
90
|
+
});
|
|
26
91
|
return [useStore, StoreProvider];
|
|
27
92
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
function bindStore(name = "KerrosExternalStore") {
|
|
93
|
+
function bindStore(nameOrOptions = "KerrosExternalStore", inputOptions) {
|
|
94
|
+
const name = typeof nameOrOptions === "string" ? nameOrOptions : "KerrosExternalStore";
|
|
95
|
+
const tracking = (typeof nameOrOptions === "string" ? inputOptions : nameOrOptions)?.tracking ?? true;
|
|
32
96
|
const StoreContext = (0, react.createContext)(void 0);
|
|
33
97
|
/** Provide one existing Store instance without copying its snapshot */
|
|
34
|
-
const StoreProvider = (props) => {
|
|
98
|
+
const StoreProvider = ((props) => {
|
|
35
99
|
const { children, store } = props;
|
|
36
100
|
return (0, react.createElement)(StoreContext.Provider, { value: store }, children);
|
|
37
|
-
};
|
|
101
|
+
});
|
|
38
102
|
StoreProvider.displayName = `${name}Provider`;
|
|
39
103
|
StoreContext.displayName = `${name}Context`;
|
|
40
104
|
/** Select snapshot fields directly from the bound external Store */
|
|
41
|
-
const useStore = (selector) => {
|
|
42
|
-
return
|
|
43
|
-
};
|
|
105
|
+
const useStore = ((selector) => {
|
|
106
|
+
return useStoreValue(useStoreContext(StoreContext), selector, tracking);
|
|
107
|
+
});
|
|
44
108
|
/** Read the exact Store instance bound to the current Provider */
|
|
45
|
-
const useInstance = () => useStoreContext(StoreContext);
|
|
109
|
+
const useInstance = (() => useStoreContext(StoreContext));
|
|
46
110
|
return [
|
|
47
111
|
useStore,
|
|
48
112
|
StoreProvider,
|
|
@@ -76,21 +140,6 @@ function useStoreContext(context) {
|
|
|
76
140
|
if (!store) throw new Error("Kerros store hook must be used within its matching Provider");
|
|
77
141
|
return store;
|
|
78
142
|
}
|
|
79
|
-
/**
|
|
80
|
-
* Select fields directly from an external Store subscription
|
|
81
|
-
*/
|
|
82
|
-
function useStoreSelector(store, selector) {
|
|
83
|
-
return (0, use_sync_external_store_shim_with_selector.useSyncExternalStoreWithSelector)(store.subscribe, store.getSnapshot, store.getSnapshot, selector, shallowEqual);
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Compare selector objects by their enumerable top-level fields
|
|
87
|
-
*/
|
|
88
|
-
function shallowEqual(left, right) {
|
|
89
|
-
if (Object.is(left, right)) return true;
|
|
90
|
-
const leftKeys = Object.keys(left);
|
|
91
|
-
if (leftKeys.length !== Object.keys(right).length) return false;
|
|
92
|
-
return leftKeys.every((key) => Object.prototype.hasOwnProperty.call(right, key) && Object.is(left[key], right[key]));
|
|
93
|
-
}
|
|
94
143
|
//#endregion
|
|
95
144
|
exports.bindStore = bindStore;
|
|
96
145
|
exports.createStore = createStore;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,13 +1,37 @@
|
|
|
1
1
|
import { FC, PropsWithChildren } from "react";
|
|
2
2
|
//#region src/index.d.ts
|
|
3
|
+
declare const storeHookMarker: unique symbol;
|
|
4
|
+
declare const storeInstanceHookMarker: unique symbol;
|
|
5
|
+
declare const externalStoreProviderMarker: unique symbol;
|
|
6
|
+
/** Store behavior options */
|
|
7
|
+
interface StoreOptions {
|
|
8
|
+
/** Automatically track properties read by selector-free Store hooks */
|
|
9
|
+
tracking?: boolean;
|
|
10
|
+
}
|
|
3
11
|
/** Store selector returning an object compared with shallow equality */
|
|
4
12
|
type StoreSelector<TStore, TSelection extends object> = (store: TStore) => TSelection;
|
|
5
13
|
/** Hook used by consumers to select Store fields */
|
|
6
14
|
interface StoreHook<TStore> {
|
|
15
|
+
/** Type-only Store hook identity */
|
|
16
|
+
readonly [storeHookMarker]: TStore;
|
|
17
|
+
(): TStore;
|
|
7
18
|
<TSelection extends object>(selector: StoreSelector<TStore, TSelection>): TSelection;
|
|
8
19
|
}
|
|
9
20
|
/** Provider created for a Store hook */
|
|
10
21
|
type StoreProvider<TProps> = FC<PropsWithChildren<TProps>>;
|
|
22
|
+
/** Hook returning the exact external Store instance */
|
|
23
|
+
interface StoreInstanceHook<TStore> {
|
|
24
|
+
/** Type-only Store instance hook identity */
|
|
25
|
+
readonly [storeInstanceHookMarker]: TStore;
|
|
26
|
+
(): TStore;
|
|
27
|
+
}
|
|
28
|
+
/** Provider carrying an existing external Store instance */
|
|
29
|
+
type ExternalStoreProvider<TStore> = StoreProvider<{
|
|
30
|
+
store: TStore;
|
|
31
|
+
}> & {
|
|
32
|
+
/** Type-only external Store Provider identity */
|
|
33
|
+
readonly [externalStoreProviderMarker]: TStore;
|
|
34
|
+
};
|
|
11
35
|
/** Existing external Store contract supported by bindStore */
|
|
12
36
|
interface ExternalStore<TSnapshot> {
|
|
13
37
|
/** Read the current immutable snapshot */
|
|
@@ -18,16 +42,15 @@ interface ExternalStore<TSnapshot> {
|
|
|
18
42
|
/** Extract the snapshot exposed by an external Store */
|
|
19
43
|
type ExternalStoreSnapshot<TStore> = TStore extends ExternalStore<infer TSnapshot> ? TSnapshot : never;
|
|
20
44
|
/** React bindings created for an existing external Store type */
|
|
21
|
-
type StoreBinding<TStore extends ExternalStore<TSnapshot>, TSnapshot = ExternalStoreSnapshot<TStore>> = readonly [StoreHook<TSnapshot>,
|
|
22
|
-
store: TStore;
|
|
23
|
-
}>, () => TStore];
|
|
45
|
+
type StoreBinding<TStore extends ExternalStore<TSnapshot>, TSnapshot = ExternalStoreSnapshot<TStore>> = readonly [StoreHook<TSnapshot>, ExternalStoreProvider<TStore>, StoreInstanceHook<TStore>];
|
|
24
46
|
/**
|
|
25
|
-
* Create a
|
|
47
|
+
* Create a React Store with automatic tracking and explicit selector support
|
|
26
48
|
*/
|
|
27
|
-
declare function createStore<TStore, TProps = Record<never, never>>(useModel: (props: TProps) => TStore): readonly [StoreHook<TStore>, StoreProvider<TProps>];
|
|
49
|
+
declare function createStore<TStore, TProps = Record<never, never>>(useModel: (props: TProps) => TStore, options?: StoreOptions): readonly [StoreHook<TStore>, StoreProvider<TProps>];
|
|
28
50
|
/**
|
|
29
51
|
* Bind existing external Store instances to scoped React consumers
|
|
30
52
|
*/
|
|
31
|
-
declare function bindStore<TStore extends ExternalStore<TSnapshot>, TSnapshot = ExternalStoreSnapshot<TStore>>(
|
|
53
|
+
declare function bindStore<TStore extends ExternalStore<TSnapshot>, TSnapshot = ExternalStoreSnapshot<TStore>>(options?: StoreOptions): StoreBinding<TStore, TSnapshot>;
|
|
54
|
+
declare function bindStore<TStore extends ExternalStore<TSnapshot>, TSnapshot = ExternalStoreSnapshot<TStore>>(name?: string, options?: StoreOptions): StoreBinding<TStore, TSnapshot>;
|
|
32
55
|
//#endregion
|
|
33
|
-
export { ExternalStore, ExternalStoreSnapshot, StoreBinding, StoreHook, StoreProvider, StoreSelector, bindStore, createStore };
|
|
56
|
+
export { ExternalStore, ExternalStoreSnapshot, StoreBinding, StoreHook, StoreOptions, StoreProvider, StoreSelector, bindStore, createStore };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,13 +1,37 @@
|
|
|
1
1
|
import { FC, PropsWithChildren } from "react";
|
|
2
2
|
//#region src/index.d.ts
|
|
3
|
+
declare const storeHookMarker: unique symbol;
|
|
4
|
+
declare const storeInstanceHookMarker: unique symbol;
|
|
5
|
+
declare const externalStoreProviderMarker: unique symbol;
|
|
6
|
+
/** Store behavior options */
|
|
7
|
+
interface StoreOptions {
|
|
8
|
+
/** Automatically track properties read by selector-free Store hooks */
|
|
9
|
+
tracking?: boolean;
|
|
10
|
+
}
|
|
3
11
|
/** Store selector returning an object compared with shallow equality */
|
|
4
12
|
type StoreSelector<TStore, TSelection extends object> = (store: TStore) => TSelection;
|
|
5
13
|
/** Hook used by consumers to select Store fields */
|
|
6
14
|
interface StoreHook<TStore> {
|
|
15
|
+
/** Type-only Store hook identity */
|
|
16
|
+
readonly [storeHookMarker]: TStore;
|
|
17
|
+
(): TStore;
|
|
7
18
|
<TSelection extends object>(selector: StoreSelector<TStore, TSelection>): TSelection;
|
|
8
19
|
}
|
|
9
20
|
/** Provider created for a Store hook */
|
|
10
21
|
type StoreProvider<TProps> = FC<PropsWithChildren<TProps>>;
|
|
22
|
+
/** Hook returning the exact external Store instance */
|
|
23
|
+
interface StoreInstanceHook<TStore> {
|
|
24
|
+
/** Type-only Store instance hook identity */
|
|
25
|
+
readonly [storeInstanceHookMarker]: TStore;
|
|
26
|
+
(): TStore;
|
|
27
|
+
}
|
|
28
|
+
/** Provider carrying an existing external Store instance */
|
|
29
|
+
type ExternalStoreProvider<TStore> = StoreProvider<{
|
|
30
|
+
store: TStore;
|
|
31
|
+
}> & {
|
|
32
|
+
/** Type-only external Store Provider identity */
|
|
33
|
+
readonly [externalStoreProviderMarker]: TStore;
|
|
34
|
+
};
|
|
11
35
|
/** Existing external Store contract supported by bindStore */
|
|
12
36
|
interface ExternalStore<TSnapshot> {
|
|
13
37
|
/** Read the current immutable snapshot */
|
|
@@ -18,16 +42,15 @@ interface ExternalStore<TSnapshot> {
|
|
|
18
42
|
/** Extract the snapshot exposed by an external Store */
|
|
19
43
|
type ExternalStoreSnapshot<TStore> = TStore extends ExternalStore<infer TSnapshot> ? TSnapshot : never;
|
|
20
44
|
/** React bindings created for an existing external Store type */
|
|
21
|
-
type StoreBinding<TStore extends ExternalStore<TSnapshot>, TSnapshot = ExternalStoreSnapshot<TStore>> = readonly [StoreHook<TSnapshot>,
|
|
22
|
-
store: TStore;
|
|
23
|
-
}>, () => TStore];
|
|
45
|
+
type StoreBinding<TStore extends ExternalStore<TSnapshot>, TSnapshot = ExternalStoreSnapshot<TStore>> = readonly [StoreHook<TSnapshot>, ExternalStoreProvider<TStore>, StoreInstanceHook<TStore>];
|
|
24
46
|
/**
|
|
25
|
-
* Create a
|
|
47
|
+
* Create a React Store with automatic tracking and explicit selector support
|
|
26
48
|
*/
|
|
27
|
-
declare function createStore<TStore, TProps = Record<never, never>>(useModel: (props: TProps) => TStore): readonly [StoreHook<TStore>, StoreProvider<TProps>];
|
|
49
|
+
declare function createStore<TStore, TProps = Record<never, never>>(useModel: (props: TProps) => TStore, options?: StoreOptions): readonly [StoreHook<TStore>, StoreProvider<TProps>];
|
|
28
50
|
/**
|
|
29
51
|
* Bind existing external Store instances to scoped React consumers
|
|
30
52
|
*/
|
|
31
|
-
declare function bindStore<TStore extends ExternalStore<TSnapshot>, TSnapshot = ExternalStoreSnapshot<TStore>>(
|
|
53
|
+
declare function bindStore<TStore extends ExternalStore<TSnapshot>, TSnapshot = ExternalStoreSnapshot<TStore>>(options?: StoreOptions): StoreBinding<TStore, TSnapshot>;
|
|
54
|
+
declare function bindStore<TStore extends ExternalStore<TSnapshot>, TSnapshot = ExternalStoreSnapshot<TStore>>(name?: string, options?: StoreOptions): StoreBinding<TStore, TSnapshot>;
|
|
32
55
|
//#endregion
|
|
33
|
-
export { ExternalStore, ExternalStoreSnapshot, StoreBinding, StoreHook, StoreProvider, StoreSelector, bindStore, createStore };
|
|
56
|
+
export { ExternalStore, ExternalStoreSnapshot, StoreBinding, StoreHook, StoreOptions, StoreProvider, StoreSelector, bindStore, createStore };
|
package/dist/index.mjs
CHANGED
|
@@ -1,13 +1,78 @@
|
|
|
1
|
-
import { createContext, createElement, useContext, useEffect, useLayoutEffect, useState } from "react";
|
|
1
|
+
import { createContext, createElement, useCallback, useContext, useEffect, useLayoutEffect, useReducer, useRef, useState } from "react";
|
|
2
|
+
import { createProxy, isChanged } from "proxy-compare";
|
|
2
3
|
import { useSyncExternalStoreWithSelector } from "use-sync-external-store/shim/with-selector";
|
|
4
|
+
//#region src/tracking.ts
|
|
5
|
+
const useStoreLayoutEffect$1 = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
6
|
+
/**
|
|
7
|
+
* Subscribe through either an explicit selector, shallow snapshots, or render access tracking
|
|
8
|
+
*/
|
|
9
|
+
function useStoreValue(store, selector, tracking) {
|
|
10
|
+
const committedTracking = useRef(void 0);
|
|
11
|
+
const [proxyCache] = useState(() => /* @__PURE__ */ new WeakMap());
|
|
12
|
+
const [calibration, calibrate] = useReducer((previous, snapshot) => ({
|
|
13
|
+
snapshot,
|
|
14
|
+
version: (previous?.version ?? 0) + 1
|
|
15
|
+
}), void 0);
|
|
16
|
+
const selectSnapshot = useCallback((snapshot) => {
|
|
17
|
+
const currentSnapshot = calibration && Object.is(calibration.snapshot, snapshot) ? calibration.snapshot : snapshot;
|
|
18
|
+
return selector ? selector(currentSnapshot) : currentSnapshot;
|
|
19
|
+
}, [calibration, selector]);
|
|
20
|
+
const compareSelections = useCallback((previous, next) => {
|
|
21
|
+
if (selector || !tracking) return shallowEqual(previous, next);
|
|
22
|
+
const committed = committedTracking.current;
|
|
23
|
+
if (!committed) return Object.is(previous, next);
|
|
24
|
+
return !isChanged(committed.snapshot, next, committed.affected, /* @__PURE__ */ new WeakMap());
|
|
25
|
+
}, [selector, tracking]);
|
|
26
|
+
const snapshot = useSyncExternalStoreWithSelector(store.subscribe, store.getSnapshot, store.getSnapshot, selectSnapshot, compareSelections);
|
|
27
|
+
const affected = /* @__PURE__ */ new WeakMap();
|
|
28
|
+
const shouldTrack = !selector && tracking;
|
|
29
|
+
const value = shouldTrack ? createProxy(snapshot, affected, proxyCache) : snapshot;
|
|
30
|
+
useStoreLayoutEffect$1(() => {
|
|
31
|
+
if (shouldTrack) {
|
|
32
|
+
const renderedSnapshot = snapshot;
|
|
33
|
+
committedTracking.current = {
|
|
34
|
+
affected,
|
|
35
|
+
snapshot: renderedSnapshot
|
|
36
|
+
};
|
|
37
|
+
const currentSnapshot = store.getSnapshot();
|
|
38
|
+
if (isChanged(renderedSnapshot, currentSnapshot, affected, /* @__PURE__ */ new WeakMap())) calibrate(currentSnapshot);
|
|
39
|
+
}
|
|
40
|
+
}, [
|
|
41
|
+
affected,
|
|
42
|
+
shouldTrack,
|
|
43
|
+
snapshot,
|
|
44
|
+
store
|
|
45
|
+
]);
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Compare values by identity first and enumerable top-level fields second
|
|
50
|
+
*/
|
|
51
|
+
function shallowEqual(left, right) {
|
|
52
|
+
if (Object.is(left, right)) return true;
|
|
53
|
+
if (!isShallowComparable(left) || !isShallowComparable(right)) return false;
|
|
54
|
+
const leftKeys = Object.keys(left);
|
|
55
|
+
if (leftKeys.length !== Object.keys(right).length) return false;
|
|
56
|
+
return leftKeys.every((key) => Object.prototype.hasOwnProperty.call(right, key) && Object.is(left[key], right[key]));
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Narrow values before enumerable field comparison
|
|
60
|
+
*/
|
|
61
|
+
function isShallowComparable(value) {
|
|
62
|
+
if (typeof value !== "object" || value === null) return false;
|
|
63
|
+
const prototype = Object.getPrototypeOf(value);
|
|
64
|
+
return prototype === Object.prototype || prototype === Array.prototype;
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
3
67
|
//#region src/index.tsx
|
|
4
68
|
const useStoreLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
5
69
|
/**
|
|
6
|
-
* Create a
|
|
70
|
+
* Create a React Store with automatic tracking and explicit selector support
|
|
7
71
|
*/
|
|
8
|
-
function createStore(useModel) {
|
|
72
|
+
function createStore(useModel, options) {
|
|
9
73
|
const StoreContext = createContext(void 0);
|
|
10
74
|
const storeName = useModel.name || "KerrosStore";
|
|
75
|
+
const tracking = options?.tracking ?? true;
|
|
11
76
|
/** Run the model Hook and publish its committed snapshot */
|
|
12
77
|
const StoreProvider = (props) => {
|
|
13
78
|
const { children, ...storeProps } = props;
|
|
@@ -19,29 +84,28 @@ function createStore(useModel) {
|
|
|
19
84
|
StoreProvider.displayName = `${storeName}Provider`;
|
|
20
85
|
StoreContext.displayName = `${storeName}Context`;
|
|
21
86
|
/** Select Store fields through the stable Provider container */
|
|
22
|
-
const useStore = (selector) => {
|
|
23
|
-
return
|
|
24
|
-
};
|
|
87
|
+
const useStore = ((selector) => {
|
|
88
|
+
return useStoreValue(useStoreContext(StoreContext), selector, tracking);
|
|
89
|
+
});
|
|
25
90
|
return [useStore, StoreProvider];
|
|
26
91
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
function bindStore(name = "KerrosExternalStore") {
|
|
92
|
+
function bindStore(nameOrOptions = "KerrosExternalStore", inputOptions) {
|
|
93
|
+
const name = typeof nameOrOptions === "string" ? nameOrOptions : "KerrosExternalStore";
|
|
94
|
+
const tracking = (typeof nameOrOptions === "string" ? inputOptions : nameOrOptions)?.tracking ?? true;
|
|
31
95
|
const StoreContext = createContext(void 0);
|
|
32
96
|
/** Provide one existing Store instance without copying its snapshot */
|
|
33
|
-
const StoreProvider = (props) => {
|
|
97
|
+
const StoreProvider = ((props) => {
|
|
34
98
|
const { children, store } = props;
|
|
35
99
|
return createElement(StoreContext.Provider, { value: store }, children);
|
|
36
|
-
};
|
|
100
|
+
});
|
|
37
101
|
StoreProvider.displayName = `${name}Provider`;
|
|
38
102
|
StoreContext.displayName = `${name}Context`;
|
|
39
103
|
/** Select snapshot fields directly from the bound external Store */
|
|
40
|
-
const useStore = (selector) => {
|
|
41
|
-
return
|
|
42
|
-
};
|
|
104
|
+
const useStore = ((selector) => {
|
|
105
|
+
return useStoreValue(useStoreContext(StoreContext), selector, tracking);
|
|
106
|
+
});
|
|
43
107
|
/** Read the exact Store instance bound to the current Provider */
|
|
44
|
-
const useInstance = () => useStoreContext(StoreContext);
|
|
108
|
+
const useInstance = (() => useStoreContext(StoreContext));
|
|
45
109
|
return [
|
|
46
110
|
useStore,
|
|
47
111
|
StoreProvider,
|
|
@@ -75,20 +139,5 @@ function useStoreContext(context) {
|
|
|
75
139
|
if (!store) throw new Error("Kerros store hook must be used within its matching Provider");
|
|
76
140
|
return store;
|
|
77
141
|
}
|
|
78
|
-
/**
|
|
79
|
-
* Select fields directly from an external Store subscription
|
|
80
|
-
*/
|
|
81
|
-
function useStoreSelector(store, selector) {
|
|
82
|
-
return useSyncExternalStoreWithSelector(store.subscribe, store.getSnapshot, store.getSnapshot, selector, shallowEqual);
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Compare selector objects by their enumerable top-level fields
|
|
86
|
-
*/
|
|
87
|
-
function shallowEqual(left, right) {
|
|
88
|
-
if (Object.is(left, right)) return true;
|
|
89
|
-
const leftKeys = Object.keys(left);
|
|
90
|
-
if (leftKeys.length !== Object.keys(right).length) return false;
|
|
91
|
-
return leftKeys.every((key) => Object.prototype.hasOwnProperty.call(right, key) && Object.is(left[key], right[key]));
|
|
92
|
-
}
|
|
93
142
|
//#endregion
|
|
94
143
|
export { bindStore, createStore };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@violetflux/kerros",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "Hook-native state sharing for React with focused
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Hook-native state sharing for React with automatic access tracking and focused selectors.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
7
7
|
"react-hooks",
|
|
@@ -26,6 +26,9 @@
|
|
|
26
26
|
"license": "MIT",
|
|
27
27
|
"author": "Violetflux",
|
|
28
28
|
"type": "module",
|
|
29
|
+
"workspaces": [
|
|
30
|
+
"packages/*"
|
|
31
|
+
],
|
|
29
32
|
"sideEffects": false,
|
|
30
33
|
"files": [
|
|
31
34
|
"dist",
|
|
@@ -47,17 +50,27 @@
|
|
|
47
50
|
}
|
|
48
51
|
},
|
|
49
52
|
"scripts": {
|
|
50
|
-
"build": "
|
|
53
|
+
"build": "bun run build:runtime && bun run build:plugin",
|
|
54
|
+
"build:runtime": "tsdown",
|
|
55
|
+
"build:plugin": "bun run --filter @violetflux/eslint-plugin-kerros build",
|
|
56
|
+
"benchmark:eslint": "bun benchmarks/eslint/run.ts",
|
|
57
|
+
"benchmark:tracking": "bun benchmarks/tracking/runtime.tsx",
|
|
51
58
|
"dev": "rspress dev",
|
|
52
59
|
"docs:build": "rspress build",
|
|
53
60
|
"docs:preview": "rspress preview",
|
|
54
61
|
"docs:check": "bun scripts/check-docs.ts",
|
|
62
|
+
"quality:check": "bun scripts/check-selectors.ts",
|
|
55
63
|
"lint": "eslint .",
|
|
56
|
-
"typecheck": "
|
|
57
|
-
"
|
|
64
|
+
"typecheck": "bun run typecheck:runtime && bun run typecheck:plugin",
|
|
65
|
+
"typecheck:runtime": "tsc --noEmit",
|
|
66
|
+
"typecheck:plugin": "bun run --filter @violetflux/eslint-plugin-kerros typecheck",
|
|
67
|
+
"test": "bun run test:runtime && bun run test:plugin",
|
|
68
|
+
"test:runtime": "vitest run",
|
|
69
|
+
"test:plugin": "bun run --filter @violetflux/eslint-plugin-kerros test",
|
|
70
|
+
"test:benchmarks": "vitest run --config benchmarks/vitest.config.ts",
|
|
58
71
|
"test:watch": "vitest",
|
|
59
|
-
"check": "bun run lint && bun run typecheck && bun run test && bun run build && bun run docs:check && bun run docs:build",
|
|
60
|
-
"prepublishOnly": "bun run lint && bun run typecheck && bun run test && bun run build"
|
|
72
|
+
"check": "bun run quality:check && bun run lint && bun run typecheck && bun run test && bun run build && bun run docs:check && bun run docs:build",
|
|
73
|
+
"prepublishOnly": "bun run quality:check && bun run lint && bun run typecheck && bun run test && bun run build"
|
|
61
74
|
},
|
|
62
75
|
"publishConfig": {
|
|
63
76
|
"access": "public"
|
|
@@ -66,11 +79,13 @@
|
|
|
66
79
|
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
|
67
80
|
},
|
|
68
81
|
"dependencies": {
|
|
82
|
+
"proxy-compare": "3.0.1",
|
|
69
83
|
"use-sync-external-store": "1.6.0"
|
|
70
84
|
},
|
|
71
85
|
"devDependencies": {
|
|
72
86
|
"@eslint/js": "10.0.1",
|
|
73
87
|
"@rspress/core": "2.0.17",
|
|
88
|
+
"@types/jsdom": "28.0.3",
|
|
74
89
|
"@types/node": "26.1.1",
|
|
75
90
|
"@types/react": "19.2.17",
|
|
76
91
|
"@types/react-dom": "19.2.3",
|
|
@@ -81,6 +96,7 @@
|
|
|
81
96
|
"jsdom": "29.1.1",
|
|
82
97
|
"react": "19.2.7",
|
|
83
98
|
"react-dom": "19.2.7",
|
|
99
|
+
"tinybench": "6.1.2",
|
|
84
100
|
"tsdown": "0.22.9",
|
|
85
101
|
"typescript": "5.9.3",
|
|
86
102
|
"typescript-eslint": "8.64.0",
|
package/skills/kerros/SKILL.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: kerros
|
|
3
|
-
description:
|
|
3
|
+
description: Use when implementing, refactoring, reviewing, or testing shared React state with @violetflux/kerros, including createStore, bindStore, automatic property tracking, Provider scoping, explicit selectors, external Stores, cross-Store composition, Hox or Context migrations, and React 17–19 compatibility.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Kerros
|
|
7
7
|
|
|
8
|
-
Build shared state from ordinary React Hooks. Keep Provider scope and multiple instances while
|
|
8
|
+
Build shared state from ordinary React Hooks. Keep Provider scope and multiple instances while automatic property tracking prevents unrelated consumers from rerendering by default.
|
|
9
9
|
|
|
10
10
|
## Workflow
|
|
11
11
|
|
|
@@ -13,7 +13,7 @@ Build shared state from ordinary React Hooks. Keep Provider scope and multiple i
|
|
|
13
13
|
2. Keep state local when only one component needs it. Create a Kerros Store only when several components need the same Hook state.
|
|
14
14
|
3. Group state by domain and identify one authoritative owner for every mutable value.
|
|
15
15
|
4. Use `createStore` by default. Use the advanced `bindStore` API only when an authoritative headless external Store already exists.
|
|
16
|
-
5. Mount the Provider at the narrowest shared ancestor and
|
|
16
|
+
5. Mount the Provider at the narrowest shared ancestor and let consumers immediately destructure the fields they read.
|
|
17
17
|
6. Order composed Providers from dependency to dependent and reject circular Store dependencies.
|
|
18
18
|
7. Run the project's typecheck, tests, lint, and the narrowest useful render test.
|
|
19
19
|
|
|
@@ -60,20 +60,24 @@ function App() {
|
|
|
60
60
|
}
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
Call the Store Hook without a selector and immediately destructure the fields used by the component:
|
|
64
64
|
|
|
65
65
|
```tsx
|
|
66
66
|
function Counter() {
|
|
67
|
-
const { count, increment } = useCounter(
|
|
68
|
-
count: s.count,
|
|
69
|
-
increment: s.increment,
|
|
70
|
-
}))
|
|
67
|
+
const { count, increment } = useCounter()
|
|
71
68
|
|
|
72
69
|
return <button onClick={increment}>{count}</button>
|
|
73
70
|
}
|
|
74
71
|
```
|
|
75
72
|
|
|
76
|
-
Kerros
|
|
73
|
+
Kerros automatically tracks object, array, and nested property reads made during render. An update to an unread field must not rerender this component; Kerros does not deep-compare the complete Store.
|
|
74
|
+
|
|
75
|
+
## Subscription modes
|
|
76
|
+
|
|
77
|
+
- `useStore()` is the default. Read properties immediately, normally through destructuring. Do not save, return, spread, serialize, or pass the complete tracked result.
|
|
78
|
+
- `useStore(s => ({ ... }))` is the advanced path for derived values or measured hot spots. Keep the selector inline, name its parameter `s`, and return an object whose top-level fields are shallowly compared with `Object.is`.
|
|
79
|
+
- `createStore(model, { tracking: false })` and `bindStore({ tracking: false })` disable automatic tracking for selector-free calls and compare the complete Store at the top level instead.
|
|
80
|
+
- Primitive Store snapshots use `Object.is`. `Map`, `Set`, class instances, and other atomic objects are tracked by reference as a whole.
|
|
77
81
|
|
|
78
82
|
## Advanced external Store binding
|
|
79
83
|
|
|
@@ -97,7 +101,7 @@ Mount the original instance without mirroring its snapshot:
|
|
|
97
101
|
</StreamBindingProvider>
|
|
98
102
|
```
|
|
99
103
|
|
|
100
|
-
Use `useStream` with
|
|
104
|
+
Use `useStream()` with immediate property access for ordinary snapshot reads; use an explicit selector only for derived values or measured hot spots. Use `useStreamInstance()` only in Provider descendants that need imperative commands or must supply the current instance to another headless service. It reads Context without subscribing to snapshots, so never use `useStreamInstance().getSnapshot()` for rendered state.
|
|
101
105
|
|
|
102
106
|
Keep creation, start, stop, and disposal in the owner that creates the instance. If that owner already has the instance, use it directly instead of calling the instance Hook.
|
|
103
107
|
|
|
@@ -108,7 +112,7 @@ Do not replace this with `createStore(() => useSyncExternalStore(...))`; that su
|
|
|
108
112
|
- Generate `useXxxModel` as a top-level function by default. Anonymous initializers remain valid at runtime, but React Compiler `infer` mode does not automatically recognize and compile them as Hooks.
|
|
109
113
|
- Let the Store producer own action identity. React Compiler may stabilize ordinary returned actions; without Compiler support, use `useCallback` only when a consumer or effect requires a stable action reference.
|
|
110
114
|
- Do not claim that Kerros or `use-context-selector` can determine whether two newly allocated functions are semantically equivalent. Both can compare references, not function behavior.
|
|
111
|
-
-
|
|
115
|
+
- For explicit selectors, Kerros shallowly compares the selected object's top-level fields; `use-context-selector` applies `Object.is` to the selector result, so a newly allocated object is different.
|
|
112
116
|
- Do not recommend migrating to `use-context-selector` merely to avoid Context-wide rerenders. Kerros already keeps a stable Context container and publishes committed snapshots through `useSyncExternalStoreWithSelector`.
|
|
113
117
|
|
|
114
118
|
## Provider props
|
|
@@ -141,7 +145,7 @@ function useSessionModel() {
|
|
|
141
145
|
const [useSession, SessionProvider] = createStore(useSessionModel)
|
|
142
146
|
|
|
143
147
|
function usePermissionsModel() {
|
|
144
|
-
const { userId } = useSession(
|
|
148
|
+
const { userId } = useSession()
|
|
145
149
|
return { canEdit: Boolean(userId) }
|
|
146
150
|
}
|
|
147
151
|
|
|
@@ -159,8 +163,8 @@ function Providers({ children }: PropsWithChildren) {
|
|
|
159
163
|
## Guardrails
|
|
160
164
|
|
|
161
165
|
- Use `createStore` for state owned by a React Hook. Treat `bindStore` as an advanced adapter for an already-authoritative headless Store; do not mirror that snapshot through another Hook Store.
|
|
162
|
-
-
|
|
163
|
-
-
|
|
166
|
+
- Prefer selector-free reads with immediate destructuring. Do not let the tracked result escape render through saving, returning, spreading, serializing, or passing it as an argument.
|
|
167
|
+
- When an explicit selector is justified, return an object of concrete fields and actions. Do not use array selectors or select the complete Store.
|
|
164
168
|
- Do not wrap inline selectors with `useCallback`; Kerros handles selector identity.
|
|
165
169
|
- Keep public actions as ordinary functions unless their reference stability is an explicit producer-side requirement. In React 19, use `useEffectEvent` only for events called from Effects, never as a public Store action.
|
|
166
170
|
- Do not mirror the same mutable state across Stores. Read it from its authoritative Store or move ownership.
|
|
@@ -172,8 +176,8 @@ function Providers({ children }: PropsWithChildren) {
|
|
|
172
176
|
|
|
173
177
|
## Migrate existing state
|
|
174
178
|
|
|
175
|
-
- From React Context: keep the Provider boundary, move the changing value into `createStore`, and replace broad `useContext` reads with
|
|
176
|
-
- From Hox: replace the factory with `createStore`, add the explicit Provider, remove compatibility exports, and migrate
|
|
179
|
+
- From React Context: keep the Provider boundary, move the changing value into `createStore`, and replace broad `useContext` reads with selector-free tracked access.
|
|
180
|
+
- From Hox: replace the factory with `createStore`, add the explicit Provider, remove compatibility exports, and migrate consumers to immediate selector-free access.
|
|
177
181
|
- From a global Store: split by domain only when ownership and dependencies stay clear; do not split merely by field count.
|
|
178
182
|
|
|
179
183
|
## Verify
|
|
@@ -181,6 +185,6 @@ function Providers({ children }: PropsWithChildren) {
|
|
|
181
185
|
- Confirm all consumers are below the correct Provider and multiple Provider instances stay isolated.
|
|
182
186
|
- Search Kerros `createStore` calls and confirm every initializer references a top-level `useXxxModel` function. Search `bindStore` calls and confirm the supplied Store owns a stable immutable snapshot.
|
|
183
187
|
- Test Provider props, Strict Mode, subscription cleanup, and the outside-Provider error when changing Store infrastructure.
|
|
184
|
-
- Add a render-count test showing that changing an
|
|
185
|
-
- Search for broad Store
|
|
188
|
+
- Add a render-count test showing that changing an unread field does not rerender the consumer.
|
|
189
|
+
- Search for tracked-result escape, broad Store reads, whole-Store or array selectors, duplicate subscriptions, and dependency cycles.
|
|
186
190
|
- Run the consuming project's existing validation commands without introducing a new package manager.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
interface:
|
|
2
2
|
display_name: "Kerros"
|
|
3
|
-
short_description: "Build
|
|
4
|
-
default_prompt: "Use $kerros to implement shared React state with createStore or bindStore, scoped Providers, and
|
|
3
|
+
short_description: "Build automatically tracked shared React state with Kerros"
|
|
4
|
+
default_prompt: "Use $kerros to implement shared React state with createStore or bindStore, scoped Providers, and selector-free automatic property tracking by default."
|