rask-ui 0.2.0 → 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.md CHANGED
@@ -1,263 +1,1001 @@
1
1
  # RASK
2
2
 
3
- A lightweight reactive component library built on [Snabbdom](https://github.com/snabbdom/snabbdom) with MobX-inspired reactivity.
3
+ <p align="center">
4
+ <img src="logo.png" alt="Logo" width="200">
5
+ </p>
4
6
 
5
- ## Component and Element Lifecycle
7
+ A lightweight reactive component library that combines the simplicity of observable state management with the full power of a virtual DOM reconciler.
6
8
 
7
- ### Overview
9
+ ```bash
10
+ npm install rask-ui
11
+ ```
12
+
13
+ ## The Problem with Modern UI Frameworks
14
+
15
+ Modern UI frameworks present developers with a fundamental tradeoff between state management and UI expression:
16
+
17
+ ### React: Great UI, Complex State
18
+
19
+ ```tsx
20
+ function MyApp() {
21
+ const [count, setCount] = useState(0);
22
+
23
+ return <h1 onClick={() => setCount(count + 1)}>Count is {count}</h1>;
24
+ }
25
+ ```
26
+
27
+ React excels at UI composition - you simply use the language to create dynamic UIs. However, including state management within the reconciler causes significant mental strain:
28
+
29
+ - Understanding closure captures and stale state
30
+ - Managing dependency arrays in hooks
31
+ - Dealing with re-render cascades
32
+ - Optimizing with `useMemo`, `useCallback`, and `memo`
33
+ - Wrestling with the "rules of hooks"
34
+
35
+ ### Solid: Simple Reactivity, Hidden Complexity
8
36
 
9
- RASK uses a **host-based architecture** where each component creates a host DOM element (`<component>` tag with `display: contents`) and manages its own virtual DOM independently. Components track their direct children for efficient cleanup detection.
37
+ ```tsx
38
+ function MyApp() {
39
+ const [count, setCount] = createSignal(0);
40
+
41
+ return <h1 onClick={() => setCount(count() + 1)}>Count is {count()}</h1>;
42
+ }
43
+ ```
44
+
45
+ Solid offers a simpler mental model with fine-grained reactivity. Updates don't happen by calling the component function again, but in "hidden" parts of expressions in the UI. However:
10
46
 
11
- ### Component Lifecycle Phases
47
+ - Requires understanding special compiler transformations
48
+ - Special components for expressing dynamic UIs (`<Show>`, `<For>`, etc.)
49
+ - Function call syntax for accessing values: `count()`
12
50
 
13
- #### 1. Component Creation
51
+ ### RASK: Best of Both Worlds
52
+
53
+ ```tsx
54
+ function MyApp() {
55
+ const state = createState({ count: 0 });
56
+
57
+ return () => <h1 onClick={() => state.count++}>Count is {state.count}</h1>;
58
+ }
59
+ ```
60
+
61
+ RASK gives you:
62
+
63
+ - **Predictable observable state management** - No reconciler interference, just plain reactivity
64
+ - **Full reconciler power** - Express complex UIs naturally with components
65
+ - **No special syntax** - Access state properties directly, no function calls
66
+ - **No compiler magic** - Plain JavaScript/TypeScript
67
+ - **Simple mental model** - State updates trigger only affected components
68
+
69
+ ## Getting Started
70
+
71
+ ### Installation
72
+
73
+ ```bash
74
+ npm install rask-ui
75
+ ```
76
+
77
+ ### Basic Example
78
+
79
+ ```tsx
80
+ import { createState, render } from "rask-ui";
81
+
82
+ function Counter() {
83
+ const state = createState({ count: 0 });
84
+
85
+ return () => (
86
+ <div>
87
+ <h1>Count: {state.count}</h1>
88
+ <button onClick={() => state.count++}>Increment</button>
89
+ <button onClick={() => state.count--}>Decrement</button>
90
+ </div>
91
+ );
92
+ }
93
+
94
+ render(<Counter />, document.getElementById("app")!);
95
+ ```
96
+
97
+ ### Key Concepts
98
+
99
+ #### 1. Component Structure
100
+
101
+ Components in RASK have two phases:
14
102
 
15
103
  ```tsx
16
104
  function MyComponent(props) {
17
- // Setup Phase - runs ONCE per component instance
105
+ // SETUP PHASE - Runs once when component is created
106
+ const state = createState({ value: props.initial });
107
+
108
+ onMount(() => {
109
+ console.log("Component mounted!");
110
+ });
111
+
112
+ // RENDER PHASE - Returns a function that runs on every update
113
+ return () => (
114
+ <div>
115
+ <p>{state.value}</p>
116
+ <button onClick={() => state.value++}>Update</button>
117
+ </div>
118
+ );
119
+ }
120
+ ```
121
+
122
+ The setup phase runs once, while the render function runs whenever reactive dependencies change.
123
+
124
+ #### 2. Reactive State
125
+
126
+ State objects are automatically reactive. Any property access during render is tracked:
127
+
128
+ ```tsx
129
+ function TodoList() {
130
+ const state = createState({
131
+ todos: [],
132
+ filter: "all",
133
+ });
134
+
135
+ const addTodo = (text) => {
136
+ state.todos.push({ id: Date.now(), text, done: false });
137
+ };
138
+
139
+ return () => (
140
+ <div>
141
+ <input
142
+ value={state.filter}
143
+ onInput={(e) => (state.filter = e.target.value)}
144
+ />
145
+ <ul>
146
+ {state.todos
147
+ .filter(
148
+ (todo) => state.filter === "all" || todo.text.includes(state.filter)
149
+ )
150
+ .map((todo) => (
151
+ <li key={todo.id}>{todo.text}</li>
152
+ ))}
153
+ </ul>
154
+ </div>
155
+ );
156
+ }
157
+ ```
158
+
159
+ #### 3. Props are Reactive Too
160
+
161
+ Props passed to components are automatically reactive:
162
+
163
+ ```tsx
164
+ function Child(props) {
165
+ // props is reactive - accessing props.value tracks the dependency
166
+ return () => <div>{props.value}</div>;
167
+ }
168
+
169
+ function Parent() {
170
+ const state = createState({ count: 0 });
171
+
172
+ return () => (
173
+ <div>
174
+ <Child value={state.count} />
175
+ <button onClick={() => state.count++}>Update</button>
176
+ </div>
177
+ );
178
+ }
179
+ ```
180
+
181
+ When `state.count` changes in Parent, only Child re-renders because it accesses `props.value`.
182
+
183
+ ### Important: Do Not Destructure Reactive Objects
184
+
185
+ **RASK follows the same rule as Solid.js**: Never destructure reactive objects (state, props, context values, async, query, mutation). Destructuring extracts plain values and breaks reactivity.
186
+
187
+ ```tsx
188
+ // ❌ BAD - Destructuring breaks reactivity
189
+ function Counter(props) {
190
+ const state = createState({ count: 0 });
191
+ const { count } = state; // Extracts plain value!
192
+
193
+ return () => <div>{count}</div>; // Won't update!
194
+ }
195
+
196
+ function Child({ value, name }) {
197
+ // Destructuring props!
198
+ return () => (
199
+ <div>
200
+ {value} {name}
201
+ </div>
202
+ ); // Won't update!
203
+ }
204
+
205
+ // ✅ GOOD - Access properties directly in render
206
+ function Counter(props) {
18
207
  const state = createState({ count: 0 });
19
- const ref = createRef();
20
208
 
21
- onMount(() => console.log('Mounted!'));
22
- onCleanup(() => console.log('Cleaning up!'));
209
+ return () => <div>{state.count}</div>; // Reactive!
210
+ }
23
211
 
24
- // Return render function
25
- return () => <div ref={ref}>{state.count}</div>;
212
+ function Child(props) {
213
+ // Don't destructure
214
+ return () => (
215
+ <div>
216
+ {props.value} {props.name}
217
+ </div>
218
+ ); // Reactive!
26
219
  }
27
220
  ```
28
221
 
29
- **What happens:**
30
- - `createComponentInstance()` creates a new component instance
31
- - Component function executes (setup phase)
32
- - `createState()` creates reactive state
33
- - `onMount()` and `onCleanup()` register lifecycle callbacks
34
- - Render function is stored on the instance
35
- - Instance is wrapped with an observer for reactivity
222
+ **Why this happens:**
223
+
224
+ Reactive objects are implemented using JavaScript Proxies. When you access a property during render (e.g., `state.count`), the proxy tracks that dependency. But when you destructure (`const { count } = state`), the destructuring happens during setup—before any tracking context exists. You get a plain value instead of a tracked property access.
225
+
226
+ **This applies to:**
227
+
228
+ - `createState()` - Never destructure state objects
229
+ - Props - Never destructure component props
230
+ - `createContext().get()` - Never destructure context values
231
+ - `createAsync()` - Never destructure async state
232
+ - `createQuery()` - Never destructure query objects
233
+ - `createMutation()` - Never destructure mutation objects
234
+ - `createView()` - Never destructure view objects
36
235
 
37
- #### 2. Initial Render
236
+ **Learn more:** This is the same design decision as [Solid.js reactive primitives](https://www.solidjs.com/tutorial/introduction_signals), which also use this pattern for fine-grained reactivity.
38
237
 
238
+ ## API Reference
239
+
240
+ ### Core Functions
241
+
242
+ #### `render(component, container)`
243
+
244
+ Mounts a component to a DOM element.
245
+
246
+ ```tsx
247
+ import { render } from "rask-ui";
248
+
249
+ render(<App />, document.getElementById("app")!);
39
250
  ```
40
- render(<MyComponent />, container)
41
-
42
- jsxToVNode() - Convert JSX to VNode
43
-
44
- renderComponent() - Create component instance
45
-
46
- observer(() => { /* render function */ })
47
-
48
- Initial render (hostElement = null)
49
-
50
- Store child VNodes for host element creation
51
-
52
- Create host <component> VNode with children
53
-
54
- patch() - Superfine creates DOM
55
-
56
- setupComponentInstances() - Walk VNode tree
57
-
58
- Set instance.hostElement from vnode.node
59
-
60
- Store instance on element.__componentInstance
61
-
62
- Find parent component via DOM traversal
63
-
64
- Add to parent.children Set
65
-
66
- Run onMount() callbacks
251
+
252
+ **Parameters:**
253
+
254
+ - `component` - The JSX component to render
255
+ - `container` - The DOM element to mount into
256
+
257
+ ---
258
+
259
+ #### `createState<T>(initialState)`
260
+
261
+ Creates a reactive state object. Any property access during render is tracked, and changes trigger re-renders.
262
+
263
+ ```tsx
264
+ import { createState } from "rask-ui";
265
+
266
+ function Example() {
267
+ const state = createState({
268
+ count: 0,
269
+ items: ["a", "b", "c"],
270
+ nested: { value: 42 },
271
+ });
272
+
273
+ // All mutations are reactive
274
+ state.count++;
275
+ state.items.push("d");
276
+ state.nested.value = 100;
277
+
278
+ return () => <div>{state.count}</div>;
279
+ }
67
280
  ```
68
281
 
69
- **Key steps:**
70
- 1. **JSX to VNode**: JSX is converted to Superfine VNodes
71
- 2. **Component Instance Creation**: Each component gets a unique instance
72
- 3. **Observer Setup**: Render function is wrapped to track state dependencies
73
- 4. **Initial Render**: Observer runs but hostElement is null, so child VNodes are stored
74
- 5. **Host Element Creation**: A `<component>` VNode is created with the children
75
- 6. **Superfine Patch**: Superfine creates actual DOM elements
76
- 7. **Instance Setup**: VNode tree is walked to set `hostElement` from `vnode.node`
77
- 8. **Parent-Child Tracking**: Each instance finds its parent and registers as a child
78
- 9. **Mount Callbacks**: `onMount()` callbacks run after DOM is ready
282
+ **Parameters:**
283
+
284
+ - `initialState: T` - Initial state object
285
+
286
+ **Returns:** Reactive proxy of the state object
287
+
288
+ **Features:**
289
+
290
+ - Deep reactivity - nested objects and arrays are automatically reactive
291
+ - Direct mutations - no setter functions required
292
+ - Efficient tracking - only re-renders components that access changed properties
293
+
294
+ ---
79
295
 
80
- #### 3. State Changes and Re-renders
296
+ #### `createView<T>(...objects)`
81
297
 
298
+ Creates a view that merges multiple objects (reactive or plain) into a single object while maintaining reactivity through getters. Properties from later arguments override earlier ones.
299
+
300
+ ```tsx
301
+ import { createView, createState } from "rask-ui";
302
+
303
+ function Counter() {
304
+ const state = createState({ count: 0, name: "Counter" });
305
+ const helpers = {
306
+ increment: () => state.count++,
307
+ decrement: () => state.count--,
308
+ reset: () => (state.count = 0),
309
+ };
310
+
311
+ const view = createView(state, helpers);
312
+
313
+ return () => (
314
+ <div>
315
+ <h1>
316
+ {view.name}: {view.count}
317
+ </h1>
318
+ <button onClick={view.increment}>+</button>
319
+ <button onClick={view.decrement}>-</button>
320
+ <button onClick={view.reset}>Reset</button>
321
+ </div>
322
+ );
323
+ }
82
324
  ```
83
- state.count++ (or props change)
84
-
85
- Observer notified (reactive dependency)
86
-
87
- observer() callback executes
88
-
89
- Render function executes
90
-
91
- jsx() - Convert new JSX to VNodes
92
-
93
- patch(hostNode, newVNode)
94
-
95
- Snabbdom updates DOM
325
+
326
+ **Parameters:**
327
+
328
+ - `...objects: object[]` - Objects to merge (reactive or plain). Later arguments override earlier ones.
329
+
330
+ **Returns:** A view object with getters for all properties, maintaining reactivity
331
+
332
+ **Use Cases:**
333
+
334
+ 1. **Merge state with helper methods:**
335
+
336
+ ```tsx
337
+ const state = createState({ count: 0 });
338
+ const helpers = { increment: () => state.count++ };
339
+ const view = createView(state, helpers);
340
+ // Access both: view.count, view.increment()
341
+ ```
342
+
343
+ 2. **Combine multiple reactive objects:**
344
+
345
+ ```tsx
346
+ const user = createState({ name: "Alice" });
347
+ const settings = createState({ theme: "dark" });
348
+ const view = createView(user, settings);
349
+ // Access both: view.name, view.theme
350
+ ```
351
+
352
+ 3. **Override properties with computed values:**
353
+ ```tsx
354
+ const state = createState({ firstName: "John", lastName: "Doe" });
355
+ const computed = {
356
+ get fullName() {
357
+ return `${state.firstName} ${state.lastName}`;
358
+ },
359
+ };
360
+ const view = createView(state, computed);
361
+ // view.fullName returns "John Doe" and updates when state changes
362
+ ```
363
+
364
+ **Notes:**
365
+
366
+ - Reactivity is maintained through getters that reference the source objects
367
+ - Changes to source objects are reflected in the view
368
+ - Only enumerable properties are included
369
+ - Symbol keys are supported
370
+ - **Do not destructure** - See warning section above
371
+
372
+ ---
373
+
374
+ #### `createRef<T>()`
375
+
376
+ Creates a ref object for accessing DOM elements or component instances directly.
377
+
378
+ ```tsx
379
+ import { createRef } from "rask-ui";
380
+
381
+ function Example() {
382
+ const inputRef = createRef<HTMLInputElement>();
383
+
384
+ const focus = () => {
385
+ inputRef.current?.focus();
386
+ };
387
+
388
+ return () => (
389
+ <div>
390
+ <input ref={inputRef} type="text" />
391
+ <button onClick={focus}>Focus Input</button>
392
+ </div>
393
+ );
394
+ }
96
395
  ```
97
396
 
98
- **Key steps:**
99
- 1. **State Change**: Setting a state property notifies observers
100
- 2. **Observer Execution**: The component's observer callback runs
101
- 3. **Re-render**: Render function executes, accessing state (tracks dependencies)
102
- 4. **JSX to VNode**: New JSX is converted to VNodes
103
- 5. **Patch**: Snabbdom updates the host element's children
397
+ **Returns:** Ref object with:
398
+
399
+ - `current: T | null` - Reference to the DOM element or component instance
400
+ - Function signature for use as ref callback
401
+
402
+ **Usage:**
104
403
 
105
- #### 4. Component Cleanup
404
+ Pass the ref to an element's `ref` prop. The `current` property will be set to the DOM element when mounted and `null` when unmounted.
106
405
 
107
- **Cleanup happens in two scenarios:**
406
+ **Example with SVG:**
108
407
 
109
- ##### A. Parent Re-renders and Removes Child
408
+ ```tsx
409
+ function Drawing() {
410
+ const svgRef = createRef<SVGSVGElement>();
411
+
412
+ const getSize = () => {
413
+ if (svgRef.current) {
414
+ const bbox = svgRef.current.getBBox();
415
+ console.log(`Width: ${bbox.width}, Height: ${bbox.height}`);
416
+ }
417
+ };
110
418
 
419
+ return () => (
420
+ <div>
421
+ <svg ref={svgRef} width="200" height="200">
422
+ <circle cx="100" cy="100" r="50" />
423
+ </svg>
424
+ <button onClick={getSize}>Get SVG Size</button>
425
+ </div>
426
+ );
427
+ }
111
428
  ```
112
- Parent re-renders
113
-
114
- patch(parentHost, newVNode)
115
-
116
- Snabbdom removes child <component> from DOM
117
-
118
- Destroy hook called
119
-
120
- Run cleanupCallbacks
429
+
430
+ ---
431
+
432
+ ### Lifecycle Hooks
433
+
434
+ #### `onMount(callback)`
435
+
436
+ Registers a callback to run after the component is mounted to the DOM.
437
+
438
+ ```tsx
439
+ import { onMount } from "rask-ui";
440
+
441
+ function Example() {
442
+ onMount(() => {
443
+ console.log("Component mounted!");
444
+ });
445
+
446
+ return () => <div>Hello</div>;
447
+ }
448
+ ```
449
+
450
+ **Parameters:**
451
+
452
+ - `callback: () => void` - Function to call on mount. Can optionally return a cleanup function.
453
+
454
+ **Notes:**
455
+
456
+ - Only call during component setup phase (not in render function)
457
+ - Can be called multiple times to register multiple mount callbacks
458
+
459
+ ---
460
+
461
+ #### `onCleanup(callback)`
462
+
463
+ Registers a callback to run when the component is unmounted.
464
+
465
+ ```tsx
466
+ import { onCleanup } from "rask-ui";
467
+
468
+ function Example() {
469
+ const state = createState({ time: Date.now() });
470
+
471
+ const interval = setInterval(() => {
472
+ state.time = Date.now();
473
+ }, 1000);
474
+
475
+ onCleanup(() => {
476
+ clearInterval(interval);
477
+ });
478
+
479
+ return () => <div>{state.time}</div>;
480
+ }
121
481
  ```
122
482
 
123
- **Key steps:**
124
- 1. **Detection**: Snabbdom's destroy hook is called when element is removed
125
- 2. **Callbacks**: `onCleanup()` callbacks execute (clear timers, subscriptions, etc.)
483
+ **Parameters:**
484
+
485
+ - `callback: () => void` - Function to call on cleanup
486
+
487
+ **Notes:**
488
+
489
+ - Only call during component setup phase
490
+ - Can be called multiple times to register multiple cleanup callbacks
491
+ - Runs when component is removed from DOM
492
+
493
+ ---
494
+
495
+ ### Context API
496
+
497
+ #### `createContext<T>()`
498
+
499
+ Creates a context object for passing data through the component tree without props.
500
+
501
+ ```tsx
502
+ import { createContext } from "rask-ui";
126
503
 
127
- ### Element Lifecycle (Regular DOM Elements)
504
+ const ThemeContext = createContext<{ color: string }>();
128
505
 
129
- Regular DOM elements (div, span, etc.) follow Snabbdom's patching lifecycle:
506
+ function App() {
507
+ ThemeContext.inject({ color: "blue" });
130
508
 
509
+ return () => <Child />;
510
+ }
511
+
512
+ function Child() {
513
+ const theme = ThemeContext.get();
514
+
515
+ return () => <div style={{ color: theme.color }}>Themed text</div>;
516
+ }
131
517
  ```
132
- JSX: <div>Hello</div>
133
-
134
- jsx() creates VNode
135
-
136
- patch() creates or updates DOM
137
-
138
- vnode.elm contains DOM element
518
+
519
+ **Returns:** Context object with `inject` and `get` methods
520
+
521
+ **Methods:**
522
+
523
+ - `inject(value: T)` - Injects context value for child components (call during setup)
524
+ - `get(): T` - Gets context value from nearest parent (call during setup)
525
+
526
+ **Notes:**
527
+
528
+ - Context traversal happens via component tree (parent-child relationships)
529
+ - Must be called during component setup phase
530
+ - Throws error if context not found in parent chain
531
+
532
+ ---
533
+
534
+ ### Async Data Management
535
+
536
+ #### `createAsync<T>(promise)`
537
+
538
+ Creates reactive state for async operations with loading and error states.
539
+
540
+ ```tsx
541
+ import { createAsync } from "rask-ui";
542
+
543
+ function UserProfile() {
544
+ const user = createAsync(fetch("/api/user").then((r) => r.json()));
545
+
546
+ return () => (
547
+ <div>
548
+ {user.isPending && <p>Loading...</p>}
549
+ {user.error && <p>Error: {user.error}</p>}
550
+ {user.value && <p>Hello, {user.value.name}!</p>}
551
+ </div>
552
+ );
553
+ }
139
554
  ```
140
555
 
141
- **Key points:**
142
- - Regular elements don't create component instances
143
- - They're managed entirely by Snabbdom's patch algorithm
144
- - No special cleanup needed (browser handles DOM removal)
556
+ **Parameters:**
557
+
558
+ - `promise: Promise<T>` - The promise to track
145
559
 
146
- ### Parent-Child Relationship Tracking
560
+ **Returns:** Reactive object with:
147
561
 
148
- Components track their parent via the component stack during initialization:
562
+ - `isPending: boolean` - True while promise is pending
563
+ - `value: T | null` - Resolved value (null while pending or on error)
564
+ - `error: string | null` - Error message (null while pending or on success)
149
565
 
566
+ **States:**
567
+
568
+ - `{ isPending: true, value: null, error: null }` - Loading
569
+ - `{ isPending: false, value: T, error: null }` - Success
570
+ - `{ isPending: false, value: null, error: string }` - Error
571
+
572
+ ---
573
+
574
+ #### `createQuery<T>(fetcher)`
575
+
576
+ Creates a query with refetch capability and request cancellation.
577
+
578
+ ```tsx
579
+ import { createQuery } from "rask-ui";
580
+
581
+ function Posts() {
582
+ const posts = createQuery(() => fetch("/api/posts").then((r) => r.json()));
583
+
584
+ return () => (
585
+ <div>
586
+ <button onClick={() => posts.fetch()}>Refresh</button>
587
+ <button onClick={() => posts.fetch(true)}>Force Refresh</button>
588
+
589
+ {posts.isPending && <p>Loading...</p>}
590
+ {posts.error && <p>Error: {posts.error}</p>}
591
+ {posts.data &&
592
+ posts.data.map((post) => <article key={post.id}>{post.title}</article>)}
593
+ </div>
594
+ );
595
+ }
150
596
  ```
151
- componentStack.unshift(instance)
152
- const render = component(instance.reactiveProps)
597
+
598
+ **Parameters:**
599
+
600
+ - `fetcher: () => Promise<T>` - Function that returns a promise
601
+
602
+ **Returns:** Query object with:
603
+
604
+ - `isPending: boolean` - True while fetching
605
+ - `data: T | null` - Fetched data
606
+ - `error: string | null` - Error message
607
+ - `fetch(force?: boolean)` - Refetch data
608
+ - `force: false` (default) - Keep existing data while refetching
609
+ - `force: true` - Clear data before refetching
610
+
611
+ **Features:**
612
+
613
+ - Automatic request cancellation on refetch
614
+ - Keeps old data by default during refetch
615
+ - Automatically fetches on creation
616
+
617
+ ---
618
+
619
+ #### `createMutation<T>(mutator)`
620
+
621
+ Creates a mutation for data updates with pending and error states.
622
+
623
+ ```tsx
624
+ import { createMutation } from "rask-ui";
625
+
626
+ function CreatePost() {
627
+ const state = createState({ title: "", body: "" });
628
+
629
+ const create = createMutation((data) =>
630
+ fetch("/api/posts", {
631
+ method: "POST",
632
+ body: JSON.stringify(data),
633
+ }).then((r) => r.json())
634
+ );
635
+
636
+ const handleSubmit = () => {
637
+ create.mutate({ title: state.title, body: state.body });
638
+ };
639
+
640
+ return () => (
641
+ <form onSubmit={handleSubmit}>
642
+ <input
643
+ value={state.title}
644
+ onInput={(e) => (state.title = e.target.value)}
645
+ />
646
+ <textarea
647
+ value={state.body}
648
+ onInput={(e) => (state.body = e.target.value)}
649
+ />
650
+ <button disabled={create.isPending}>
651
+ {create.isPending ? "Creating..." : "Create"}
652
+ </button>
653
+ {create.error && <p>Error: {create.error}</p>}
654
+ </form>
655
+ );
656
+ }
153
657
  ```
154
658
 
155
- **Key points:**
156
- - Parent-child relationships tracked through component stack
157
- - Used for context propagation
158
- - Cleanup handled by Snabbdom's destroy hooks
659
+ **Parameters:**
660
+
661
+ - `mutator: (params: T) => Promise<T>` - Function that performs the mutation
662
+
663
+ **Returns:** Mutation object with:
159
664
 
160
- ### Complete Lifecycle Example
665
+ - `isPending: boolean` - True while mutation is in progress
666
+ - `params: T | null` - Current mutation parameters
667
+ - `error: string | null` - Error message
668
+ - `mutate(params: T)` - Execute the mutation
669
+
670
+ **Features:**
671
+
672
+ - Automatic request cancellation if mutation called again
673
+ - Tracks mutation parameters
674
+ - Resets state after successful completion
675
+
676
+ ---
677
+
678
+ ### Error Handling
679
+
680
+ #### `ErrorBoundary`
681
+
682
+ Component that catches errors from child components.
161
683
 
162
684
  ```tsx
163
- function Counter() {
164
- // SETUP PHASE (once)
685
+ import { ErrorBoundary } from "rask-ui";
686
+
687
+ function App() {
688
+ return () => (
689
+ <ErrorBoundary
690
+ error={(err) => (
691
+ <div>
692
+ <h1>Something went wrong</h1>
693
+ <pre>{String(err)}</pre>
694
+ </div>
695
+ )}
696
+ >
697
+ <MyComponent />
698
+ </ErrorBoundary>
699
+ );
700
+ }
701
+
702
+ function MyComponent() {
165
703
  const state = createState({ count: 0 });
166
704
 
167
- onMount(() => {
168
- console.log('Counter mounted');
705
+ return () => {
706
+ if (state.count > 5) {
707
+ throw new Error("Count too high!");
708
+ }
709
+
710
+ return <button onClick={() => state.count++}>{state.count}</button>;
711
+ };
712
+ }
713
+ ```
714
+
715
+ **Props:**
716
+
717
+ - `error: (error: unknown) => ChildNode | ChildNode[]` - Render function for error state
718
+ - `children` - Child components to protect
719
+
720
+ **Notes:**
721
+
722
+ - Catches errors during render phase
723
+ - Errors bubble up to nearest ErrorBoundary
724
+ - Can be nested for granular error handling
725
+
726
+ ---
727
+
728
+ ## Advanced Patterns
729
+
730
+ ### Lists and Keys
731
+
732
+ Use keys to maintain component identity across re-renders:
733
+
734
+ ```tsx
735
+ function TodoList() {
736
+ const state = createState({
737
+ todos: [
738
+ { id: 1, text: "Learn RASK" },
739
+ { id: 2, text: "Build app" },
740
+ ],
169
741
  });
170
742
 
171
- onCleanup(() => {
172
- console.log('Counter cleaning up');
743
+ return () => (
744
+ <ul>
745
+ {state.todos.map((todo) => (
746
+ <TodoItem key={todo.id} todo={todo} />
747
+ ))}
748
+ </ul>
749
+ );
750
+ }
751
+
752
+ function TodoItem(props) {
753
+ const state = createState({ editing: false });
754
+
755
+ return () => (
756
+ <li>
757
+ {state.editing ? (
758
+ <input value={props.todo.text} />
759
+ ) : (
760
+ <span onClick={() => (state.editing = true)}>{props.todo.text}</span>
761
+ )}
762
+ </li>
763
+ );
764
+ }
765
+ ```
766
+
767
+ Keys prevent component recreation when list order changes.
768
+
769
+ ### Computed Values
770
+
771
+ Create computed values using functions in setup:
772
+
773
+ ```tsx
774
+ function ShoppingCart() {
775
+ const state = createState({
776
+ items: [
777
+ { id: 1, price: 10, quantity: 2 },
778
+ { id: 2, price: 20, quantity: 1 },
779
+ ],
173
780
  });
174
781
 
175
- // RENDER PHASE (every update)
782
+ const total = () =>
783
+ state.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
784
+
176
785
  return () => (
177
786
  <div>
178
- <p>Count: {state.count}</p>
179
- <button onClick={() => state.count++}>+</button>
787
+ <ul>
788
+ {state.items.map((item) => (
789
+ <li key={item.id}>
790
+ ${item.price} x {item.quantity}
791
+ </li>
792
+ ))}
793
+ </ul>
794
+ <p>Total: ${total()}</p>
180
795
  </div>
181
796
  );
182
797
  }
798
+ ```
799
+
800
+ Computed functions automatically track dependencies when called during render.
183
801
 
184
- // Mount
185
- render(<Counter />, document.getElementById('app'));
186
- // Logs: "Counter mounted"
802
+ ### Composition
187
803
 
188
- // User clicks button
189
- // → state.count++ triggers observer
190
- // → Render function executes
191
- // → DOM updates via patch()
804
+ Compose complex logic by combining state and methods using `createView`:
805
+
806
+ ```tsx
807
+ function createAuthStore() {
808
+ const state = createState({
809
+ user: null,
810
+ isAuthenticated: false,
811
+ isLoading: false,
812
+ });
192
813
 
193
- // Component removed (parent re-renders without it)
194
- // Snabbdom destroy hook called
195
- // → Logs: "Counter cleaning up"
814
+ const login = async (username, password) => {
815
+ state.isLoading = true;
816
+ try {
817
+ const user = await fetch("/api/login", {
818
+ method: "POST",
819
+ body: JSON.stringify({ username, password }),
820
+ }).then((r) => r.json());
821
+ state.user = user;
822
+ state.isAuthenticated = true;
823
+ } finally {
824
+ state.isLoading = false;
825
+ }
826
+ };
827
+
828
+ const logout = () => {
829
+ state.user = null;
830
+ state.isAuthenticated = false;
831
+ };
832
+
833
+ return createView(state, { login, logout });
834
+ }
835
+
836
+ function App() {
837
+ const auth = createAuthStore();
838
+
839
+ return () => (
840
+ <div>
841
+ {auth.isAuthenticated ? (
842
+ <div>
843
+ <p>Welcome, {auth.user.name}!</p>
844
+ <button onClick={auth.logout}>Logout</button>
845
+ </div>
846
+ ) : (
847
+ <button onClick={() => auth.login("user", "pass")}>Login</button>
848
+ )}
849
+ </div>
850
+ );
851
+ }
196
852
  ```
197
853
 
198
- ## Key Architectural Decisions
854
+ This pattern is great for organizing complex business logic while keeping both state and methods accessible through a single object.
199
855
 
200
- ### 1. Host-Based Architecture
201
- - Each component has a host `<component>` element (display: contents)
202
- - Components render independently to their own host
203
- - No parent-child instance relationships for rendering
204
- - Isolation enables fine-grained updates
856
+ ### External State Management
205
857
 
206
- ### 2. Lifecycle Management
207
- - Snabbdom hooks manage component lifecycle
208
- - Insert hook runs onMount callbacks
209
- - Destroy hook runs onCleanup callbacks
210
- - Parent-child tracking for context propagation
858
+ Share state across components:
211
859
 
212
- ### 3. Observer Pattern for Reactivity
213
- - Render functions wrapped with observer
214
- - Automatically tracks state/props access
215
- - Re-renders only when observed properties change
216
- - No manual subscriptions needed
860
+ ```tsx
861
+ // store.ts
862
+ export const store = createState({
863
+ user: null,
864
+ theme: "light",
865
+ });
217
866
 
218
- ### 4. Context Traversal
219
- - Context walks component tree via parent references
220
- - Set during component initialization
221
- - Natural hierarchical lookup
222
- - Works with host element architecture
867
+ // App.tsx
868
+ import { store } from "./store";
223
869
 
224
- ### 5. Hook-Based Cleanup
225
- - Cleanup callbacks run via Snabbdom destroy hooks
226
- - Synchronous and predictable
227
- - No manual tracking required
228
- - Integrated with virtual DOM lifecycle
870
+ function Header() {
871
+ return () => <div>Theme: {store.theme}</div>;
872
+ }
229
873
 
230
- ### 6. Thunk-Based Components
874
+ function Settings() {
875
+ return () => (
876
+ <button
877
+ onClick={() => (store.theme = store.theme === "light" ? "dark" : "light")}
878
+ >
879
+ Toggle Theme
880
+ </button>
881
+ );
882
+ }
883
+ ```
231
884
 
232
- Components use Snabbdom's thunk feature for optimization:
885
+ Any component accessing `store` will re-render when it changes.
886
+
887
+ ### Conditional Rendering
233
888
 
234
889
  ```tsx
235
- const thunkNode = thunk("component", props.key, component, [props, children]);
890
+ function Conditional() {
891
+ const state = createState({ show: false });
892
+
893
+ return () => (
894
+ <div>
895
+ <button onClick={() => (state.show = !state.show)}>Toggle</button>
896
+ {state.show && <ExpensiveComponent />}
897
+ </div>
898
+ );
899
+ }
236
900
  ```
237
901
 
238
- **How it works:**
239
- - Components wrapped as thunks with custom hooks
240
- - Init hook: Creates component instance and runs setup
241
- - Prepatch hook: Updates reactive props before render
242
- - Postpatch hook: Syncs props after patch
243
- - Insert hook: Runs onMount callbacks
244
- - Destroy hook: Runs onCleanup callbacks
902
+ Components are only created when rendered, and automatically cleaned up when removed.
245
903
 
246
- **Benefits:**
247
- - Leverages Snabbdom's thunk optimization
248
- - Props changes trigger reactive updates
249
- - Lifecycle hooks integrated with virtual DOM
250
- - Efficient component reconciliation
904
+ ## TypeScript Support
251
905
 
252
- ## API Reference
906
+ RASK is written in TypeScript and provides full type inference:
907
+
908
+ ```tsx
909
+ import { createState, Component } from "rask-ui";
910
+
911
+ interface Todo {
912
+ id: number;
913
+ text: string;
914
+ done: boolean;
915
+ }
916
+
917
+ interface TodoItemProps {
918
+ todo: Todo;
919
+ onToggle: (id: number) => void;
920
+ }
921
+
922
+ const TodoItem: Component<TodoItemProps> = (props) => {
923
+ return () => (
924
+ <li onClick={() => props.onToggle(props.todo.id)}>{props.todo.text}</li>
925
+ );
926
+ };
927
+
928
+ function TodoList() {
929
+ const state = createState<{ todos: Todo[] }>({
930
+ todos: [],
931
+ });
932
+
933
+ const toggle = (id: number) => {
934
+ const todo = state.todos.find((t) => t.id === id);
935
+ if (todo) todo.done = !todo.done;
936
+ };
937
+
938
+ return () => (
939
+ <ul>
940
+ {state.todos.map((todo) => (
941
+ <TodoItem key={todo.id} todo={todo} onToggle={toggle} />
942
+ ))}
943
+ </ul>
944
+ );
945
+ }
946
+ ```
947
+
948
+ ## Configuration
949
+
950
+ ### JSX Setup
951
+
952
+ Configure TypeScript to use RASK's JSX runtime:
953
+
954
+ ```json
955
+ {
956
+ "compilerOptions": {
957
+ "jsx": "react-jsx",
958
+ "jsxImportSource": "rask-ui"
959
+ }
960
+ }
961
+ ```
962
+
963
+ ## Performance
964
+
965
+ RASK is designed for performance:
966
+
967
+ - **Fine-grained reactivity**: Only components that access changed state re-render
968
+ - **No wasted renders**: Components skip re-render if reactive dependencies haven't changed
969
+ - **Efficient DOM updates**: Powered by a custom virtual DOM implementation optimized for reactive components
970
+ - **No reconciler overhead for state**: State changes are direct, no diffing required
971
+ - **Automatic cleanup**: Components and effects cleaned up automatically
972
+
973
+ ## Comparison with Other Frameworks
974
+
975
+ | Feature | React | Solid | RASK |
976
+ | ----------------- | ------------------------- | ------------------------ | ---------------- |
977
+ | State management | Complex (hooks, closures) | Simple (signals) | Simple (proxies) |
978
+ | UI expression | Excellent | Limited | Excellent |
979
+ | Reactivity | Coarse (component level) | Fine-grained | Fine-grained |
980
+ | Reconciler | Yes | Limited | Yes (custom) |
981
+ | Syntax | JSX | JSX + special components | JSX |
982
+ | Compiler required | No | Yes | No |
983
+ | Learning curve | Steep | Moderate | Gentle |
984
+ | Access pattern | Direct | Function calls `count()` | Direct |
985
+ | Mental model | Complex | Simple (with rules) | Simple |
986
+
987
+ ## Examples
988
+
989
+ Check out the demo app in `packages/demo` for more examples.
990
+
991
+ ## Contributing
992
+
993
+ Contributions are welcome! This is an early-stage project.
994
+
995
+ ## License
996
+
997
+ MIT
998
+
999
+ ## Why "RASK"?
253
1000
 
254
- See main [README](../../README.md) for full API details:
255
- - `createState(initialState)` - Create reactive state
256
- - `onMount(callback)` - Register mount callback
257
- - `onCleanup(callback)` - Register cleanup callback
258
- - `createContext()` - Create context for data sharing
259
- - `createAsync(promise)` - Handle async operations
260
- - `createQuery(fetcher)` - Create query with refetch
261
- - `createMutation(mutator)` - Create mutation handler
262
- - `ErrorBoundary` - Error boundary component
263
- - `render(jsx, container)` - Mount component to DOM
1001
+ The name comes from Norwegian/Swedish meaning "fast" - which captures the essence of this library: fast to write, fast to understand, and fast to run.