@kdeveloper/kvark 0.1.1 → 0.3.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.
Files changed (2) hide show
  1. package/README.md +85 -3
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -39,7 +39,7 @@ import { Provider, useAtomValue, useSetAtom } from "@kdeveloper/kvark/react";
39
39
  const userIdAtom = atom({
40
40
  debugLabel: "userId",
41
41
  get: async () => 1,
42
- set: async (_ctx, id: number) => id,
42
+ set: async (_ctx, _id: number) => {},
43
43
  });
44
44
 
45
45
  const userAtom = atom({
@@ -86,7 +86,7 @@ import { atom } from "@kdeveloper/kvark";
86
86
  const countAtom = atom({
87
87
  debugLabel: "count",
88
88
  get: async () => 0,
89
- set: async (_ctx, value: number) => value,
89
+ set: async (_ctx, _value: number) => {},
90
90
  });
91
91
 
92
92
  // Derived atom — reads from another atom
@@ -100,6 +100,88 @@ const doubleAtom = atom({
100
100
  });
101
101
  ```
102
102
 
103
+ ### Writable atoms
104
+
105
+ An atom is writable when its config includes a `set` function. The return type becomes `WritableAtom<Value, Args>`, where `Args` is the tuple of arguments the setter accepts.
106
+
107
+ ```ts
108
+ const profileAtom = atom({
109
+ debugLabel: "profile",
110
+ get: async () => {
111
+ const res = await fetch("/api/profile");
112
+ return res.json() as Promise<Profile>;
113
+ },
114
+ set: async (ctx, patch: Partial<Profile>) => {
115
+ await fetch("/api/profile", {
116
+ method: "PATCH",
117
+ body: JSON.stringify(patch),
118
+ signal: ctx.signal,
119
+ });
120
+ },
121
+ });
122
+ ```
123
+
124
+ **Lifecycle after `set`:** the store calls `config.set(ctx, ...args)`, waits for the returned promise, then calls `invalidate` on the atom. This marks it `stale` and triggers a background refetch via `get` — the same stale-while-revalidate flow described [above](#stale-while-revalidate).
125
+
126
+ The `ctx` passed to `set` provides:
127
+
128
+ - **`ctx.get(key)`** — read any declared dependency (same as in `get`).
129
+ - **`ctx.signal`** — an `AbortSignal` tied to the atom's lifecycle, useful for cancelling in-flight requests.
130
+ - **`ctx.setOptimisticValue(value)`** — synchronously update the atom's cached value before the async work completes (see below).
131
+
132
+ #### Optimistic updates
133
+
134
+ Call `ctx.setOptimisticValue(value)` inside `set` 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.
135
+
136
+ ```ts
137
+ const todoAtom = atom({
138
+ debugLabel: "todo",
139
+ get: async () => {
140
+ const res = await fetch("/api/todo");
141
+ return res.json() as Promise<Todo>;
142
+ },
143
+ set: async (ctx, title: string) => {
144
+ ctx.setOptimisticValue({ title, done: false });
145
+ await fetch("/api/todo", {
146
+ method: "PUT",
147
+ body: JSON.stringify({ title }),
148
+ signal: ctx.signal,
149
+ });
150
+ },
151
+ });
152
+ ```
153
+
154
+ If the `PUT` fails, the atom reverts to whatever value `get` had loaded before the optimistic update — no manual rollback needed.
155
+
156
+ #### Writable atoms vs `onMount`
157
+
158
+ Both can update an atom's cached value, but they serve different purposes:
159
+
160
+ | | `set` | `onMount` |
161
+ |-|-------|-----------|
162
+ | **Triggered by** | Explicit call (`store.set`, `useSetAtom`) | First subscriber mounts |
163
+ | **After update** | `invalidate` → refetch via `get` | No refetch — value stays as-is |
164
+ | **Use case** | Mutations, API calls, optimistic updates | Timers, subscriptions, imperative push |
165
+
166
+ ### `onMount`
167
+
168
+ Optional lifecycle hook that runs when the atom **first gains a subscriber** in a store (for example when a React 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 `get`.
169
+
170
+ 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.
171
+
172
+ ```ts
173
+ const clockAtom = atom({
174
+ debugLabel: "clock",
175
+ get: async () => new Date().toISOString(),
176
+ onMount: (set) => {
177
+ const id = setInterval(() => {
178
+ set(new Date().toISOString());
179
+ }, 1000);
180
+ return () => clearInterval(id);
181
+ },
182
+ });
183
+ ```
184
+
103
185
  ### Parallel loading
104
186
 
105
187
  Declaring multiple dependencies causes the Store to resolve them in parallel before calling `get`. Inside `get` you control the parallelism explicitly.
@@ -301,7 +383,7 @@ export function App() {
301
383
  ## Utility Types
302
384
 
303
385
  ```ts
304
- import type { AtomValue, AtomArgs, IsWritable } from "@kdeveloper/kvark";
386
+ import type { AtomValue, AtomArgs, IsWritable, WritableAtomContext } from "@kdeveloper/kvark";
305
387
 
306
388
  type UserData = AtomValue<typeof userAtom>; // → User
307
389
  type PostArgs = AtomArgs<typeof postAtom>; // → [postId: number]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kdeveloper/kvark",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "Atomic state management with explicit dependency graphs",
5
5
  "license": "MIT",
6
6
  "files": [