@kdeveloper/kvark 0.16.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +119 -119
- package/dist/family.d.ts +12 -12
- package/dist/family.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/preact/index.d.ts +4 -4
- package/dist/preact/index.js +9 -9
- package/dist/preact/index.js.map +1 -1
- package/dist/react/index.d.ts +4 -4
- package/dist/react/index.js +9 -9
- package/dist/react/index.js.map +1 -1
- package/dist/{store-76uVtDce.js → store-CTGJpxoI.js} +51 -51
- package/dist/store-CTGJpxoI.js.map +1 -0
- package/dist/{types-d2mv_eob.d.ts → types-DOyqUVX8.d.ts} +32 -32
- package/dist/vue/index.d.ts +4 -4
- package/dist/vue/index.js +9 -9
- package/dist/vue/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/store-76uVtDce.js.map +0 -1
package/README.md
CHANGED
|
@@ -11,12 +11,12 @@ Inspired by Jotai, but built around a key difference: dependencies are declared
|
|
|
11
11
|
| | Jotai | Kvark |
|
|
12
12
|
| ---------------------- | -------------------------------- | ----------------------------------------------- |
|
|
13
13
|
| Dependency declaration | Implicit (via `get(atom)` calls) | Explicit, via `dependencies` field |
|
|
14
|
-
| `atom()` signature | `atom(read, write?)` | `atom({
|
|
15
|
-
| Async model | Optional | `
|
|
14
|
+
| `atom()` signature | `atom(read, write?)` | `atom({ read, write?, dependencies? })` |
|
|
15
|
+
| Async model | Optional | `read`, `write`, `ctx.read` — always `async` |
|
|
16
16
|
| Parallel loading | Manual `Promise.all` | Built-in through `dependencies` |
|
|
17
17
|
| Stale-while-revalidate | Re-suspends on revalidation | Shows stale data; Suspense only on first load |
|
|
18
18
|
| `atomFamily` | External (`jotai/utils`) | Core, with LRU and invalidation |
|
|
19
|
-
| External invalidation |
|
|
19
|
+
| External invalidation | Manual via store mutation APIs | Explicit `store.invalidate()` / `StoreClient` |
|
|
20
20
|
| TypeScript strictness | `strict: true` recommended | `strict: true` + 8 extra flags, `any` forbidden |
|
|
21
21
|
|
|
22
22
|
## Installation
|
|
@@ -30,20 +30,20 @@ pnpm add @kdeveloper/kvark
|
|
|
30
30
|
|
|
31
31
|
```tsx
|
|
32
32
|
import { atom, createStore } from "@kdeveloper/kvark";
|
|
33
|
-
import { Provider,
|
|
33
|
+
import { Provider, useApplyAtom, useAtomValue } from "@kdeveloper/kvark/react";
|
|
34
34
|
|
|
35
35
|
// 1. Create atoms
|
|
36
36
|
const userIdAtom = atom({
|
|
37
37
|
debugLabel: "userId",
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
read: async () => 1,
|
|
39
|
+
write: async (_ctx, _id: number) => {},
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
const userAtom = atom({
|
|
43
43
|
debugLabel: "user",
|
|
44
44
|
dependencies: { userId: userIdAtom },
|
|
45
|
-
|
|
46
|
-
const id = await ctx.
|
|
45
|
+
read: async (ctx) => {
|
|
46
|
+
const id = await ctx.read("userId");
|
|
47
47
|
const res = await fetch(`/api/users/${id}`, { signal: ctx.signal });
|
|
48
48
|
return res.json();
|
|
49
49
|
},
|
|
@@ -75,14 +75,14 @@ function UserCard() {
|
|
|
75
75
|
The same atoms and store work with `@kdeveloper/kvark/preact` — the API is identical to the React integration. Replace the React import with the Preact one:
|
|
76
76
|
|
|
77
77
|
```tsx
|
|
78
|
-
import { Provider,
|
|
78
|
+
import { Provider, useApplyAtom, useAtomValue } from "@kdeveloper/kvark/preact";
|
|
79
79
|
```
|
|
80
80
|
|
|
81
81
|
Hooks use only `preact` and `preact/hooks` internally — no `preact/compat` aliases required.
|
|
82
82
|
|
|
83
83
|
### Vue 3
|
|
84
84
|
|
|
85
|
-
The same atoms and store work with `@kdeveloper/kvark/vue`. Composables mirror the React hooks; `useAtomValue` returns an **awaitable** [`shallowRef`](https://vuejs.org/api/reactivity-advanced.html#shallowref) — use `.value` in `<script setup>` (templates unwrap refs automatically). Until the first `
|
|
85
|
+
The same atoms and store work with `@kdeveloper/kvark/vue`. Composables mirror the React hooks; `useAtomValue` returns an **awaitable** [`shallowRef`](https://vuejs.org/api/reactivity-advanced.html#shallowref) — use `.value` in `<script setup>` (templates unwrap refs automatically). Until the first `read` resolves, that ref’s value is `undefined`; TypeScript types it as `V | undefined` (`ThenableShallowRef<V>`). After `await useAtomValue(atom)` you get `Readonly<ShallowRef<V>>` with a defined `.value`. The ref also implements `PromiseLike`, so awaiting it in an async `setup` suspends until the first `read` resolves — see [Suspense (Vue 3)](#suspense-vue-3).
|
|
86
86
|
|
|
87
87
|
Composables must run **inside** a `Provider` subtree. Put `useAtomValue` / `useAtom` in a child component (or a nested route), not in the same component that renders `Provider` without passing the store through a wrapper.
|
|
88
88
|
|
|
@@ -93,15 +93,15 @@ import { atom } from "@kdeveloper/kvark";
|
|
|
93
93
|
|
|
94
94
|
export const userIdAtom = atom({
|
|
95
95
|
debugLabel: "userId",
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
read: async () => 1,
|
|
97
|
+
write: async () => {},
|
|
98
98
|
});
|
|
99
99
|
|
|
100
100
|
export const userAtom = atom({
|
|
101
101
|
debugLabel: "user",
|
|
102
102
|
dependencies: { userId: userIdAtom },
|
|
103
|
-
|
|
104
|
-
const id = await ctx.
|
|
103
|
+
read: async (ctx) => {
|
|
104
|
+
const id = await ctx.read("userId");
|
|
105
105
|
const res = await fetch(`/api/users/${id}`, { signal: ctx.signal });
|
|
106
106
|
return res.json();
|
|
107
107
|
},
|
|
@@ -149,7 +149,7 @@ Unlike React, Vue’s `<Suspense>` only activates for **async** child components
|
|
|
149
149
|
|
|
150
150
|
## Store Context
|
|
151
151
|
|
|
152
|
-
Kvark can inject arbitrary application context into every atom and mutation callback through the store, similar to router context in TanStack Router. The context is immutable for the lifetime of a store and is available in `
|
|
152
|
+
Kvark can inject arbitrary application context into every atom and mutation callback through the store, similar to router context in TanStack Router. The context is immutable for the lifetime of a store and is available in `read`, `write`, `onMount`, `mutation`, `batch.fetch`, and `infinityAtom.queryFn`.
|
|
153
153
|
|
|
154
154
|
First, declare the context type once with module augmentation:
|
|
155
155
|
|
|
@@ -179,30 +179,30 @@ export const store = createStore({
|
|
|
179
179
|
});
|
|
180
180
|
```
|
|
181
181
|
|
|
182
|
-
Now every Kvark callback can read `ctx.
|
|
182
|
+
Now every Kvark callback can read `ctx.global`:
|
|
183
183
|
|
|
184
184
|
```ts
|
|
185
185
|
import { atom, mutation } from "@kdeveloper/kvark";
|
|
186
186
|
|
|
187
187
|
const userAtom = atom({
|
|
188
188
|
dependencies: { userId: userIdAtom },
|
|
189
|
-
|
|
190
|
-
return ctx.
|
|
189
|
+
read: async (ctx) => {
|
|
190
|
+
return ctx.global.apiClient.users.get(await ctx.read("userId"));
|
|
191
191
|
},
|
|
192
|
-
|
|
193
|
-
await ctx.
|
|
192
|
+
write: async (ctx, patch: Partial<User>) => {
|
|
193
|
+
await ctx.global.apiClient.users.update(ctx.global.currentUser.id, patch);
|
|
194
194
|
},
|
|
195
195
|
});
|
|
196
196
|
|
|
197
197
|
const clockAtom = atom(Date.now(), {
|
|
198
|
-
onMount: (set, {
|
|
199
|
-
const id = setInterval(() => set(
|
|
198
|
+
onMount: (set, { global }) => {
|
|
199
|
+
const id = setInterval(() => set(global.apiClient.now()), 1000);
|
|
200
200
|
return () => clearInterval(id);
|
|
201
201
|
},
|
|
202
202
|
});
|
|
203
203
|
|
|
204
204
|
const saveUserMutation = mutation(async (ctx, patch: Partial<User>) => {
|
|
205
|
-
await ctx.
|
|
205
|
+
await ctx.global.apiClient.users.update(ctx.global.currentUser.id, patch);
|
|
206
206
|
});
|
|
207
207
|
```
|
|
208
208
|
|
|
@@ -211,20 +211,20 @@ const saveUserMutation = mutation(async (ctx, patch: Partial<User>) => {
|
|
|
211
211
|
```ts
|
|
212
212
|
const feedAtom = infinityAtom({
|
|
213
213
|
initialCursor: 0,
|
|
214
|
-
queryFn: async ({ cursor, signal,
|
|
215
|
-
return
|
|
214
|
+
queryFn: async ({ cursor, signal, global }) => {
|
|
215
|
+
return global.apiClient.feed.list({ cursor, signal });
|
|
216
216
|
},
|
|
217
217
|
getNextCursor: (lastPage) => lastPage.nextCursor,
|
|
218
218
|
});
|
|
219
219
|
```
|
|
220
220
|
|
|
221
|
-
Batch families receive it via `input.ctx.
|
|
221
|
+
Batch families receive it via `input.ctx.global` inside `batch.fetch`.
|
|
222
222
|
|
|
223
223
|
## Core Concepts
|
|
224
224
|
|
|
225
225
|
### Atoms
|
|
226
226
|
|
|
227
|
-
An atom is the smallest unit of state. Its `
|
|
227
|
+
An atom is the smallest unit of state. Its `read` function is always `async`, and dependencies must be declared explicitly.
|
|
228
228
|
|
|
229
229
|
```ts
|
|
230
230
|
import { atom } from "@kdeveloper/kvark";
|
|
@@ -232,16 +232,16 @@ import { atom } from "@kdeveloper/kvark";
|
|
|
232
232
|
// Primitive atom — no dependencies
|
|
233
233
|
const countAtom = atom({
|
|
234
234
|
debugLabel: "count",
|
|
235
|
-
|
|
236
|
-
|
|
235
|
+
read: async () => 0,
|
|
236
|
+
write: async (_ctx, _value: number) => {},
|
|
237
237
|
});
|
|
238
238
|
|
|
239
239
|
// Derived atom — reads from another atom
|
|
240
240
|
const doubleAtom = atom({
|
|
241
241
|
debugLabel: "double",
|
|
242
242
|
dependencies: { count: countAtom },
|
|
243
|
-
|
|
244
|
-
const n = await ctx.
|
|
243
|
+
read: async (ctx) => {
|
|
244
|
+
const n = await ctx.read("count");
|
|
245
245
|
return n * 2;
|
|
246
246
|
},
|
|
247
247
|
});
|
|
@@ -249,7 +249,7 @@ const doubleAtom = atom({
|
|
|
249
249
|
|
|
250
250
|
#### Simple primitive atom (`atom(initialValue)`)
|
|
251
251
|
|
|
252
|
-
For purely client-side state — counters, toggles, draft fields — there is a shorthand: pass an initial value directly. The result is a `WritableAtom` whose
|
|
252
|
+
For purely client-side state — counters, toggles, draft fields — there is a shorthand: pass an initial value directly. The result is a `WritableAtom` whose apply function accepts either the next value or an updater function `(prev) => next`. Both `store.apply(atom, value)` and `store.write(atom, value)` are supported: `apply` runs the atom's `write` callback, while `write` pushes straight into the cache. React/Preact/Vue hooks (`useAtomValue`, `useApplyAtom`, `useAtom`) work as usual.
|
|
253
253
|
|
|
254
254
|
```ts
|
|
255
255
|
import { atom, createStore } from "@kdeveloper/kvark";
|
|
@@ -268,27 +268,27 @@ const clockAtom = atom(Date.now(), {
|
|
|
268
268
|
|
|
269
269
|
const store = createStore();
|
|
270
270
|
|
|
271
|
-
await store.
|
|
272
|
-
await store.
|
|
271
|
+
await store.apply(counterAtom, 5);
|
|
272
|
+
await store.apply(counterAtom, (prev) => prev + 1);
|
|
273
273
|
store.write(counterAtom, 42);
|
|
274
274
|
```
|
|
275
275
|
|
|
276
|
-
The shorthand is intentionally limited to `debugLabel` and `onMount`; for `dependencies`, `stalePolicy`, `retry`, or async loading use the full `atom({
|
|
276
|
+
The shorthand is intentionally limited to `debugLabel` and `onMount`; for `dependencies`, `stalePolicy`, `retry`, or async loading use the full `atom({ read, write?, ... })` form.
|
|
277
277
|
|
|
278
|
-
> Note: if your initial value is itself an object that exposes a `
|
|
278
|
+
> Note: if your initial value is itself an object that exposes a `read` method (and would be parsed as an atom config), use the explicit form: `atom({ read: async () => obj, write: async (ctx, next) => ctx.setOptimisticValue(next) })`.
|
|
279
279
|
|
|
280
280
|
### Writable atoms
|
|
281
281
|
|
|
282
|
-
An atom is writable when its config includes a `
|
|
282
|
+
An atom is writable when its config includes a `write` function. The return type becomes `WritableAtom<Value, Args>`, where `Args` is the tuple of arguments accepted by `store.apply(atom, ...args)` / `useApplyAtom(atom)(...args)`.
|
|
283
283
|
|
|
284
284
|
```ts
|
|
285
285
|
const profileAtom = atom({
|
|
286
286
|
debugLabel: "profile",
|
|
287
|
-
|
|
287
|
+
read: async () => {
|
|
288
288
|
const res = await fetch("/api/profile");
|
|
289
289
|
return res.json() as Promise<Profile>;
|
|
290
290
|
},
|
|
291
|
-
|
|
291
|
+
write: async (ctx, patch: Partial<Profile>) => {
|
|
292
292
|
await fetch("/api/profile", {
|
|
293
293
|
method: "PATCH",
|
|
294
294
|
body: JSON.stringify(patch),
|
|
@@ -298,11 +298,11 @@ const profileAtom = atom({
|
|
|
298
298
|
});
|
|
299
299
|
```
|
|
300
300
|
|
|
301
|
-
**Lifecycle after `
|
|
301
|
+
**Lifecycle after `apply`:** the store calls `config.write(ctx, ...args)`, waits for the returned promise, then calls `invalidate` on the atom. This marks it `stale` and triggers a background refetch via `read` — the same stale-while-revalidate flow described [above](#stale-while-revalidate).
|
|
302
302
|
|
|
303
|
-
The `ctx` passed to `
|
|
303
|
+
The `ctx` passed to `write` provides:
|
|
304
304
|
|
|
305
|
-
- **`ctx.
|
|
305
|
+
- **`ctx.read(key)`** — read any declared dependency (same as in `read`).
|
|
306
306
|
- **`ctx.signal`** — an `AbortSignal` tied to the atom's lifecycle, useful for cancelling in-flight requests.
|
|
307
307
|
- **`ctx.setOptimisticValue(value)`** — synchronously update the atom's cached value before the async work completes (see below). Also accepts a mutator `(prev) => next`.
|
|
308
308
|
- **`ctx.writeOptimistic(atom, value)`** — synchronously write into another atom's cache with automatic rollback on error (see [Dependent mutations](#dependent-mutations)). Also accepts a mutator.
|
|
@@ -310,16 +310,16 @@ The `ctx` passed to `set` provides:
|
|
|
310
310
|
|
|
311
311
|
#### Optimistic updates
|
|
312
312
|
|
|
313
|
-
Call `ctx.setOptimisticValue(value)` inside `
|
|
313
|
+
Call `ctx.setOptimisticValue(value)` inside `write` to immediately reflect the new value in the UI while the mutation runs in the background. If the mutation throws (or the signal aborts), the store automatically rolls back to the state captured before the first `setOptimisticValue` call. Derived atoms that depend on this atom are marked `stale` so they re-render too.
|
|
314
314
|
|
|
315
315
|
```ts
|
|
316
316
|
const todoAtom = atom({
|
|
317
317
|
debugLabel: "todo",
|
|
318
|
-
|
|
318
|
+
read: async () => {
|
|
319
319
|
const res = await fetch("/api/todo");
|
|
320
320
|
return res.json() as Promise<Todo>;
|
|
321
321
|
},
|
|
322
|
-
|
|
322
|
+
write: async (ctx, title: string) => {
|
|
323
323
|
ctx.setOptimisticValue({ title, done: false });
|
|
324
324
|
await fetch("/api/todo", {
|
|
325
325
|
method: "PUT",
|
|
@@ -333,17 +333,17 @@ const todoAtom = atom({
|
|
|
333
333
|
`setOptimisticValue` also accepts a mutator function — the callback receives the previous value (or `undefined` if pending) and returns the next one:
|
|
334
334
|
|
|
335
335
|
```ts
|
|
336
|
-
|
|
336
|
+
write: async (ctx) => {
|
|
337
337
|
ctx.setOptimisticValue((prev) => ({ ...prev, loading: true }));
|
|
338
338
|
await fetch("/api/update", { signal: ctx.signal });
|
|
339
339
|
},
|
|
340
340
|
```
|
|
341
341
|
|
|
342
|
-
If the `PUT` fails, the atom reverts to whatever value `
|
|
342
|
+
If the `PUT` fails, the atom reverts to whatever value `read` had loaded before the optimistic update — no manual rollback needed.
|
|
343
343
|
|
|
344
344
|
#### Dependent mutations
|
|
345
345
|
|
|
346
|
-
The `ctx` inside `
|
|
346
|
+
The `ctx` inside `write` also provides methods for cross-atom side-effects:
|
|
347
347
|
|
|
348
348
|
- **`ctx.invalidate(atom)`** — mark another atom as `stale` and schedule a refetch. Does **not** participate in rollback; best called after the async work succeeds.
|
|
349
349
|
- **`ctx.invalidateMany(atoms)`** — same as `invalidate`, but for multiple atoms at once.
|
|
@@ -354,7 +354,7 @@ A common pattern is a list atom alongside per-item atoms managed by `atomFamily`
|
|
|
354
354
|
```ts
|
|
355
355
|
const todosListAtom = atom({
|
|
356
356
|
debugLabel: "todosList",
|
|
357
|
-
|
|
357
|
+
read: async () => {
|
|
358
358
|
const res = await fetch("/api/todos");
|
|
359
359
|
return res.json() as Promise<Todo[]>;
|
|
360
360
|
},
|
|
@@ -362,11 +362,11 @@ const todosListAtom = atom({
|
|
|
362
362
|
|
|
363
363
|
const todoAtom = atomFamily({
|
|
364
364
|
debugLabel: "todo",
|
|
365
|
-
|
|
365
|
+
read: (id: number) => async () => {
|
|
366
366
|
const res = await fetch(`/api/todos/${id}`);
|
|
367
367
|
return res.json() as Promise<Todo>;
|
|
368
368
|
},
|
|
369
|
-
|
|
369
|
+
write: (id: number) => async (ctx, patch: Partial<Todo>) => {
|
|
370
370
|
ctx.setOptimisticValue((prev) => ({ ...prev!, ...patch }));
|
|
371
371
|
ctx.writeOptimistic(todosListAtom, (list) =>
|
|
372
372
|
list?.map((t) => (t.id === id ? { ...t, ...patch } : t)),
|
|
@@ -387,20 +387,20 @@ If the `PATCH` fails, both the item and the list revert to their pre-optimistic
|
|
|
387
387
|
|
|
388
388
|
Sometimes a server action updates **several** atoms at once, and there is no single writable atom that should own the mutation. Use **`mutation(fn)`** to define that work once, then run it with **`store.mutate(m, ...args)`** or **`useMutation(m)`** in components.
|
|
389
389
|
|
|
390
|
-
The callback receives a **`MutationContext`** (the same optimistic and invalidation helpers as in `
|
|
390
|
+
The callback receives a **`MutationContext`** (the same optimistic and invalidation helpers as in `write`, but **without** `setOptimisticValue` or `ctx.signal`). Standalone mutations can read any atom with **`ctx.read(atom)`**. In contrast, inside **`write`**, **`ctx.read(...)`** still means "read a declared dependency by key":
|
|
391
391
|
|
|
392
|
-
- **`ctx.
|
|
393
|
-
- **`ctx.writeOptimistic(atom, value | mutator)`** — same semantics as inside `
|
|
392
|
+
- **`ctx.read(atom)`** — read any atom with the same semantics as `store.read(atom)` / `client.read(atom)`.
|
|
393
|
+
- **`ctx.writeOptimistic(atom, value | mutator)`** — same semantics as inside `write`. On throw, every atom touched by `writeOptimistic` rolls back to its state before the first optimistic write in this run.
|
|
394
394
|
- **`ctx.invalidate` / `ctx.invalidateMany`** — mark atoms stale (not rolled back on error).
|
|
395
395
|
|
|
396
|
-
Unlike **`
|
|
396
|
+
Unlike **`write`**, a successful **`mutate` does not invalidate any atom automatically**. Call `ctx.invalidate` / `ctx.invalidateMany` when you want stale-while-revalidate after the request completes.
|
|
397
397
|
|
|
398
398
|
```ts
|
|
399
399
|
import { atom, mutation, createStore } from "@kdeveloper/kvark";
|
|
400
400
|
|
|
401
401
|
const listAtom = atom({
|
|
402
402
|
debugLabel: "list",
|
|
403
|
-
|
|
403
|
+
read: async () => {
|
|
404
404
|
const res = await fetch("/api/items");
|
|
405
405
|
return res.json() as Promise<string[]>;
|
|
406
406
|
},
|
|
@@ -408,7 +408,7 @@ const listAtom = atom({
|
|
|
408
408
|
|
|
409
409
|
const countAtom = atom({
|
|
410
410
|
debugLabel: "count",
|
|
411
|
-
|
|
411
|
+
read: async () => {
|
|
412
412
|
const res = await fetch("/api/count");
|
|
413
413
|
return (await res.json()) as { n: number };
|
|
414
414
|
},
|
|
@@ -452,22 +452,22 @@ const runAddItem = useMutation(addItem);
|
|
|
452
452
|
|
|
453
453
|
Both can update an atom's cached value, but they serve different purposes:
|
|
454
454
|
|
|
455
|
-
| | `
|
|
456
|
-
| ---------------- |
|
|
457
|
-
| **Triggered by** | Explicit call (`store.
|
|
458
|
-
| **After update** | `invalidate` → refetch via `
|
|
459
|
-
| **Use case** | Mutations, API calls, optimistic updates
|
|
455
|
+
| | `write` | `onMount` |
|
|
456
|
+
| ---------------- | ---------------------------------------------- | -------------------------------------- |
|
|
457
|
+
| **Triggered by** | Explicit call (`store.apply`, `useApplyAtom`) | First subscriber mounts |
|
|
458
|
+
| **After update** | `invalidate` → refetch via `read` | No refetch — value stays as-is |
|
|
459
|
+
| **Use case** | Mutations, API calls, optimistic updates | Timers, subscriptions, imperative push |
|
|
460
460
|
|
|
461
461
|
### `onMount`
|
|
462
462
|
|
|
463
|
-
Optional lifecycle hook that runs when the atom **first gains a subscriber** in a store (for example when a component using `useAtomValue` mounts). It receives a synchronous `set(value)` that marks the atom `fresh` and notifies listeners — useful for timers, subscriptions, or imperative updates that should not go through `
|
|
463
|
+
Optional lifecycle hook that runs when the atom **first gains a subscriber** in a store (for example when a component using `useAtomValue` mounts). It receives a synchronous `set(value)` that marks the atom `fresh` and notifies listeners — useful for timers, subscriptions, or imperative updates that should not go through `read`.
|
|
464
464
|
|
|
465
465
|
You may return a cleanup function; it runs when the **last** subscriber unsubscribes (for example when the last mounted consumer unmounts). If several components subscribe to the same atom, `onMount` runs once and the cleanup runs once after all of them unsubscribe.
|
|
466
466
|
|
|
467
467
|
```ts
|
|
468
468
|
const clockAtom = atom({
|
|
469
469
|
debugLabel: "clock",
|
|
470
|
-
|
|
470
|
+
read: async () => new Date().toISOString(),
|
|
471
471
|
onMount: (set) => {
|
|
472
472
|
const id = setInterval(() => {
|
|
473
473
|
set(new Date().toISOString());
|
|
@@ -479,13 +479,13 @@ const clockAtom = atom({
|
|
|
479
479
|
|
|
480
480
|
### Parallel loading
|
|
481
481
|
|
|
482
|
-
Declaring multiple dependencies causes the Store to
|
|
482
|
+
Declaring multiple dependencies causes the Store to start loading them in parallel before calling `read`. Inside `read` you control the parallelism explicitly.
|
|
483
483
|
|
|
484
484
|
```ts
|
|
485
485
|
const dashboardAtom = atom({
|
|
486
486
|
dependencies: { user: userAtom, settings: settingsAtom },
|
|
487
|
-
|
|
488
|
-
const [user, settings] = await Promise.all([ctx.
|
|
487
|
+
read: async (ctx) => {
|
|
488
|
+
const [user, settings] = await Promise.all([ctx.read("user"), ctx.read("settings")]);
|
|
489
489
|
return { user, settings };
|
|
490
490
|
},
|
|
491
491
|
});
|
|
@@ -528,13 +528,13 @@ Available `stalePolicy` values:
|
|
|
528
528
|
|
|
529
529
|
### Retry on error
|
|
530
530
|
|
|
531
|
-
By default a failed `
|
|
531
|
+
By default a failed `read` immediately sets the atom to `error` state. You can opt into automatic retries with the `retry` and `retryDelay` options:
|
|
532
532
|
|
|
533
533
|
```ts
|
|
534
534
|
const userAtom = atom({
|
|
535
535
|
retry: 3,
|
|
536
536
|
retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30_000),
|
|
537
|
-
|
|
537
|
+
read: async (ctx) => {
|
|
538
538
|
const res = await fetch("/api/user", { signal: ctx.signal });
|
|
539
539
|
if (!res.ok) throw new Error("Failed to fetch user");
|
|
540
540
|
return res.json();
|
|
@@ -553,7 +553,7 @@ The same options are available in `atomFamily` and `infinityAtom`.
|
|
|
553
553
|
|
|
554
554
|
### `atomFamily`
|
|
555
555
|
|
|
556
|
-
Create a family of atoms parametrised by a key.
|
|
556
|
+
Create a family of atoms parametrised by a key. Members use `read(param)` and optional `write(param)` callbacks, are cached by param, and support LRU eviction.
|
|
557
557
|
|
|
558
558
|
```ts
|
|
559
559
|
import { atomFamily } from "@kdeveloper/kvark/family";
|
|
@@ -563,8 +563,8 @@ const postFamily = atomFamily({
|
|
|
563
563
|
cachePolicy: "lru",
|
|
564
564
|
lruSize: 50,
|
|
565
565
|
dependencies: (_postId: number) => ({ user: userAtom }),
|
|
566
|
-
|
|
567
|
-
const user = await ctx.
|
|
566
|
+
read: (postId) => async (ctx) => {
|
|
567
|
+
const user = await ctx.read("user");
|
|
568
568
|
const res = await fetch(`/api/posts/${postId}?userId=${user.id}`, {
|
|
569
569
|
signal: ctx.signal,
|
|
570
570
|
});
|
|
@@ -585,20 +585,20 @@ postFamily.invalidateAll();
|
|
|
585
585
|
|
|
586
586
|
### `atomFamily` as a dependency
|
|
587
587
|
|
|
588
|
-
A `dependencies` entry can be either a regular `Atom` or an `atomFamily`. When the entry is a family, supply the param at the call site as the **second positional argument** to `ctx.
|
|
588
|
+
A `dependencies` entry can be either a regular `Atom` or an `atomFamily`. When the entry is a family, supply the param at the call site as the **second positional argument** to `ctx.read`. Each unique param maps to its own family member; reverse-dependency edges are wired up automatically, so invalidating any read member (or the whole family) propagates `stale` to the consumer.
|
|
589
589
|
|
|
590
590
|
```ts
|
|
591
591
|
import { atom } from "@kdeveloper/kvark";
|
|
592
592
|
import { atomFamily } from "@kdeveloper/kvark/family";
|
|
593
593
|
|
|
594
594
|
const userFamily = atomFamily({
|
|
595
|
-
|
|
595
|
+
read: (id: number) => async () => fetch(`/api/users/${id}`).then((r) => r.json()),
|
|
596
596
|
});
|
|
597
597
|
|
|
598
598
|
const profileAtom = atom({
|
|
599
599
|
dependencies: { user: userFamily },
|
|
600
|
-
|
|
601
|
-
const user = await ctx.
|
|
600
|
+
read: async (ctx) => {
|
|
601
|
+
const user = await ctx.read("user", 7); // reads userFamily(7)
|
|
602
602
|
return `profile-of-${user.name}`;
|
|
603
603
|
},
|
|
604
604
|
});
|
|
@@ -607,31 +607,31 @@ const profileAtom = atom({
|
|
|
607
607
|
userFamily.invalidate(7); // marks profileAtom stale
|
|
608
608
|
```
|
|
609
609
|
|
|
610
|
-
The signature of `ctx.
|
|
610
|
+
The signature of `ctx.read` is keyed on the dependency entry:
|
|
611
611
|
|
|
612
612
|
| Entry | Call form | Compile-time errors |
|
|
613
613
|
| ------------ | --------------------- | ---------------------------------------------------- |
|
|
614
|
-
| `Atom<V>` | `ctx.
|
|
615
|
-
| `atomFamily` | `ctx.
|
|
614
|
+
| `Atom<V>` | `ctx.read(key)` | passing a second arg → error |
|
|
615
|
+
| `atomFamily` | `ctx.read(key, param)` | omitting `param` → error; wrong `param` type → error |
|
|
616
616
|
|
|
617
617
|
```ts
|
|
618
618
|
const a = atom({
|
|
619
619
|
dependencies: { base: countAtom, user: userFamily },
|
|
620
|
-
|
|
621
|
-
await ctx.
|
|
622
|
-
await ctx.
|
|
620
|
+
read: async (ctx) => {
|
|
621
|
+
await ctx.read("base"); // ok
|
|
622
|
+
await ctx.read("user", 1); // ok
|
|
623
623
|
|
|
624
624
|
// @ts-expect-error family-key requires a parameter
|
|
625
|
-
ctx.
|
|
625
|
+
ctx.read("user");
|
|
626
626
|
// @ts-expect-error plain atom-key does not accept a parameter
|
|
627
|
-
ctx.
|
|
627
|
+
ctx.read("base", 1);
|
|
628
628
|
// @ts-expect-error param type must match the family's `Param`
|
|
629
|
-
ctx.
|
|
629
|
+
ctx.read("user", "not-a-number");
|
|
630
630
|
},
|
|
631
631
|
});
|
|
632
632
|
```
|
|
633
633
|
|
|
634
|
-
Repeated reads with the same `param` are deduplicated through the in-flight promise of the
|
|
634
|
+
Repeated reads with the same `param` are deduplicated through the in-flight promise of the already-read family member; reads with different params use distinct atoms and run independently. Family-typed dependencies are **not** preloaded before `read` runs (the param is only known at the `ctx.read` call site), so unread members never trigger a fetch.
|
|
635
635
|
|
|
636
636
|
### Object params and `paramKey`
|
|
637
637
|
|
|
@@ -643,14 +643,14 @@ import { atomFamily, stableFamilyKey } from "@kdeveloper/kvark/family";
|
|
|
643
643
|
const searchFamily = atomFamily({
|
|
644
644
|
// Two different object literals with the same fields → same atom
|
|
645
645
|
paramKey: (filters) => stableFamilyKey(filters),
|
|
646
|
-
|
|
646
|
+
read: (filters) => async () => fetchResults(filters),
|
|
647
647
|
});
|
|
648
648
|
|
|
649
649
|
searchFamily({ page: 1, query: "hello" }); // creates atom, key = '{"page":1,"query":"hello"}'
|
|
650
650
|
searchFamily({ query: "hello", page: 1 }); // returns the same atom (fields are sorted)
|
|
651
651
|
```
|
|
652
652
|
|
|
653
|
-
The `paramKey` function is called once per `family(param)` invocation. The returned key is used for all cache operations (`
|
|
653
|
+
The `paramKey` function is called once per `family(param)` invocation. The returned key is used for all cache operations (`read`, `apply`, `invalidate`, `remove`, LRU). The original `param` is still passed to `read`, `write`, and `dependencies`.
|
|
654
654
|
|
|
655
655
|
#### `stableFamilyKey(value)`
|
|
656
656
|
|
|
@@ -663,7 +663,7 @@ A built-in helper that serialises plain objects and arrays into a deterministic
|
|
|
663
663
|
|
|
664
664
|
### Batching (`atomFamily` with `batch`)
|
|
665
665
|
|
|
666
|
-
Instead of fetching each atom individually, you can batch multiple concurrent requests into a single call — inspired by [`@yornaath/batshit`](https://www.npmjs.com/package/@yornaath/batshit). Replace `
|
|
666
|
+
Instead of fetching each atom individually, you can batch multiple concurrent requests into a single call — inspired by [`@yornaath/batshit`](https://www.npmjs.com/package/@yornaath/batshit). Replace `read` with `batch`:
|
|
667
667
|
|
|
668
668
|
```ts
|
|
669
669
|
import { atomFamily, windowScheduler } from "@kdeveloper/kvark/family";
|
|
@@ -674,7 +674,7 @@ const userFamily = atomFamily({
|
|
|
674
674
|
batch: {
|
|
675
675
|
scheduler: windowScheduler(10),
|
|
676
676
|
fetch: async ({ keys, params, signal, ctx }) => {
|
|
677
|
-
const auth = await ctx.
|
|
677
|
+
const auth = await ctx.read("auth");
|
|
678
678
|
const res = await fetch(`/api/users?ids=${keys.join(",")}`, {
|
|
679
679
|
headers: { Authorization: auth.token },
|
|
680
680
|
signal,
|
|
@@ -685,13 +685,13 @@ const userFamily = atomFamily({
|
|
|
685
685
|
},
|
|
686
686
|
});
|
|
687
687
|
|
|
688
|
-
// Each call returns an atom; concurrent
|
|
688
|
+
// Each call returns an atom; concurrent reads within the scheduler
|
|
689
689
|
// window are batched into a single fetch.
|
|
690
690
|
const alice = useAtomValue(userFamily(1));
|
|
691
691
|
const bob = useAtomValue(userFamily(2));
|
|
692
692
|
```
|
|
693
693
|
|
|
694
|
-
**How it works:** when multiple atoms are
|
|
694
|
+
**How it works:** when multiple atoms are read concurrently (e.g. several components mount at once), each atom's `read` enqueues its key into the batch coordinator. The scheduler decides when to flush the queue: the default `microtaskScheduler` flushes at the end of the current microtask; `windowScheduler(ms)` waits up to `ms` milliseconds.
|
|
695
695
|
|
|
696
696
|
`batch.fetch` receives:
|
|
697
697
|
|
|
@@ -762,9 +762,9 @@ type InfiniteData<Page, Cursor> = {
|
|
|
762
762
|
};
|
|
763
763
|
```
|
|
764
764
|
|
|
765
|
-
Call `store.
|
|
765
|
+
Call `store.apply(projectsAtom, "loadNext")` (or `useApplyAtom(projectsAtom)("loadNext")`) to fetch and append the next page. If `hasNextPage` is `false`, the call is a no-op.
|
|
766
766
|
|
|
767
|
-
When the atom is invalidated, `
|
|
767
|
+
When the atom is invalidated, `read` re-fetches every cached page **sequentially** by replaying the stored `pageCursors` — the same stale-while-revalidate flow as regular atoms.
|
|
768
768
|
|
|
769
769
|
Use `maxPages` to cap the number of pages held in memory and re-fetched on invalidation:
|
|
770
770
|
|
|
@@ -799,8 +799,8 @@ const userPostsFamily = infinityAtomFamily({
|
|
|
799
799
|
});
|
|
800
800
|
|
|
801
801
|
const aliceFeed = useAtomValue(userPostsFamily(1));
|
|
802
|
-
const
|
|
803
|
-
await
|
|
802
|
+
const applyAliceFeed = useApplyAtom(userPostsFamily(1));
|
|
803
|
+
await applyAliceFeed("loadNext");
|
|
804
804
|
|
|
805
805
|
userPostsFamily.invalidate(1);
|
|
806
806
|
userPostsFamily.invalidateAll();
|
|
@@ -814,7 +814,7 @@ All hooks must be used inside a `<Provider>`.
|
|
|
814
814
|
|
|
815
815
|
### `useStore`
|
|
816
816
|
|
|
817
|
-
Returns the `Store` instance from context — for advanced cases (e.g. calling `store.
|
|
817
|
+
Returns the `Store` instance from context — for advanced cases (e.g. calling `store.read` in async setup patterns) or when you need the store outside atom helpers.
|
|
818
818
|
|
|
819
819
|
### `useAtomValue`
|
|
820
820
|
|
|
@@ -828,13 +828,13 @@ const user = useAtomValue(userAtom);
|
|
|
828
828
|
const { value, isStale, error } = useAtomValue(userAtom, { observe: true });
|
|
829
829
|
```
|
|
830
830
|
|
|
831
|
-
### `
|
|
831
|
+
### `useApplyAtom`
|
|
832
832
|
|
|
833
|
-
Returns
|
|
833
|
+
Returns a stable function that calls `store.apply` for a writable atom, without subscribing to the value.
|
|
834
834
|
|
|
835
835
|
```tsx
|
|
836
|
-
const
|
|
837
|
-
await
|
|
836
|
+
const applyCount = useApplyAtom(countAtom);
|
|
837
|
+
await applyCount(42);
|
|
838
838
|
```
|
|
839
839
|
|
|
840
840
|
### `useMutation`
|
|
@@ -848,10 +848,10 @@ await runReorder(itemId, newIndex);
|
|
|
848
848
|
|
|
849
849
|
### `useAtom`
|
|
850
850
|
|
|
851
|
-
Combines `useAtomValue` and `
|
|
851
|
+
Combines `useAtomValue` and `useApplyAtom` into a `[value, apply]` tuple.
|
|
852
852
|
|
|
853
853
|
```tsx
|
|
854
|
-
const [count,
|
|
854
|
+
const [count, applyCount] = useAtom(countAtom);
|
|
855
855
|
```
|
|
856
856
|
|
|
857
857
|
### `useAtomContext`
|
|
@@ -860,7 +860,7 @@ Imperative access to the `StoreClient` inside a callback. Does not subscribe.
|
|
|
860
860
|
|
|
861
861
|
```tsx
|
|
862
862
|
const readBalance = useAtomContext(async (client) => {
|
|
863
|
-
return client.
|
|
863
|
+
return client.read(balanceAtom);
|
|
864
864
|
});
|
|
865
865
|
|
|
866
866
|
// Call imperatively, e.g. in an event handler
|
|
@@ -869,21 +869,21 @@ const balance = await readBalance();
|
|
|
869
869
|
|
|
870
870
|
## Preact (`@kdeveloper/kvark/preact`)
|
|
871
871
|
|
|
872
|
-
Same hook names, signatures, and behaviour as the React integration: `Provider`, `useStore`, `useAtomValue`, `
|
|
872
|
+
Same hook names, signatures, and behaviour as the React integration: `Provider`, `useStore`, `useAtomValue`, `useApplyAtom`, `useMutation`, `useAtom`, `useAtomContext`. All hooks must be used inside a `<Provider>`.
|
|
873
873
|
|
|
874
874
|
Internally the entry imports only from `preact` and `preact/hooks` — there is **no dependency on `preact/compat`**, so your app does not need any React compatibility aliases.
|
|
875
875
|
|
|
876
876
|
## Vue 3 (`@kdeveloper/kvark/vue`)
|
|
877
877
|
|
|
878
|
-
Same composable names and behaviour as React: `Provider`, `useStore`, `useAtomValue`, `
|
|
878
|
+
Same composable names and behaviour as React: `Provider`, `useStore`, `useAtomValue`, `useApplyAtom`, `useMutation`, `useAtom`, `useAtomContext`. Wrap your app (or subtree) with `Provider` and pass `:store="store"`.
|
|
879
879
|
|
|
880
880
|
Exported types: **`ThenableShallowRef<V>`** (default `useAtomValue`), **`ThenableObservedShallowRef<V>`** and **`ObservedValue<V>`** (with `{ observe: true }`). They encode pending vs resolved ref shapes for TypeScript.
|
|
881
881
|
|
|
882
|
-
`useAtomValue` / `useAtom` expose values as **awaitable shallow refs** (`PromiseLike`) — in script code use `.value`; in templates Vue unwraps refs for you. `await useAtomValue(atom)` in an async setup suspends until the atom's first `
|
|
882
|
+
`useAtomValue` / `useAtom` expose values as **awaitable shallow refs** (`PromiseLike`) — in script code use `.value`; in templates Vue unwraps refs for you. `await useAtomValue(atom)` in an async setup suspends until the atom's first `read` resolves, integrating with `<Suspense>`.
|
|
883
883
|
|
|
884
884
|
### Suspense (Vue 3)
|
|
885
885
|
|
|
886
|
-
Vue’s `<Suspense>` boundary applies to **async** components (e.g. **`async setup`** or [top-level `await` in `<script setup>`](https://vuejs.org/guide/built-ins/suspense.html#async-setup)), not to composables alone. Because `useAtomValue` and `useAtom` return `PromiseLike` refs, you can simply `await` them to suspend until the first `
|
|
886
|
+
Vue’s `<Suspense>` boundary applies to **async** components (e.g. **`async setup`** or [top-level `await` in `<script setup>`](https://vuejs.org/guide/built-ins/suspense.html#async-setup)), not to composables alone. Because `useAtomValue` and `useAtom` return `PromiseLike` refs, you can simply `await` them to suspend until the first `read` resolves.
|
|
887
887
|
|
|
888
888
|
`atoms.ts`
|
|
889
889
|
|
|
@@ -891,7 +891,7 @@ Vue’s `<Suspense>` boundary applies to **async** components (e.g. **`async set
|
|
|
891
891
|
import { atom } from "@kdeveloper/kvark";
|
|
892
892
|
|
|
893
893
|
export const slowAtom = atom({
|
|
894
|
-
|
|
894
|
+
read: async () => {
|
|
895
895
|
await new Promise((r) => setTimeout(r, 50));
|
|
896
896
|
return "ready";
|
|
897
897
|
},
|
|
@@ -939,7 +939,7 @@ const store = createStore();
|
|
|
939
939
|
</template>
|
|
940
940
|
```
|
|
941
941
|
|
|
942
|
-
Alternatively, use **`defineComponent({ async setup() { ... } })`** and `await useAtomValue(slowAtom)` before returning the render function — the same pattern as in `test/vue/hooks.test.ts`. `useAtom` is also awaitable: `const [ref,
|
|
942
|
+
Alternatively, use **`defineComponent({ async setup() { ... } })`** and `await useAtomValue(slowAtom)` before returning the render function — the same pattern as in `test/vue/hooks.test.ts`. `useAtom` is also awaitable: `const [ref, apply] = await useAtom(writableAtom)`.
|
|
943
943
|
|
|
944
944
|
## External Invalidation
|
|
945
945
|
|
|
@@ -982,7 +982,7 @@ const unsub = client.subscribe(userAtom, (state) => {
|
|
|
982
982
|
|
|
983
983
|
### Direct Write
|
|
984
984
|
|
|
985
|
-
When the server pushes a complete, authoritative value (e.g. via WebSocket or SSE), use `client.write` to store it directly — no refetch through `
|
|
985
|
+
When the server pushes a complete, authoritative value (e.g. via WebSocket or SSE), use `store.write` / `client.write` to store it directly — no refetch through `read` is triggered, and the atom's `write` callback is not invoked. Any in-flight `read` for the atom is aborted, and derived atoms that depend on it are marked `stale` so they re-compute.
|
|
986
986
|
|
|
987
987
|
```ts
|
|
988
988
|
ws.addEventListener("message", (event) => {
|
|
@@ -1001,18 +1001,18 @@ client.write(counterAtom, (prev) => (prev ?? 0) + 1);
|
|
|
1001
1001
|
|
|
1002
1002
|
> If `V` itself is a function type, pass a mutator that returns it: `write(atom, () => myFn)`.
|
|
1003
1003
|
|
|
1004
|
-
`write` works on any `Atom<V>` — the atom does not need a `
|
|
1004
|
+
`write` works on any `Atom<V>` — the atom does not need a `write` config. This makes it the right tool when you already have the final value and want to skip a redundant network round-trip. Use `apply` when you want to run the atom's async `write` callback instead.
|
|
1005
1005
|
|
|
1006
1006
|
### `StoreClient` interface
|
|
1007
1007
|
|
|
1008
|
-
The concrete `Store` class exposes the same surface as `getClient()` — e.g. `store.mutate(m, ...args)`
|
|
1008
|
+
The concrete `Store` class exposes the same surface as `getClient()` — e.g. `store.read(atom)` / `client.read(atom)`, `store.apply(atom, ...args)` / `client.apply(atom, ...args)`, and `store.mutate(m, ...args)` / `client.mutate(m, ...args)`.
|
|
1009
1009
|
|
|
1010
1010
|
```ts
|
|
1011
1011
|
import type { Atom, WritableAtom, AtomState, Mutation } from "@kdeveloper/kvark";
|
|
1012
1012
|
|
|
1013
1013
|
interface StoreClient {
|
|
1014
|
-
|
|
1015
|
-
|
|
1014
|
+
read<V>(atom: Atom<V>): Promise<V>;
|
|
1015
|
+
apply<V, A extends readonly unknown[]>(atom: WritableAtom<V, A>, ...args: A): Promise<void>;
|
|
1016
1016
|
mutate<Args extends readonly unknown[]>(mutation: Mutation<Args>, ...args: Args): Promise<void>;
|
|
1017
1017
|
write<V>(atom: Atom<V>, value: V): void;
|
|
1018
1018
|
write<V>(atom: Atom<V>, mutate: (prev: V | undefined) => V): void;
|
|
@@ -1071,16 +1071,16 @@ type PostArgs = AtomArgs<typeof postAtom>; // → [postId: number]
|
|
|
1071
1071
|
type Writable = IsWritable<typeof countAtom>; // → true | false
|
|
1072
1072
|
```
|
|
1073
1073
|
|
|
1074
|
-
`WritableAtomContext` extends `MutationContext` — the same `writeOptimistic` / `invalidate` / `invalidateMany` helpers appear on both standalone mutations and writable `
|
|
1074
|
+
`WritableAtomContext` extends `MutationContext` — the same `writeOptimistic` / `invalidate` / `invalidateMany` helpers appear on both standalone mutations and writable `write` callbacks.
|
|
1075
1075
|
|
|
1076
1076
|
## Package Structure
|
|
1077
1077
|
|
|
1078
1078
|
| Import | Contents |
|
|
1079
1079
|
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1080
1080
|
| `@kdeveloper/kvark` | `atom`, `infinityAtom`, `infinityAtomFamily`, `mutation`, `createStore`, all types (including `Mutation`, `MutationContext`) |
|
|
1081
|
-
| `@kdeveloper/kvark/react` | `Provider`, `useStore`, `useAtomValue`, `
|
|
1082
|
-
| `@kdeveloper/kvark/preact` | `Provider`, `useStore`, `useAtomValue`, `
|
|
1083
|
-
| `@kdeveloper/kvark/vue` | `Provider`, `useStore`, `useAtomValue`, `
|
|
1081
|
+
| `@kdeveloper/kvark/react` | `Provider`, `useStore`, `useAtomValue`, `useApplyAtom`, `useMutation`, `useAtom`, `useAtomContext` |
|
|
1082
|
+
| `@kdeveloper/kvark/preact` | `Provider`, `useStore`, `useAtomValue`, `useApplyAtom`, `useMutation`, `useAtom`, `useAtomContext` |
|
|
1083
|
+
| `@kdeveloper/kvark/vue` | `Provider`, `useStore`, `useAtomValue`, `useApplyAtom`, `useMutation`, `useAtom`, `useAtomContext`; types `ThenableShallowRef`, `ThenableObservedShallowRef`, `ObservedValue` |
|
|
1084
1084
|
| `@kdeveloper/kvark/family` | `atomFamily`, `infinityAtomFamily`, `stableFamilyKey`, `microtaskScheduler`, `windowScheduler`, `maxBatchSizeScheduler`, `windowedFiniteBatchScheduler`, re-exports `atom`, `infinityAtom`, `mutation`, `createStore`; types include `AtomFamily`, `InfinityAtomFamily`, `BatchScheduler`, `BatchFetchInput`, `Mutation`, `MutationContext`, and core atom types |
|
|
1085
1085
|
|
|
1086
1086
|
The core (`@kdeveloper/kvark`) has **zero runtime dependencies**. **React**, **Preact**, and **Vue** are optional peer dependencies — install the framework you use and import from `/react`, `/preact`, or `/vue`.
|