@violetflux/kerros 0.1.7 → 0.1.8
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 +11 -3
- package/README.es.md +11 -3
- package/README.fr.md +11 -3
- package/README.ja.md +11 -3
- package/README.ko.md +11 -3
- package/README.md +41 -13
- package/README.zh-CN.md +41 -13
- package/dist/index.cjs +46 -9
- package/dist/index.d.cts +19 -2
- package/dist/index.d.mts +19 -2
- package/dist/index.mjs +46 -10
- package/package.json +1 -1
- package/skills/kerros/SKILL.md +62 -15
- package/skills/kerros/agents/openai.yaml +1 -1
package/README.de.md
CHANGED
|
@@ -34,14 +34,18 @@ Installiere @violetflux/kerros mit dem Paketmanager dieses Projekts und führe d
|
|
|
34
34
|
import { createStore } from '@violetflux/kerros'
|
|
35
35
|
import { useState } from 'react'
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
function useCounterModel() {
|
|
38
38
|
const [count, setCount] = useState(0)
|
|
39
39
|
return { count, setCount }
|
|
40
|
-
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const [useCounter, CounterProvider] = createStore(useCounterModel)
|
|
41
43
|
```
|
|
42
44
|
|
|
43
45
|
Im Store Hook können weiterhin `useState`, `useReducer`, Context, SDK Hooks und eigene Hooks verwendet werden.
|
|
44
46
|
|
|
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
|
+
|
|
45
49
|
## Provider einbinden und Werte auswählen
|
|
46
50
|
|
|
47
51
|
```tsx
|
|
@@ -79,12 +83,16 @@ Ohne verstecktes Modul-Singleton kann derselbe Provider mehrfach eingebunden, ü
|
|
|
79
83
|
|
|
80
84
|
```ts
|
|
81
85
|
function createStore<TStore, TProps = Record<never, never>>(
|
|
82
|
-
|
|
86
|
+
useModel: (props: TProps) => TStore,
|
|
83
87
|
): readonly [StoreHook<TStore>, StoreProvider<TProps>]
|
|
88
|
+
|
|
89
|
+
const [useStream, StreamProvider] = bindStore<Stream>('Stream')
|
|
84
90
|
```
|
|
85
91
|
|
|
86
92
|
Der zurückgegebene Store Hook verlangt einen Selektor, der ein Objekt liefert. Außerhalb des passenden Providers wird ein verständlicher Fehler ausgelöst. Strict Mode und Server Rendering werden unterstützt.
|
|
87
93
|
|
|
94
|
+
`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
|
+
|
|
88
96
|
## Dokumentation
|
|
89
97
|
|
|
90
98
|
- [Einführung](https://violetflux.github.io/kerros/de/guide/introduction)
|
package/README.es.md
CHANGED
|
@@ -34,14 +34,18 @@ Instala @violetflux/kerros con el gestor de paquetes del proyecto y después eje
|
|
|
34
34
|
import { createStore } from '@violetflux/kerros'
|
|
35
35
|
import { useState } from 'react'
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
function useCounterModel() {
|
|
38
38
|
const [count, setCount] = useState(0)
|
|
39
39
|
return { count, setCount }
|
|
40
|
-
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const [useCounter, CounterProvider] = createStore(useCounterModel)
|
|
41
43
|
```
|
|
42
44
|
|
|
43
45
|
El Hook del Store puede seguir usando `useState`, `useReducer`, Context, Hooks de SDK y Hooks personalizados.
|
|
44
46
|
|
|
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
|
+
|
|
45
49
|
## Montar el Provider y seleccionar valores
|
|
46
50
|
|
|
47
51
|
```tsx
|
|
@@ -79,12 +83,16 @@ Sin un singleton de módulo oculto, el mismo Provider puede montarse varias vece
|
|
|
79
83
|
|
|
80
84
|
```ts
|
|
81
85
|
function createStore<TStore, TProps = Record<never, never>>(
|
|
82
|
-
|
|
86
|
+
useModel: (props: TProps) => TStore,
|
|
83
87
|
): readonly [StoreHook<TStore>, StoreProvider<TProps>]
|
|
88
|
+
|
|
89
|
+
const [useStream, StreamProvider] = bindStore<Stream>('Stream')
|
|
84
90
|
```
|
|
85
91
|
|
|
86
92
|
El Hook devuelto requiere un selector que retorne un objeto. Usarlo fuera de su Provider correspondiente produce un error claro. Admite Strict Mode y renderizado en servidor.
|
|
87
93
|
|
|
94
|
+
`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
|
+
|
|
88
96
|
## Documentación
|
|
89
97
|
|
|
90
98
|
- [Introducción](https://violetflux.github.io/kerros/es/guide/introduction)
|
package/README.fr.md
CHANGED
|
@@ -34,14 +34,18 @@ Installe @violetflux/kerros avec le gestionnaire de paquets du projet, puis exé
|
|
|
34
34
|
import { createStore } from '@violetflux/kerros'
|
|
35
35
|
import { useState } from 'react'
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
function useCounterModel() {
|
|
38
38
|
const [count, setCount] = useState(0)
|
|
39
39
|
return { count, setCount }
|
|
40
|
-
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const [useCounter, CounterProvider] = createStore(useCounterModel)
|
|
41
43
|
```
|
|
42
44
|
|
|
43
45
|
Le Hook du Store peut continuer à utiliser `useState`, `useReducer`, Context, des Hooks SDK et vos Hooks personnalisés.
|
|
44
46
|
|
|
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
|
+
|
|
45
49
|
## Monter le Provider et sélectionner les valeurs
|
|
46
50
|
|
|
47
51
|
```tsx
|
|
@@ -79,12 +83,16 @@ Sans singleton de module caché, un même Provider peut être monté plusieurs f
|
|
|
79
83
|
|
|
80
84
|
```ts
|
|
81
85
|
function createStore<TStore, TProps = Record<never, never>>(
|
|
82
|
-
|
|
86
|
+
useModel: (props: TProps) => TStore,
|
|
83
87
|
): readonly [StoreHook<TStore>, StoreProvider<TProps>]
|
|
88
|
+
|
|
89
|
+
const [useStream, StreamProvider] = bindStore<Stream>('Stream')
|
|
84
90
|
```
|
|
85
91
|
|
|
86
92
|
Le Hook retourné exige un sélecteur qui renvoie un objet. Son utilisation hors du Provider correspondant lève une erreur claire. Strict Mode et le rendu serveur sont pris en charge.
|
|
87
93
|
|
|
94
|
+
`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
|
+
|
|
88
96
|
## Documentation
|
|
89
97
|
|
|
90
98
|
- [Introduction](https://violetflux.github.io/kerros/fr/guide/introduction)
|
package/README.ja.md
CHANGED
|
@@ -34,14 +34,18 @@ Kerros は React の状態を Hook の中、Provider の下という自然な場
|
|
|
34
34
|
import { createStore } from '@violetflux/kerros'
|
|
35
35
|
import { useState } from 'react'
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
function useCounterModel() {
|
|
38
38
|
const [count, setCount] = useState(0)
|
|
39
39
|
return { count, setCount }
|
|
40
|
-
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const [useCounter, CounterProvider] = createStore(useCounterModel)
|
|
41
43
|
```
|
|
42
44
|
|
|
43
45
|
Store Hook 内では `useState`、`useReducer`、Context、SDK Hook、カスタム Hook をそのまま利用できます。
|
|
44
46
|
|
|
47
|
+
initializer は `useCounterModel` のようなモジュール直下の名前付き Hook として定義してください。匿名 initializer も実行時には動作しますが、React Compiler の `infer` モードでは Hook として自動コンパイルされません。
|
|
48
|
+
|
|
45
49
|
## Provider を配置して値を選択する
|
|
46
50
|
|
|
47
51
|
```tsx
|
|
@@ -80,12 +84,16 @@ Provider は Store Hook を実行し、Context には安定した購読コンテ
|
|
|
80
84
|
|
|
81
85
|
```ts
|
|
82
86
|
function createStore<TStore, TProps = Record<never, never>>(
|
|
83
|
-
|
|
87
|
+
useModel: (props: TProps) => TStore,
|
|
84
88
|
): readonly [StoreHook<TStore>, StoreProvider<TProps>]
|
|
89
|
+
|
|
90
|
+
const [useStream, StreamProvider] = bindStore<Stream>('Stream')
|
|
85
91
|
```
|
|
86
92
|
|
|
87
93
|
返される Store Hook にはオブジェクトを返す selector が必須です。対応する Provider の外で呼び出すと明確なエラーを送出します。Strict Mode とサーバーレンダリングをサポートします。
|
|
88
94
|
|
|
95
|
+
高度な連携として、既存の Headless External Store にだけ `bindStore` を使います。通常の Hook 状態には `createStore` を使ってください。Context は元の Store インスタンスだけを保持し、コンシューマーは `getSnapshot` と `subscribe` を直接利用します。
|
|
96
|
+
|
|
89
97
|
## ドキュメント
|
|
90
98
|
|
|
91
99
|
- [はじめに](https://violetflux.github.io/kerros/ja/guide/introduction)
|
package/README.ko.md
CHANGED
|
@@ -34,14 +34,18 @@ Kerros는 React 상태를 Hook 안과 Provider 아래라는 자연스러운 위
|
|
|
34
34
|
import { createStore } from '@violetflux/kerros'
|
|
35
35
|
import { useState } from 'react'
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
function useCounterModel() {
|
|
38
38
|
const [count, setCount] = useState(0)
|
|
39
39
|
return { count, setCount }
|
|
40
|
-
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const [useCounter, CounterProvider] = createStore(useCounterModel)
|
|
41
43
|
```
|
|
42
44
|
|
|
43
45
|
Store Hook 안에서 `useState`, `useReducer`, Context, SDK Hook, 사용자 Hook을 그대로 사용할 수 있습니다.
|
|
44
46
|
|
|
47
|
+
initializer는 `useCounterModel`처럼 모듈 최상위의 이름 있는 Hook으로 정의하세요. 익명 initializer도 런타임에서는 동작하지만 React Compiler의 `infer` 모드에서는 Hook으로 자동 컴파일되지 않습니다.
|
|
48
|
+
|
|
45
49
|
## Provider를 마운트하고 필요한 값 선택하기
|
|
46
50
|
|
|
47
51
|
```tsx
|
|
@@ -80,12 +84,16 @@ Provider는 Store Hook을 실행하고 Context에는 안정적인 구독 컨테
|
|
|
80
84
|
|
|
81
85
|
```ts
|
|
82
86
|
function createStore<TStore, TProps = Record<never, never>>(
|
|
83
|
-
|
|
87
|
+
useModel: (props: TProps) => TStore,
|
|
84
88
|
): readonly [StoreHook<TStore>, StoreProvider<TProps>]
|
|
89
|
+
|
|
90
|
+
const [useStream, StreamProvider] = bindStore<Stream>('Stream')
|
|
85
91
|
```
|
|
86
92
|
|
|
87
93
|
반환된 Store Hook에는 객체를 반환하는 selector가 필요합니다. 대응하는 Provider 밖에서 호출하면 명확한 오류가 발생합니다. Strict Mode와 서버 렌더링을 지원합니다.
|
|
88
94
|
|
|
95
|
+
고급 통합이 필요한 기존 Headless External Store에만 `bindStore`를 사용하세요. 일반 Hook 상태에는 `createStore`를 사용합니다. Context는 원래 Store 인스턴스만 보관하고 소비자는 `getSnapshot`과 `subscribe`를 직접 사용합니다.
|
|
96
|
+
|
|
89
97
|
## 문서
|
|
90
98
|
|
|
91
99
|
- [소개](https://violetflux.github.io/kerros/ko/guide/introduction)
|
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ interface Task {
|
|
|
48
48
|
title: string
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
function useTaskModel() {
|
|
52
52
|
const [tasks, setTasks] = useState<Task[]>([])
|
|
53
53
|
|
|
54
54
|
const addTask = (task: Task) => {
|
|
@@ -60,13 +60,17 @@ export const [useTask, TaskProvider] = createStore(() => {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
return { tasks, addTask, finishTask }
|
|
63
|
-
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const [useTask, TaskProvider] = createStore(useTaskModel)
|
|
64
66
|
```
|
|
65
67
|
|
|
66
68
|
`createStore` returns two values: the Hook used by components and its matching Provider.
|
|
67
69
|
|
|
68
70
|
The Store is still a normal React Hook. It may use `useState`, `useReducer`, Context, SDK Hooks, or your own custom Hooks.
|
|
69
71
|
|
|
72
|
+
Keep the initializer as a top-level named Hook such as `useTaskModel`. Anonymous initializers still work at runtime, but React Compiler `infer` mode does not automatically compile them as Hooks.
|
|
73
|
+
|
|
70
74
|
### Mount the Provider
|
|
71
75
|
|
|
72
76
|
Only descendants of `TaskProvider` may use `useTask`:
|
|
@@ -162,7 +166,7 @@ Each `TaskList` automatically reads its nearest Provider.
|
|
|
162
166
|
A Store may call another Store directly. For example, a task Store can read the current account:
|
|
163
167
|
|
|
164
168
|
```tsx
|
|
165
|
-
|
|
169
|
+
function useTaskModel() {
|
|
166
170
|
const { user } = useAccount(s => ({ user: s.user }))
|
|
167
171
|
const [tasks, setTasks] = useState<Task[]>([])
|
|
168
172
|
|
|
@@ -178,7 +182,9 @@ export const [useTask, TaskProvider] = createStore(() => {
|
|
|
178
182
|
}
|
|
179
183
|
|
|
180
184
|
return { tasks, addTask }
|
|
181
|
-
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export const [useTask, TaskProvider] = createStore(useTaskModel)
|
|
182
188
|
```
|
|
183
189
|
|
|
184
190
|
Mount Providers in dependency order and keep dependencies one-way:
|
|
@@ -200,12 +206,12 @@ interface CounterProps {
|
|
|
200
206
|
initialCount: number
|
|
201
207
|
}
|
|
202
208
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
)
|
|
209
|
+
function useCounterModel({ initialCount }: CounterProps) {
|
|
210
|
+
const [count, setCount] = useState(initialCount)
|
|
211
|
+
return { count, setCount }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const [useCounter, CounterProvider] = createStore(useCounterModel)
|
|
209
215
|
```
|
|
210
216
|
|
|
211
217
|
```tsx
|
|
@@ -216,18 +222,40 @@ const [useCounter, CounterProvider] = createStore(
|
|
|
216
222
|
|
|
217
223
|
## API
|
|
218
224
|
|
|
225
|
+
### `createStore` (default)
|
|
226
|
+
|
|
219
227
|
```ts
|
|
220
228
|
function createStore<TStore, TProps = Record<never, never>>(
|
|
221
|
-
|
|
229
|
+
useModel: (props: TProps) => TStore,
|
|
222
230
|
): readonly [StoreHook<TStore>, StoreProvider<TProps>]
|
|
223
231
|
```
|
|
224
232
|
|
|
225
|
-
- `
|
|
226
|
-
- Provider props, excluding `children`, are passed to `
|
|
233
|
+
- `useModel` follows the Rules of Hooks
|
|
234
|
+
- Provider props, excluding `children`, are passed to `useModel`
|
|
227
235
|
- the returned Store Hook requires an object-returning selector
|
|
228
236
|
- using the Store Hook outside its matching Provider throws a clear error
|
|
229
237
|
- Provider instances work with Strict Mode and server rendering
|
|
230
238
|
|
|
239
|
+
### Advanced: bind an existing external Store
|
|
240
|
+
|
|
241
|
+
Most applications only need `createStore`. Use `bindStore` when a library or SDK already owns authoritative state outside React and exposes stable `getSnapshot` and `subscribe` functions.
|
|
242
|
+
|
|
243
|
+
Without this API, an integration must either repeat the Context and selector subscription code or copy the external snapshot through a second Store. `bindStore` provides Provider scoping and selectors while keeping the original Store as the only state owner.
|
|
244
|
+
|
|
245
|
+
```tsx
|
|
246
|
+
const [useStream, StreamBindingProvider] = bindStore<Stream>('Stream')
|
|
247
|
+
|
|
248
|
+
<StreamBindingProvider store={stream}>
|
|
249
|
+
<App />
|
|
250
|
+
</StreamBindingProvider>
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
The Provider stores only the original Store instance in Context. Consumers subscribe directly, so Kerros does not copy snapshots or add another publication layer. The component that creates the Store keeps lifecycle and imperative access.
|
|
254
|
+
|
|
255
|
+
In one sentence: `bindStore` is a React adapter for an external Store, not a state synchronizer. State remains only in the original Store; React reads it with `getSnapshot` after a subscription notification.
|
|
256
|
+
|
|
257
|
+
If the state begins in `useState`, `useReducer`, an SDK Hook, or another custom Hook, keep using `createStore`.
|
|
258
|
+
|
|
231
259
|
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.
|
|
232
260
|
|
|
233
261
|
## Documentation
|
package/README.zh-CN.md
CHANGED
|
@@ -40,7 +40,7 @@ interface Task {
|
|
|
40
40
|
title: string
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
function useTaskModel() {
|
|
44
44
|
const [tasks, setTasks] = useState<Task[]>([])
|
|
45
45
|
|
|
46
46
|
const addTask = (task: Task) => {
|
|
@@ -52,13 +52,17 @@ export const [useTask, TaskProvider] = createStore(() => {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
return { tasks, addTask, finishTask }
|
|
55
|
-
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const [useTask, TaskProvider] = createStore(useTaskModel)
|
|
56
58
|
```
|
|
57
59
|
|
|
58
60
|
`createStore` 返回两个值:组件调用的 Hook 和对应的 Provider。
|
|
59
61
|
|
|
60
62
|
Store 仍然是普通 React Hook,可以继续使用 `useState`、`useReducer`、Context、SDK Hook 或其他 custom Hook。
|
|
61
63
|
|
|
64
|
+
请把 initializer 写成 `useTaskModel` 这类同文件顶层命名 Hook。匿名 initializer 在运行时仍然可用,但 React Compiler 的 `infer` 模式不会自动把它识别并编译为 Hook。
|
|
65
|
+
|
|
62
66
|
### 挂载 Provider
|
|
63
67
|
|
|
64
68
|
只有 `TaskProvider` 的子节点可以调用 `useTask`:
|
|
@@ -154,7 +158,7 @@ Kerros 简单、轻量、可靠。先把状态写成普通 Hook,需要共享
|
|
|
154
158
|
一个 Store 可以直接调用另一个 Store。例如任务 Store 读取当前账户:
|
|
155
159
|
|
|
156
160
|
```tsx
|
|
157
|
-
|
|
161
|
+
function useTaskModel() {
|
|
158
162
|
const { user } = useAccount(s => ({ user: s.user }))
|
|
159
163
|
const [tasks, setTasks] = useState<Task[]>([])
|
|
160
164
|
|
|
@@ -170,7 +174,9 @@ export const [useTask, TaskProvider] = createStore(() => {
|
|
|
170
174
|
}
|
|
171
175
|
|
|
172
176
|
return { tasks, addTask }
|
|
173
|
-
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export const [useTask, TaskProvider] = createStore(useTaskModel)
|
|
174
180
|
```
|
|
175
181
|
|
|
176
182
|
按依赖顺序挂 Provider,并保持单向依赖:
|
|
@@ -192,12 +198,12 @@ interface CounterProps {
|
|
|
192
198
|
initialCount: number
|
|
193
199
|
}
|
|
194
200
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
)
|
|
201
|
+
function useCounterModel({ initialCount }: CounterProps) {
|
|
202
|
+
const [count, setCount] = useState(initialCount)
|
|
203
|
+
return { count, setCount }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const [useCounter, CounterProvider] = createStore(useCounterModel)
|
|
201
207
|
```
|
|
202
208
|
|
|
203
209
|
```tsx
|
|
@@ -208,18 +214,40 @@ const [useCounter, CounterProvider] = createStore(
|
|
|
208
214
|
|
|
209
215
|
## API
|
|
210
216
|
|
|
217
|
+
### `createStore`(默认用法)
|
|
218
|
+
|
|
211
219
|
```ts
|
|
212
220
|
function createStore<TStore, TProps = Record<never, never>>(
|
|
213
|
-
|
|
221
|
+
useModel: (props: TProps) => TStore,
|
|
214
222
|
): readonly [StoreHook<TStore>, StoreProvider<TProps>]
|
|
215
223
|
```
|
|
216
224
|
|
|
217
|
-
- `
|
|
218
|
-
- 除 `children` 外的 Provider props 会传给 `
|
|
225
|
+
- `useModel` 必须遵守 Hooks 规则
|
|
226
|
+
- 除 `children` 外的 Provider props 会传给 `useModel`
|
|
219
227
|
- Store Hook 必须接收一个返回对象的 selector
|
|
220
228
|
- 在对应 Provider 外调用会抛出明确错误
|
|
221
229
|
- 支持 Strict Mode、服务端渲染和 Provider 多实例
|
|
222
230
|
|
|
231
|
+
### 高级用法:绑定已有 External Store
|
|
232
|
+
|
|
233
|
+
绝大多数应用只需要 `createStore`。只有当某个库或 SDK 已经在 React 外持有权威状态,并提供稳定的 `getSnapshot` 和 `subscribe` 函数时,才使用 `bindStore`。
|
|
234
|
+
|
|
235
|
+
没有这个 API 时,集成层只能重复实现 Context 和 selector 订阅,或者把 External Store 快照复制进第二个 Store。`bindStore` 只提供 Provider 作用域和 selector,原 Store 仍是唯一状态所有者。
|
|
236
|
+
|
|
237
|
+
```tsx
|
|
238
|
+
const [useStream, StreamBindingProvider] = bindStore<Stream>('Stream')
|
|
239
|
+
|
|
240
|
+
<StreamBindingProvider store={stream}>
|
|
241
|
+
<App />
|
|
242
|
+
</StreamBindingProvider>
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Provider 的 Context 只保存原 Store 实例,组件直接订阅它;Kerros 不复制快照,也不增加中间发布层。创建 Store 的组件继续负责生命周期和命令式访问。
|
|
246
|
+
|
|
247
|
+
一句话理解:`bindStore` 是 External Store 的 React 适配器,不是状态同步器。状态仍只存在于原 Store 中,React 在收到订阅通知后通过 `getSnapshot` 读取它。
|
|
248
|
+
|
|
249
|
+
如果状态来自 `useState`、`useReducer`、SDK Hook 或其他 custom Hook,继续使用 `createStore`。
|
|
250
|
+
|
|
223
251
|
React 17 使用官方 `use-sync-external-store` shim;React 18 和 19 可用时优先使用 React 原生实现。React Compiler 不是必需项。
|
|
224
252
|
|
|
225
253
|
## 文档
|
package/dist/index.cjs
CHANGED
|
@@ -6,28 +6,50 @@ const useStoreLayoutEffect = typeof window === "undefined" ? react.useEffect : r
|
|
|
6
6
|
/**
|
|
7
7
|
* Create a selector-first React Store and its matching Provider
|
|
8
8
|
*/
|
|
9
|
-
function createStore(
|
|
9
|
+
function createStore(useModel) {
|
|
10
10
|
const StoreContext = (0, react.createContext)(void 0);
|
|
11
|
-
const storeName =
|
|
12
|
-
/** Run the
|
|
11
|
+
const storeName = useModel.name || "KerrosStore";
|
|
12
|
+
/** Run the model Hook and publish its committed snapshot */
|
|
13
13
|
const StoreProvider = (props) => {
|
|
14
14
|
const { children, ...storeProps } = props;
|
|
15
|
-
const
|
|
16
|
-
const [container] = (0, react.useState)(() => createStoreContainer(
|
|
17
|
-
useStoreLayoutEffect(() => container.publish(
|
|
15
|
+
const model = useModel(storeProps);
|
|
16
|
+
const [container] = (0, react.useState)(() => createStoreContainer(model));
|
|
17
|
+
useStoreLayoutEffect(() => container.publish(model), [container, model]);
|
|
18
18
|
return (0, react.createElement)(StoreContext.Provider, { value: container }, children);
|
|
19
19
|
};
|
|
20
20
|
StoreProvider.displayName = `${storeName}Provider`;
|
|
21
21
|
StoreContext.displayName = `${storeName}Context`;
|
|
22
22
|
/** Select Store fields through the stable Provider container */
|
|
23
23
|
const useStore = (selector) => {
|
|
24
|
-
|
|
25
|
-
if (!container) throw new Error("Kerros store hook must be used within its matching Provider");
|
|
26
|
-
return (0, use_sync_external_store_shim_with_selector.useSyncExternalStoreWithSelector)(container.subscribe, container.getSnapshot, container.getSnapshot, selector, shallowEqual);
|
|
24
|
+
return useStoreSelector(useStoreContext(StoreContext), selector);
|
|
27
25
|
};
|
|
28
26
|
return [useStore, StoreProvider];
|
|
29
27
|
}
|
|
30
28
|
/**
|
|
29
|
+
* Bind existing external Store instances to scoped React consumers
|
|
30
|
+
*/
|
|
31
|
+
function bindStore(name = "KerrosExternalStore") {
|
|
32
|
+
const StoreContext = (0, react.createContext)(void 0);
|
|
33
|
+
/** Provide one existing Store instance without copying its snapshot */
|
|
34
|
+
const StoreProvider = (props) => {
|
|
35
|
+
const { children, store } = props;
|
|
36
|
+
return (0, react.createElement)(StoreContext.Provider, { value: store }, children);
|
|
37
|
+
};
|
|
38
|
+
StoreProvider.displayName = `${name}Provider`;
|
|
39
|
+
StoreContext.displayName = `${name}Context`;
|
|
40
|
+
/** Select snapshot fields directly from the bound external Store */
|
|
41
|
+
const useStore = (selector) => {
|
|
42
|
+
return useStoreSelector(useStoreContext(StoreContext), selector);
|
|
43
|
+
};
|
|
44
|
+
/** Read the exact Store instance bound to the current Provider */
|
|
45
|
+
const useInstance = () => useStoreContext(StoreContext);
|
|
46
|
+
return [
|
|
47
|
+
useStore,
|
|
48
|
+
StoreProvider,
|
|
49
|
+
useInstance
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
31
53
|
* Create the stable external-store container for one Provider instance
|
|
32
54
|
*/
|
|
33
55
|
function createStoreContainer(initialSnapshot) {
|
|
@@ -47,6 +69,20 @@ function createStoreContainer(initialSnapshot) {
|
|
|
47
69
|
};
|
|
48
70
|
}
|
|
49
71
|
/**
|
|
72
|
+
* Read the Store bound to the current Provider
|
|
73
|
+
*/
|
|
74
|
+
function useStoreContext(context) {
|
|
75
|
+
const store = (0, react.useContext)(context);
|
|
76
|
+
if (!store) throw new Error("Kerros store hook must be used within its matching Provider");
|
|
77
|
+
return store;
|
|
78
|
+
}
|
|
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
|
+
/**
|
|
50
86
|
* Compare selector objects by their enumerable top-level fields
|
|
51
87
|
*/
|
|
52
88
|
function shallowEqual(left, right) {
|
|
@@ -56,4 +92,5 @@ function shallowEqual(left, right) {
|
|
|
56
92
|
return leftKeys.every((key) => Object.prototype.hasOwnProperty.call(right, key) && Object.is(left[key], right[key]));
|
|
57
93
|
}
|
|
58
94
|
//#endregion
|
|
95
|
+
exports.bindStore = bindStore;
|
|
59
96
|
exports.createStore = createStore;
|
package/dist/index.d.cts
CHANGED
|
@@ -8,9 +8,26 @@ interface StoreHook<TStore> {
|
|
|
8
8
|
}
|
|
9
9
|
/** Provider created for a Store hook */
|
|
10
10
|
type StoreProvider<TProps> = FC<PropsWithChildren<TProps>>;
|
|
11
|
+
/** Existing external Store contract supported by bindStore */
|
|
12
|
+
interface ExternalStore<TSnapshot> {
|
|
13
|
+
/** Read the current immutable snapshot */
|
|
14
|
+
getSnapshot: () => TSnapshot;
|
|
15
|
+
/** Subscribe to snapshot changes */
|
|
16
|
+
subscribe: (listener: () => void) => () => void;
|
|
17
|
+
}
|
|
18
|
+
/** Extract the snapshot exposed by an external Store */
|
|
19
|
+
type ExternalStoreSnapshot<TStore> = TStore extends ExternalStore<infer TSnapshot> ? TSnapshot : never;
|
|
20
|
+
/** React bindings created for an existing external Store type */
|
|
21
|
+
type StoreBinding<TStore extends ExternalStore<TSnapshot>, TSnapshot = ExternalStoreSnapshot<TStore>> = readonly [StoreHook<TSnapshot>, StoreProvider<{
|
|
22
|
+
store: TStore;
|
|
23
|
+
}>, () => TStore];
|
|
11
24
|
/**
|
|
12
25
|
* Create a selector-first React Store and its matching Provider
|
|
13
26
|
*/
|
|
14
|
-
declare function createStore<TStore, TProps = Record<never, never>>(
|
|
27
|
+
declare function createStore<TStore, TProps = Record<never, never>>(useModel: (props: TProps) => TStore): readonly [StoreHook<TStore>, StoreProvider<TProps>];
|
|
28
|
+
/**
|
|
29
|
+
* Bind existing external Store instances to scoped React consumers
|
|
30
|
+
*/
|
|
31
|
+
declare function bindStore<TStore extends ExternalStore<TSnapshot>, TSnapshot = ExternalStoreSnapshot<TStore>>(name?: string): StoreBinding<TStore, TSnapshot>;
|
|
15
32
|
//#endregion
|
|
16
|
-
export { StoreHook, StoreProvider, StoreSelector, createStore };
|
|
33
|
+
export { ExternalStore, ExternalStoreSnapshot, StoreBinding, StoreHook, StoreProvider, StoreSelector, bindStore, createStore };
|
package/dist/index.d.mts
CHANGED
|
@@ -8,9 +8,26 @@ interface StoreHook<TStore> {
|
|
|
8
8
|
}
|
|
9
9
|
/** Provider created for a Store hook */
|
|
10
10
|
type StoreProvider<TProps> = FC<PropsWithChildren<TProps>>;
|
|
11
|
+
/** Existing external Store contract supported by bindStore */
|
|
12
|
+
interface ExternalStore<TSnapshot> {
|
|
13
|
+
/** Read the current immutable snapshot */
|
|
14
|
+
getSnapshot: () => TSnapshot;
|
|
15
|
+
/** Subscribe to snapshot changes */
|
|
16
|
+
subscribe: (listener: () => void) => () => void;
|
|
17
|
+
}
|
|
18
|
+
/** Extract the snapshot exposed by an external Store */
|
|
19
|
+
type ExternalStoreSnapshot<TStore> = TStore extends ExternalStore<infer TSnapshot> ? TSnapshot : never;
|
|
20
|
+
/** React bindings created for an existing external Store type */
|
|
21
|
+
type StoreBinding<TStore extends ExternalStore<TSnapshot>, TSnapshot = ExternalStoreSnapshot<TStore>> = readonly [StoreHook<TSnapshot>, StoreProvider<{
|
|
22
|
+
store: TStore;
|
|
23
|
+
}>, () => TStore];
|
|
11
24
|
/**
|
|
12
25
|
* Create a selector-first React Store and its matching Provider
|
|
13
26
|
*/
|
|
14
|
-
declare function createStore<TStore, TProps = Record<never, never>>(
|
|
27
|
+
declare function createStore<TStore, TProps = Record<never, never>>(useModel: (props: TProps) => TStore): readonly [StoreHook<TStore>, StoreProvider<TProps>];
|
|
28
|
+
/**
|
|
29
|
+
* Bind existing external Store instances to scoped React consumers
|
|
30
|
+
*/
|
|
31
|
+
declare function bindStore<TStore extends ExternalStore<TSnapshot>, TSnapshot = ExternalStoreSnapshot<TStore>>(name?: string): StoreBinding<TStore, TSnapshot>;
|
|
15
32
|
//#endregion
|
|
16
|
-
export { StoreHook, StoreProvider, StoreSelector, createStore };
|
|
33
|
+
export { ExternalStore, ExternalStoreSnapshot, StoreBinding, StoreHook, StoreProvider, StoreSelector, bindStore, createStore };
|
package/dist/index.mjs
CHANGED
|
@@ -5,28 +5,50 @@ const useStoreLayoutEffect = typeof window === "undefined" ? useEffect : useLayo
|
|
|
5
5
|
/**
|
|
6
6
|
* Create a selector-first React Store and its matching Provider
|
|
7
7
|
*/
|
|
8
|
-
function createStore(
|
|
8
|
+
function createStore(useModel) {
|
|
9
9
|
const StoreContext = createContext(void 0);
|
|
10
|
-
const storeName =
|
|
11
|
-
/** Run the
|
|
10
|
+
const storeName = useModel.name || "KerrosStore";
|
|
11
|
+
/** Run the model Hook and publish its committed snapshot */
|
|
12
12
|
const StoreProvider = (props) => {
|
|
13
13
|
const { children, ...storeProps } = props;
|
|
14
|
-
const
|
|
15
|
-
const [container] = useState(() => createStoreContainer(
|
|
16
|
-
useStoreLayoutEffect(() => container.publish(
|
|
14
|
+
const model = useModel(storeProps);
|
|
15
|
+
const [container] = useState(() => createStoreContainer(model));
|
|
16
|
+
useStoreLayoutEffect(() => container.publish(model), [container, model]);
|
|
17
17
|
return createElement(StoreContext.Provider, { value: container }, children);
|
|
18
18
|
};
|
|
19
19
|
StoreProvider.displayName = `${storeName}Provider`;
|
|
20
20
|
StoreContext.displayName = `${storeName}Context`;
|
|
21
21
|
/** Select Store fields through the stable Provider container */
|
|
22
22
|
const useStore = (selector) => {
|
|
23
|
-
|
|
24
|
-
if (!container) throw new Error("Kerros store hook must be used within its matching Provider");
|
|
25
|
-
return useSyncExternalStoreWithSelector(container.subscribe, container.getSnapshot, container.getSnapshot, selector, shallowEqual);
|
|
23
|
+
return useStoreSelector(useStoreContext(StoreContext), selector);
|
|
26
24
|
};
|
|
27
25
|
return [useStore, StoreProvider];
|
|
28
26
|
}
|
|
29
27
|
/**
|
|
28
|
+
* Bind existing external Store instances to scoped React consumers
|
|
29
|
+
*/
|
|
30
|
+
function bindStore(name = "KerrosExternalStore") {
|
|
31
|
+
const StoreContext = createContext(void 0);
|
|
32
|
+
/** Provide one existing Store instance without copying its snapshot */
|
|
33
|
+
const StoreProvider = (props) => {
|
|
34
|
+
const { children, store } = props;
|
|
35
|
+
return createElement(StoreContext.Provider, { value: store }, children);
|
|
36
|
+
};
|
|
37
|
+
StoreProvider.displayName = `${name}Provider`;
|
|
38
|
+
StoreContext.displayName = `${name}Context`;
|
|
39
|
+
/** Select snapshot fields directly from the bound external Store */
|
|
40
|
+
const useStore = (selector) => {
|
|
41
|
+
return useStoreSelector(useStoreContext(StoreContext), selector);
|
|
42
|
+
};
|
|
43
|
+
/** Read the exact Store instance bound to the current Provider */
|
|
44
|
+
const useInstance = () => useStoreContext(StoreContext);
|
|
45
|
+
return [
|
|
46
|
+
useStore,
|
|
47
|
+
StoreProvider,
|
|
48
|
+
useInstance
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
30
52
|
* Create the stable external-store container for one Provider instance
|
|
31
53
|
*/
|
|
32
54
|
function createStoreContainer(initialSnapshot) {
|
|
@@ -46,6 +68,20 @@ function createStoreContainer(initialSnapshot) {
|
|
|
46
68
|
};
|
|
47
69
|
}
|
|
48
70
|
/**
|
|
71
|
+
* Read the Store bound to the current Provider
|
|
72
|
+
*/
|
|
73
|
+
function useStoreContext(context) {
|
|
74
|
+
const store = useContext(context);
|
|
75
|
+
if (!store) throw new Error("Kerros store hook must be used within its matching Provider");
|
|
76
|
+
return store;
|
|
77
|
+
}
|
|
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
|
+
/**
|
|
49
85
|
* Compare selector objects by their enumerable top-level fields
|
|
50
86
|
*/
|
|
51
87
|
function shallowEqual(left, right) {
|
|
@@ -55,4 +91,4 @@ function shallowEqual(left, right) {
|
|
|
55
91
|
return leftKeys.every((key) => Object.prototype.hasOwnProperty.call(right, key) && Object.is(left[key], right[key]));
|
|
56
92
|
}
|
|
57
93
|
//#endregion
|
|
58
|
-
export { createStore };
|
|
94
|
+
export { bindStore, createStore };
|
package/package.json
CHANGED
package/skills/kerros/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: kerros
|
|
3
|
-
description: Implement, refactor, review, or test shared React state with @violetflux/kerros. Use for createStore, Provider scoping, selector subscriptions, cross-Store composition, migrating from Hox or frequently changing React Context, preventing Context-wide rerenders, and React 17–19 compatibility.
|
|
3
|
+
description: Implement, refactor, review, or test shared React state with @violetflux/kerros. Use for createStore, bindStore, Provider scoping, selector subscriptions, existing headless external Stores, cross-Store composition, migrating from Hox or frequently changing React Context, preventing Context-wide rerenders, and React 17–19 compatibility.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Kerros
|
|
@@ -12,9 +12,10 @@ Build shared state from ordinary React Hooks. Keep Provider scope and multiple i
|
|
|
12
12
|
1. Inspect the project package manager, React version, existing state owner, Provider tree, and naming conventions.
|
|
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
|
-
4.
|
|
16
|
-
5.
|
|
17
|
-
6.
|
|
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 migrate consumers to focused selectors.
|
|
17
|
+
6. Order composed Providers from dependency to dependent and reject circular Store dependencies.
|
|
18
|
+
7. Run the project's typecheck, tests, lint, and the narrowest useful render test.
|
|
18
19
|
|
|
19
20
|
## Install
|
|
20
21
|
|
|
@@ -31,18 +32,20 @@ Kerros supports React 17, 18, and 19. Do not change package managers or create a
|
|
|
31
32
|
|
|
32
33
|
## Core pattern
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
Define the Store implementation as a top-level named Hook ending in `Model`, then pass that function to `createStore`. The model may return both state and actions. Name the public pair after the domain; do not repeat `Store` unless the codebase already requires it.
|
|
35
36
|
|
|
36
37
|
```tsx
|
|
37
38
|
import { createStore } from '@violetflux/kerros'
|
|
38
39
|
import { useState } from 'react'
|
|
39
40
|
|
|
40
|
-
|
|
41
|
+
function useCounterModel() {
|
|
41
42
|
const [count, setCount] = useState(0)
|
|
42
43
|
const increment = () => setCount(v => v + 1)
|
|
43
44
|
|
|
44
45
|
return { count, increment }
|
|
45
|
-
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const [useCounter, CounterProvider] = createStore(useCounterModel)
|
|
46
49
|
```
|
|
47
50
|
|
|
48
51
|
Mount the Provider at the narrowest ancestor shared by all consumers:
|
|
@@ -72,15 +75,53 @@ function Counter() {
|
|
|
72
75
|
|
|
73
76
|
Kerros shallowly compares the selected object's top-level fields with `Object.is`. An update to an unselected field must not rerender this component.
|
|
74
77
|
|
|
78
|
+
## Advanced external Store binding
|
|
79
|
+
|
|
80
|
+
Most applications should stop at `createStore`. Use `bindStore` only when a headless Store already owns authoritative state outside React and exposes stable `getSnapshot` and `subscribe` functions.
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
import { bindStore } from '@violetflux/kerros'
|
|
84
|
+
|
|
85
|
+
export const [
|
|
86
|
+
useStream,
|
|
87
|
+
StreamBindingProvider,
|
|
88
|
+
useStreamInstance,
|
|
89
|
+
] = bindStore<Stream>('Stream')
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Mount the original instance without mirroring its snapshot:
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
<StreamBindingProvider store={stream}>
|
|
96
|
+
<App />
|
|
97
|
+
</StreamBindingProvider>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Use `useStream` with focused selectors for snapshot reads. 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
|
+
|
|
102
|
+
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
|
+
|
|
104
|
+
Do not replace this with `createStore(() => useSyncExternalStore(...))`; that subscribes the Provider to the entire external snapshot and republishes it through a second container.
|
|
105
|
+
|
|
106
|
+
## React Compiler and identity
|
|
107
|
+
|
|
108
|
+
- 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
|
+
- 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
|
+
- 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
|
+
- Prefer Kerros's existing object selector behavior for consumers that return fresh objects. 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
|
+
- 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
|
+
|
|
75
114
|
## Provider props
|
|
76
115
|
|
|
77
116
|
Accept initialization or scope-specific inputs as Store Hook props. Pass them to the generated Provider instead of reading mutable module globals.
|
|
78
117
|
|
|
79
118
|
```tsx
|
|
80
|
-
|
|
119
|
+
function useGreetingModel(props: { initialName: string }) {
|
|
81
120
|
const [name, setName] = useState(props.initialName)
|
|
82
121
|
return { name, setName }
|
|
83
|
-
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const [useGreeting, GreetingProvider] = createStore(useGreetingModel)
|
|
84
125
|
|
|
85
126
|
<GreetingProvider initialName="Ada">
|
|
86
127
|
<Profile />
|
|
@@ -92,15 +133,19 @@ const [useGreeting, GreetingProvider] = createStore((props: { initialName: strin
|
|
|
92
133
|
An inner Store may call an outer Store Hook. Mount the dependency first and keep the graph one-way.
|
|
93
134
|
|
|
94
135
|
```tsx
|
|
95
|
-
|
|
136
|
+
function useSessionModel() {
|
|
96
137
|
const [userId, setUserId] = useState<string>()
|
|
97
138
|
return { userId, setUserId }
|
|
98
|
-
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const [useSession, SessionProvider] = createStore(useSessionModel)
|
|
99
142
|
|
|
100
|
-
|
|
143
|
+
function usePermissionsModel() {
|
|
101
144
|
const { userId } = useSession(s => ({ userId: s.userId }))
|
|
102
145
|
return { canEdit: Boolean(userId) }
|
|
103
|
-
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const [usePermissions, PermissionsProvider] = createStore(usePermissionsModel)
|
|
104
149
|
|
|
105
150
|
function Providers({ children }: PropsWithChildren) {
|
|
106
151
|
return (
|
|
@@ -113,15 +158,16 @@ function Providers({ children }: PropsWithChildren) {
|
|
|
113
158
|
|
|
114
159
|
## Guardrails
|
|
115
160
|
|
|
161
|
+
- 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.
|
|
116
162
|
- Require every Store read to use an object selector. Do not use array selectors.
|
|
117
163
|
- Select concrete fields and actions. Do not expose or select a changing aggregate Store snapshot.
|
|
118
164
|
- Do not wrap inline selectors with `useCallback`; Kerros handles selector identity.
|
|
119
|
-
- Keep public actions as ordinary functions. In React 19, use `useEffectEvent` only for events called from Effects, never as a public Store action.
|
|
165
|
+
- 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.
|
|
120
166
|
- Do not mirror the same mutable state across Stores. Read it from its authoritative Store or move ownership.
|
|
121
167
|
- Do not create circular Store dependencies. Split ownership or invert the Provider order.
|
|
122
168
|
- Do not call a Store Hook outside its matching Provider; Kerros intentionally throws a clear error.
|
|
123
169
|
- Do not replace scoped Providers with a hidden module singleton. Put an application-wide Provider at the root only when the state is truly application-wide.
|
|
124
|
-
- Preserve SDK caches, subscriptions, and streams under a single
|
|
170
|
+
- Preserve SDK caches, subscriptions, and streams under a single owner. Bind an existing headless Store directly; call a connection-owning SDK Hook inside one `createStore` initializer only when no external Store instance exists.
|
|
125
171
|
- Respect the project's React version. Avoid React 19-only APIs when the consuming project still supports React 17 or 18.
|
|
126
172
|
|
|
127
173
|
## Migrate existing state
|
|
@@ -133,6 +179,7 @@ function Providers({ children }: PropsWithChildren) {
|
|
|
133
179
|
## Verify
|
|
134
180
|
|
|
135
181
|
- Confirm all consumers are below the correct Provider and multiple Provider instances stay isolated.
|
|
182
|
+
- 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.
|
|
136
183
|
- Test Provider props, Strict Mode, subscription cleanup, and the outside-Provider error when changing Store infrastructure.
|
|
137
184
|
- Add a render-count test showing that changing an unselected field does not rerender the consumer.
|
|
138
185
|
- Search for broad Store selections, array selectors, duplicate subscriptions, and dependency cycles.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
interface:
|
|
2
2
|
display_name: "Kerros"
|
|
3
3
|
short_description: "Build selector-based shared React state with Kerros"
|
|
4
|
-
default_prompt: "Use $kerros to implement shared React state with createStore, Providers, and focused selectors."
|
|
4
|
+
default_prompt: "Use $kerros to implement shared React state with createStore or bindStore, scoped Providers, and focused selectors."
|