rask-ui 0.12.1 → 0.12.3

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,97 +1,20 @@
1
1
  # RASK
2
2
 
3
3
  <p align="center">
4
- <img src="logo.png" alt="Logo" width="200">
4
+ <img src="https://raw.githubusercontent.com/christianalfoni/rask-ui/main/logo.png" alt="Logo" width="200">
5
5
  </p>
6
6
 
7
- A lightweight reactive component library that combines the simplicity of observable state management with the full power of a virtual DOM reconciler. Ideal for single page applications, using web technology.
7
+ A lightweight reactive component library that combines the simplicity of observable state management with the full power of a virtual DOM reconciler.
8
8
 
9
- ```bash
10
- npm install rask-ui
11
- ```
12
-
13
- ---
14
-
15
- ## 📢 Open For Feedback
16
-
17
- **RASK is feature-complete and ready for community feedback!**
18
-
19
- The core implementation is finished, including:
20
- - ✅ Inferno-based reconciler for powerful UI expression
21
- - ✅ JSX transformation plugin (Inferno JSX + stateful components)
22
- - ✅ Reactive primitives for simple state management
23
-
24
- **Before the official release, we need your input:**
25
- - Would a library like this be valuable for your projects?
26
- - Is the API clear and intuitive?
27
- - Is the documentation helpful and complete?
28
- - Does the approach resonate with you?
29
-
30
- **[Share your feedback by creating an issue →](https://github.com/christianalfoni/rask/issues/new)**
31
-
32
- Your feedback will directly shape the final release. All perspectives welcome!
33
-
34
- ---
35
-
36
- ## The Itch with Modern UI Frameworks
37
-
38
- Modern UI frameworks present developers with a fundamental tradeoff between state management and UI expression:
39
-
40
- ### React: Great UI, Complex State
41
-
42
- ```tsx
43
- function MyApp() {
44
- const [count, setCount] = useState(0);
9
+ **[Visit rask-ui.io for full documentation](https://rask-ui.io)**
45
10
 
46
- return <h1 onClick={() => setCount(count + 1)}>Count is {count}</h1>;
47
- }
48
- ```
49
-
50
- 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:
51
-
52
- - Understanding closure captures and stale state
53
- - Managing dependency arrays in hooks
54
- - Dealing with re-render cascades
55
- - Optimizing with `useMemo`, `useCallback`, and `memo`
56
- - Wrestling with the "rules of hooks"
57
-
58
- ### Solid: Simple Reactivity, Hidden Complexity
59
-
60
- ```tsx
61
- function MyApp() {
62
- const [count, setCount] = createSignal(0);
63
-
64
- return <h1 onClick={() => setCount(count() + 1)}>Count is {count()}</h1>;
65
- }
66
- ```
67
-
68
- Solid offers a seamingly simpler mental model with fine-grained reactivity. Updates don't happen by calling the component function again, which resolves the mental strain of expressing state management, however:
69
-
70
- - The code you write is compiled to balance DX vs requirements of the runtime
71
- - Special components for expressing dynamic UIs (`<Show>`, `<For>`, etc.)
72
- - Different signatures for accessing reactive values: `count()` VS `state.count`
73
-
74
- ### RASK: Best of Both Worlds
75
-
76
- ```tsx
77
- function MyApp() {
78
- const state = createState({ count: 0 });
11
+ ## Installation
79
12
 
80
- return () => <h1 onClick={() => state.count++}>Count is {state.count}</h1>;
81
- }
13
+ ```bash
14
+ npm install rask-ui
82
15
  ```
83
16
 
84
- RASK gives you:
85
-
86
- - **Simple state management** - No reconciler interference with your state management
87
- - **Full reconciler power** - Express complex UIs naturally with the language
88
- - **No compiler magic** - Plain JavaScript/TypeScript, it runs as you write it
89
-
90
- :fire: Built on [Inferno JS](https://github.com/infernojs/inferno).
91
-
92
- ## Getting Started
93
-
94
- The fastest way to get started is using `create-rask-ui`:
17
+ Or create a new project:
95
18
 
96
19
  ```bash
97
20
  npm create rask-ui my-app
@@ -99,18 +22,7 @@ cd my-app
99
22
  npm run dev
100
23
  ```
101
24
 
102
- This will scaffold a new Vite project with RASK UI pre-configured and ready to go. You'll be prompted to choose between TypeScript or JavaScript.
103
-
104
- ### What's Included
105
-
106
- The scaffolded project includes:
107
-
108
- - ✅ Vite configured with the RASK plugin
109
- - ✅ TypeScript or JavaScript with proper JSX configuration
110
- - ✅ Hot Module Replacement (HMR) working out of the box
111
- - ✅ Sample counter component to get you started
112
-
113
- ### Basic Example
25
+ ## Quick Example
114
26
 
115
27
  ```tsx
116
28
  import { createState, render } from "rask-ui";
@@ -122,1633 +34,27 @@ function Counter() {
122
34
  <div>
123
35
  <h1>Count: {state.count}</h1>
124
36
  <button onClick={() => state.count++}>Increment</button>
125
- <button onClick={() => state.count--}>Decrement</button>
126
- </div>
127
- );
128
- }
129
-
130
- render(<Counter />, document.getElementById("app")!);
131
- ```
132
-
133
- ### Key Concepts
134
-
135
- #### 1. Component Structure
136
-
137
- Components in RASK have two phases:
138
-
139
- ```tsx
140
- function MyComponent(props) {
141
- // SETUP PHASE - Runs once when component is created
142
- const state = createState({ value: props.initial });
143
-
144
- createMountEffect(() => {
145
- console.log("Component mounted!");
146
- });
147
-
148
- // RENDER PHASE - Returns a function that runs on every update
149
- return () => (
150
- <div>
151
- <p>{state.value}</p>
152
- <button onClick={() => state.value++}>Update</button>
153
- </div>
154
- );
155
- }
156
- ```
157
-
158
- The setup phase runs once, while the render function runs whenever reactive dependencies change.
159
-
160
- #### 2. Reactive State
161
-
162
- State objects are automatically reactive. Any property access during render is tracked:
163
-
164
- ```tsx
165
- function TodoList() {
166
- const state = createState({
167
- todos: [],
168
- filter: "all",
169
- });
170
-
171
- const addTodo = (text) => {
172
- state.todos.push({ id: Date.now(), text, done: false });
173
- };
174
-
175
- return () => (
176
- <div>
177
- <input
178
- value={state.filter}
179
- onInput={(e) => (state.filter = e.target.value)}
180
- />
181
- <ul>
182
- {state.todos
183
- .filter(
184
- (todo) => state.filter === "all" || todo.text.includes(state.filter)
185
- )
186
- .map((todo) => (
187
- <li key={todo.id}>{todo.text}</li>
188
- ))}
189
- </ul>
190
- </div>
191
- );
192
- }
193
- ```
194
-
195
- #### 3. Props are Reactive Too
196
-
197
- Props passed to components are automatically reactive:
198
-
199
- ```tsx
200
- function Child(props) {
201
- // props is reactive - accessing props.value tracks the dependency
202
- return () => <div>{props.value}</div>;
203
- }
204
-
205
- function Parent() {
206
- const state = createState({ count: 0 });
207
-
208
- return () => (
209
- <div>
210
- <Child value={state.count} />
211
- <button onClick={() => state.count++}>Update</button>
212
- </div>
213
- );
214
- }
215
- ```
216
-
217
- When `state.count` changes in Parent, only Child re-renders because it accesses `props.value`.
218
-
219
- ### One Rule To Accept
220
-
221
- **RASK has observable primitives**: Never destructure reactive objects (state, props, context values, tasks). Destructuring extracts plain values and breaks reactivity.
222
-
223
- ```tsx
224
- // ❌ BAD - Destructuring breaks reactivity
225
- function Counter(props) {
226
- const state = createState({ count: 0 });
227
- const { count } = state; // Extracts plain value!
228
-
229
- return () => <div>{count}</div>; // Won't update!
230
- }
231
-
232
- function Child({ value, name }) {
233
- // Destructuring props!
234
- return () => (
235
- <div>
236
- {value} {name}
237
- </div>
238
- ); // Won't update!
239
- }
240
-
241
- // ✅ GOOD - Access properties directly in render
242
- function Counter(props) {
243
- const state = createState({ count: 0 });
244
-
245
- return () => <div>{state.count}</div>; // Reactive!
246
- }
247
-
248
- function Child(props) {
249
- // Don't destructure
250
- return () => (
251
- <div>
252
- {props.value} {props.name}
253
- </div>
254
- ); // Reactive!
255
- }
256
- ```
257
-
258
- **Why this happens:**
259
-
260
- 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.
261
-
262
- **This applies to:**
263
-
264
- - `createState()` - Never destructure state objects
265
- - Props - Never destructure component props
266
- - `createContext().get()` - Never destructure context values
267
- - `createTask()` - Never destructure task objects
268
- - `createView()` - Never destructure view objects
269
- - `createComputed()` - Never destructure computed objects
270
-
271
- ## API Reference
272
-
273
- ### Core Functions
274
-
275
- #### `render(component, container)`
276
-
277
- Mounts a component to a DOM element.
278
-
279
- ```tsx
280
- import { render } from "rask-ui";
281
-
282
- render(<App />, document.getElementById("app")!);
283
- ```
284
-
285
- **Parameters:**
286
-
287
- - `component` - The JSX component to render
288
- - `container` - The DOM element to mount into
289
-
290
- ---
291
-
292
- #### `createState<T>(initialState)`
293
-
294
- Creates a reactive state object. Any property access during render is tracked, and changes trigger re-renders.
295
-
296
- ```tsx
297
- import { createState } from "rask-ui";
298
-
299
- function Example() {
300
- const state = createState({
301
- count: 0,
302
- items: ["a", "b", "c"],
303
- nested: { value: 42 },
304
- });
305
-
306
- // All mutations are reactive
307
- state.count++;
308
- state.items.push("d");
309
- state.nested.value = 100;
310
-
311
- return () => <div>{state.count}</div>;
312
- }
313
- ```
314
-
315
- **Parameters:**
316
-
317
- - `initialState: T` - Initial state object
318
-
319
- **Returns:** Reactive proxy of the state object
320
-
321
- **Features:**
322
-
323
- - Deep reactivity - nested objects and arrays are automatically reactive
324
- - Direct mutations - no setter functions required
325
- - Efficient tracking - only re-renders components that access changed properties
326
-
327
- ---
328
-
329
- #### `assignState<T>(state, newState)`
330
-
331
- Merges properties from a new state object into an existing reactive state object. Returns the updated state object.
332
-
333
- ```tsx
334
- import { assignState, createState } from "rask-ui";
335
-
336
- function Example() {
337
- const state = createState({
338
- name: "Alice",
339
- age: 30,
340
- email: "alice@example.com",
341
- });
342
-
343
- const updateProfile = (updates) => {
344
- return assignState(state, updates);
345
- };
346
-
347
- return () => (
348
- <div>
349
- <p>{state.name} - {state.email}</p>
350
- <button onClick={() => updateProfile({ name: "Bob", age: 35 })}>
351
- Update Profile
352
- </button>
353
- </div>
354
- );
355
- }
356
- ```
357
-
358
- **Parameters:**
359
-
360
- - `state: T` - The reactive state object to update
361
- - `newState: T` - Object with properties to merge into the state
362
-
363
- **Returns:** The updated state object (same reference as input state)
364
-
365
- **Notes:**
366
-
367
- - Equivalent to `Object.assign(state, newState)` - returns the state for chaining
368
- - Triggers reactivity for all updated properties
369
- - Useful for bulk state updates from form data or API responses
370
-
371
- ---
372
-
373
- #### `createView<T>(...objects)`
374
-
375
- 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.
376
-
377
- ```tsx
378
- import { createView, createState } from "rask-ui";
379
-
380
- function createCounter() {
381
- const state = createState({ count: 0, name: "Counter" });
382
- const increment = () => state.count++;
383
- const decrement = () => state.count--;
384
- const reset = () => (state.count = 0);
385
-
386
- return createView(state, { increment, decrement, reset });
387
- }
388
-
389
- function Counter() {
390
- const counter = createCounter();
391
-
392
- return () => (
393
- <div>
394
- <h1>
395
- {counter.name}: {counter.count}
396
- </h1>
397
- <button onClick={counter.increment}>+</button>
398
- <button onClick={counter.decrement}>-</button>
399
- <button onClick={counter.reset}>Reset</button>
400
- </div>
401
- );
402
- }
403
- ```
404
-
405
- **Parameters:**
406
-
407
- - `...objects: object[]` - Objects to merge (reactive or plain). Later arguments override earlier ones.
408
-
409
- **Returns:** A view object with getters for all properties, maintaining reactivity
410
-
411
- **Notes:**
412
-
413
- - Reactivity is maintained through getters that reference the source objects
414
- - Changes to source objects are reflected in the view
415
- - Only enumerable properties are included
416
- - Symbol keys are supported
417
- - **Do not destructure** - See warning section above
418
-
419
- ---
420
-
421
- #### `createRef<T>()`
422
-
423
- Creates a ref object for accessing DOM elements or component instances directly.
424
-
425
- ```tsx
426
- import { createRef } from "rask-ui";
427
-
428
- function Example() {
429
- const inputRef = createRef<HTMLInputElement>();
430
-
431
- const focus = () => {
432
- inputRef.current?.focus();
433
- };
434
-
435
- return () => (
436
- <div>
437
- <input ref={inputRef} type="text" />
438
- <button onClick={focus}>Focus Input</button>
439
- </div>
440
- );
441
- }
442
- ```
443
-
444
- **Returns:** Ref object with:
445
-
446
- - `current: T | null` - Reference to the DOM element or component instance
447
- - Function signature for use as ref callback
448
-
449
- **Usage:**
450
-
451
- 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.
452
-
453
- ---
454
-
455
- ### Reactivity Primitives
456
-
457
- #### `createEffect(callback)`
458
-
459
- Creates an effect that automatically tracks reactive dependencies and re-runs whenever they change. The effect runs immediately on creation.
460
-
461
- ```tsx
462
- import { createEffect, createState } from "rask-ui";
463
-
464
- function Timer() {
465
- const state = createState({ count: 0, log: [] });
466
-
467
- // Effect runs immediately and whenever state.count changes
468
- createEffect(() => {
469
- console.log("Count changed:", state.count);
470
- state.log.push(`Count: ${state.count}`);
471
- });
472
-
473
- return () => (
474
- <div>
475
- <p>Count: {state.count}</p>
476
- <button onClick={() => state.count++}>Increment</button>
477
- <ul>
478
- {state.log.map((entry, i) => (
479
- <li key={i}>{entry}</li>
480
- ))}
481
- </ul>
482
- </div>
483
- );
484
- }
485
- ```
486
-
487
- **Effect with Disposal:**
488
-
489
- The callback can optionally return a dispose function that runs before the effect executes again:
490
-
491
- ```tsx
492
- import { createEffect, createState } from "rask-ui";
493
-
494
- function LiveData() {
495
- const state = createState({ url: "/api/data", data: null });
496
-
497
- createEffect(() => {
498
- const eventSource = new EventSource(state.url);
499
-
500
- eventSource.onmessage = (event) => {
501
- state.data = JSON.parse(event.data);
502
- };
503
-
504
- // Dispose function runs before effect re-executes
505
- return () => {
506
- eventSource.close();
507
- };
508
- });
509
-
510
- return () => (
511
- <div>
512
- <input value={state.url} onInput={(e) => state.url = e.target.value} />
513
- <pre>{JSON.stringify(state.data, null, 2)}</pre>
514
37
  </div>
515
38
  );
516
39
  }
517
- ```
518
-
519
- **Parameters:**
520
-
521
- - `callback: () => void | (() => void)` - Function to run when dependencies change. Can optionally return a dispose function that runs before the effect executes again.
522
-
523
- **Features:**
524
-
525
- - Runs immediately and synchronously on creation during setup
526
- - Automatically tracks reactive dependencies accessed during execution
527
- - Re-runs when dependencies change
528
- - Automatically cleaned up when component unmounts
529
- - Optional dispose function for cleaning up resources before re-execution
530
- - Can be used for side effects like logging, syncing to localStorage, managing subscriptions, or updating derived state
531
-
532
- **Notes:**
533
-
534
- - Only call during component setup phase (not in render function)
535
- - Effect runs synchronously during setup, making it predictable and easier to reason about
536
- - Be careful with effects that modify state - can cause infinite loops if not careful
537
- - Dispose functions run before the effect re-executes, not when the component unmounts
538
- - For component unmount cleanup, use `createCleanup()` instead
539
-
540
- ---
541
-
542
- #### `createComputed<T>(computed)`
543
-
544
- Creates an object with computed properties that automatically track dependencies and cache results until dependencies change.
545
-
546
- ```tsx
547
- import { createComputed, createState } from "rask-ui";
548
-
549
- function ShoppingCart() {
550
- const state = createState({
551
- items: [
552
- { id: 1, name: "Apple", price: 1.5, quantity: 3 },
553
- { id: 2, name: "Banana", price: 0.8, quantity: 5 },
554
- ],
555
- taxRate: 0.2,
556
- });
557
-
558
- const computed = createComputed({
559
- subtotal: () =>
560
- state.items.reduce((sum, item) => sum + item.price * item.quantity, 0),
561
- tax: () => computed.subtotal * state.taxRate,
562
- total: () => computed.subtotal + computed.tax,
563
- itemCount: () => state.items.reduce((sum, item) => sum + item.quantity, 0),
564
- });
565
40
 
566
- return () => (
567
- <div>
568
- <h2>Cart ({computed.itemCount} items)</h2>
569
- <ul>
570
- {state.items.map((item) => (
571
- <li key={item.id}>
572
- {item.name}: ${item.price} x {item.quantity}
573
- <button onClick={() => item.quantity++}>+</button>
574
- <button onClick={() => item.quantity--}>-</button>
575
- </li>
576
- ))}
577
- </ul>
578
- <div>
579
- <p>Subtotal: ${computed.subtotal.toFixed(2)}</p>
580
- <p>
581
- Tax ({state.taxRate * 100}%): ${computed.tax.toFixed(2)}
582
- </p>
583
- <p>
584
- <strong>Total: ${computed.total.toFixed(2)}</strong>
585
- </p>
586
- </div>
587
- </div>
588
- );
589
- }
41
+ render(<Counter />, document.getElementById("app"));
590
42
  ```
591
43
 
592
- **Parameters:**
593
-
594
- - `computed: T` - Object where each property is a function returning a computed value
595
-
596
- **Returns:** Reactive object with cached computed properties
597
-
598
- **Features:**
44
+ ## Reactive Primitives
599
45
 
600
- - **Lazy evaluation** - Computed values are only calculated when accessed
601
- - **Automatic caching** - Results are cached until dependencies change
602
- - **Dependency tracking** - Automatically tracks what state each computed depends on
603
- - **Composable** - Computed properties can depend on other computed properties
604
- - **Efficient** - Only recomputes when dirty (dependencies changed)
605
- - **Automatic cleanup** - Cleaned up when component unmounts
46
+ RASK provides a set of reactive primitives for building interactive UIs:
606
47
 
607
- **Notes:**
48
+ - **`createState`** - Create reactive state objects
49
+ - **`createEffect`** - Run side effects when dependencies change
50
+ - **`createComputed`** - Derive values from state with automatic caching
51
+ - **`createTask`** - Manage async operations (fetch, mutations, etc.)
52
+ - **`createRouter`** - Type-safe client-side routing
53
+ - **`createContext`** - Share data through the component tree
54
+ - **`createView`** - Compose state and methods into reusable objects
608
55
 
609
- - Access computed properties directly (e.g., `computed.total`), don't call as functions
610
- - Computed properties are getters, not functions
611
- - **Do not destructure** - Breaks reactivity (see warning section above)
612
- - Only call during component setup phase
56
+ Visit [rask-ui.io](https://rask-ui.io) for complete API documentation and guides.
613
57
 
614
- ---
615
-
616
- ### Automatic Batching
617
-
618
- RASK automatically batches state updates to minimize re-renders. This happens transparently without any special syntax.
619
-
620
- **How it works:**
621
-
622
- - **User interactions** (clicks, inputs, keyboard, etc.) - State changes are batched and flushed synchronously at the end of the event
623
- - **Other updates** (setTimeout, fetch callbacks, etc.) - State changes are batched and flushed on the next microtask
624
-
625
- ```tsx
626
- function BatchingExample() {
627
- const state = createState({ count: 0, clicks: 0 });
628
-
629
- const handleClick = () => {
630
- // All three updates are batched into a single render
631
- state.count++;
632
- state.clicks++;
633
- state.count++;
634
- // UI updates once with count=2, clicks=1
635
- };
636
-
637
- const handleAsync = () => {
638
- setTimeout(() => {
639
- // These updates are also batched (async batch)
640
- state.count++;
641
- state.clicks++;
642
- // UI updates once on next microtask
643
- }, 100);
644
- };
645
-
646
- return () => (
647
- <div>
648
- <p>
649
- Count: {state.count}, Clicks: {state.clicks}
650
- </p>
651
- <button onClick={handleClick}>Sync Update</button>
652
- <button onClick={handleAsync}>Async Update</button>
653
- </div>
654
- );
655
- }
656
- ```
657
-
658
- ---
659
-
660
- ### Lifecycle Hooks
661
-
662
- #### `createMountEffect(callback)`
663
-
664
- Registers a callback to run after the component is mounted to the DOM.
665
-
666
- ```tsx
667
- import { createMountEffect } from "rask-ui";
668
-
669
- function Example() {
670
- createMountEffect(() => {
671
- console.log("Component mounted!");
672
- });
673
-
674
- return () => <div>Hello</div>;
675
- }
676
- ```
677
-
678
- **Parameters:**
679
-
680
- - `callback: () => void` - Function to call on mount. Can optionally return a cleanup function.
681
-
682
- **Notes:**
683
-
684
- - Only call during component setup phase (not in render function)
685
- - Can be called multiple times to register multiple mount callbacks
686
-
687
- ---
688
-
689
- #### `createCleanup(callback)`
690
-
691
- Registers a callback to run when the component is unmounted.
692
-
693
- ```tsx
694
- import { createCleanup } from "rask-ui";
695
-
696
- function Example() {
697
- const state = createState({ time: Date.now() });
698
-
699
- const interval = setInterval(() => {
700
- state.time = Date.now();
701
- }, 1000);
702
-
703
- createCleanup(() => {
704
- clearInterval(interval);
705
- });
706
-
707
- return () => <div>{state.time}</div>;
708
- }
709
- ```
710
-
711
- **Parameters:**
712
-
713
- - `callback: () => void` - Function to call on cleanup
714
-
715
- **Notes:**
716
-
717
- - Only call during component setup phase
718
- - Can be called multiple times to register multiple cleanup callbacks
719
- - Runs when component is removed from DOM
720
-
721
- ---
722
-
723
- ### Context API
724
-
725
- #### `createContext<T>()`
726
-
727
- Creates a context object for passing data through the component tree without props.
728
-
729
- ```tsx
730
- import { createContext } from "rask-ui";
731
-
732
- const ThemeContext = createContext<{ color: string }>();
733
-
734
- function App() {
735
- ThemeContext.inject({ color: "blue" });
736
-
737
- return () => <Child />;
738
- }
739
-
740
- function Child() {
741
- const theme = ThemeContext.get();
742
-
743
- return () => <div style={{ color: theme.color }}>Themed text</div>;
744
- }
745
- ```
746
-
747
- **Returns:** Context object with `inject` and `get` methods
748
-
749
- **Methods:**
750
-
751
- - `inject(value: T)` - Injects context value for child components (call during setup)
752
- - `get(): T` - Gets context value from nearest parent (call during setup)
753
-
754
- **Notes:**
755
-
756
- - Context traversal happens via component tree (parent-child relationships)
757
- - Must be called during component setup phase
758
- - Throws error if context not found in parent chain
759
-
760
- ---
761
-
762
- ### Async Data Management
763
-
764
- #### `createTask<P, T>(task)`
765
-
766
- A low-level reactive primitive for managing any async operation. `createTask` provides the foundation for building data fetching, mutations, polling, debouncing, and any other async pattern you need. It gives you full control without prescribing specific patterns.
767
-
768
- The `Task<P, T>` type is also exported for type annotations. When used with one type parameter, it represents a task with no parameters. With two type parameters, the first is the params type and the second is the return type.
769
-
770
- ```tsx
771
- import { createTask, createState } from "rask-ui";
772
-
773
- // Fetching data - auto-runs on creation
774
- function UserProfile() {
775
- const user = createTask(() => fetch("/api/user").then((r) => r.json()));
776
-
777
- return () => {
778
- if (user.isRunning) {
779
- return <p>Loading...</p>;
780
- }
781
-
782
- if (user.error) {
783
- return <p>Error: {user.error}</p>;
784
- }
785
-
786
- return <p>Hello, {user.result.name}!</p>;
787
- };
788
- }
789
-
790
- // Fetching with parameters
791
- function Posts() {
792
- const state = createState({ page: 1 });
793
-
794
- const posts = createTask((page: number) =>
795
- fetch(`/api/posts?page=${page}&limit=10`).then((r) => r.json())
796
- );
797
-
798
- // Fetch when page changes
799
- createEffect(() => {
800
- posts.run(state.page);
801
- });
802
-
803
- return () => (
804
- <div>
805
- <h1>Posts - Page {state.page}</h1>
806
- {posts.isRunning && <p>Loading...</p>}
807
- {posts.result?.map((post) => (
808
- <article key={post.id}>{post.title}</article>
809
- ))}
810
- <button onClick={() => state.page--}>Previous</button>
811
- <button onClick={() => state.page++}>Next</button>
812
- </div>
813
- );
814
- }
815
-
816
- // Mutation - creating data on server
817
- function CreatePost() {
818
- const state = createState({ title: "", body: "" });
819
-
820
- const createPost = createTask((data: { title: string; body: string }) =>
821
- fetch("/api/posts", {
822
- method: "POST",
823
- headers: { "Content-Type": "application/json" },
824
- body: JSON.stringify(data),
825
- }).then((r) => r.json())
826
- );
827
-
828
- const handleSubmit = async () => {
829
- await createPost.run({ title: state.title, body: state.body });
830
- // Clear form on success
831
- state.title = "";
832
- state.body = "";
833
- };
834
-
835
- return () => (
836
- <form onSubmit={handleSubmit}>
837
- <input
838
- placeholder="Title"
839
- value={state.title}
840
- onInput={(e) => (state.title = e.target.value)}
841
- />
842
- <textarea
843
- placeholder="Body"
844
- value={state.body}
845
- onInput={(e) => (state.body = e.target.value)}
846
- />
847
- <button disabled={createPost.isRunning}>
848
- {createPost.isRunning ? "Creating..." : "Create Post"}
849
- </button>
850
- {createPost.error && <p>Error: {createPost.error}</p>}
851
- {createPost.result && <p>Post created! ID: {createPost.result.id}</p>}
852
- </form>
853
- );
854
- }
855
-
856
- // Optimistic updates - instant UI updates with rollback on error
857
- function TodoList() {
858
- const state = createState({
859
- todos: [],
860
- optimisticTodo: null,
861
- });
862
-
863
- const createTodo = createTask((text: string) =>
864
- fetch("/api/todos", {
865
- method: "POST",
866
- headers: { "Content-Type": "application/json" },
867
- body: JSON.stringify({ text, done: false }),
868
- }).then((r) => r.json())
869
- );
870
-
871
- const addTodo = async (text: string) => {
872
- // Show optimistically
873
- state.optimisticTodo = { id: Date.now(), text, done: false };
874
-
875
- try {
876
- const savedTodo = await createTodo.run(text);
877
- state.todos.push(savedTodo);
878
- state.optimisticTodo = null;
879
- } catch {
880
- // Rollback on error
881
- state.optimisticTodo = null;
882
- }
883
- };
884
-
885
- return () => (
886
- <div>
887
- <ul>
888
- {[...state.todos, state.optimisticTodo]
889
- .filter(Boolean)
890
- .map((todo) => (
891
- <li key={todo.id} style={{ opacity: todo === state.optimisticTodo ? 0.5 : 1 }}>
892
- {todo.text}
893
- </li>
894
- ))}
895
- </ul>
896
- <button onClick={() => addTodo("New todo")}>Add Todo</button>
897
- </div>
898
- );
899
- }
900
- ```
901
-
902
- **Type Signatures:**
903
-
904
- ```tsx
905
- // Task without parameters - auto-runs on creation
906
- createTask<T>(task: () => Promise<T>): Task<T> & {
907
- run(): Promise<T>;
908
- rerun(): Promise<T>;
909
- }
910
-
911
- // Task with parameters - manual control
912
- createTask<P, T>(task: (params: P) => Promise<T>): Task<P, T> & {
913
- run(params: P): Promise<T>;
914
- rerun(params: P): Promise<T>;
915
- }
916
- ```
917
-
918
- **Task Type:**
919
-
920
- The `Task` type is exported and can be used for type annotations:
921
-
922
- ```tsx
923
- import { Task } from "rask-ui";
924
-
925
- // Task that returns a string, no parameters
926
- const myTask: Task<string>;
927
-
928
- // Task that accepts string parameters and returns a string
929
- const myTask: Task<string, string>;
930
-
931
- // Task that accepts a number parameter and returns a User object
932
- const myTask: Task<number, User>;
933
- ```
934
-
935
- **Returns:** Task object with reactive state and methods:
936
-
937
- **State Properties:**
938
- - `isRunning: boolean` - True while task is executing
939
- - `result: T | null` - Result of successful execution (null if not yet run, running, or error)
940
- - `error: string | null` - Error message from failed execution (null if successful or running)
941
- - `params: P | null` - Current parameters while running (null when idle)
942
-
943
- **Methods:**
944
- - `run(params?: P): Promise<T>` - Execute the task, clearing previous result
945
- - `rerun(params?: P): Promise<T>` - Re-execute the task, keeping previous result until new one arrives
946
-
947
- **State Transitions:**
948
-
949
- Initial state (no params):
950
- ```tsx
951
- { isRunning: false, params: null, result: null, error: null }
952
- ```
953
-
954
- While running:
955
- ```tsx
956
- { isRunning: true, result: T | null, params: P, error: null }
957
- ```
958
-
959
- Success:
960
- ```tsx
961
- { isRunning: false, params: null, result: T, error: null }
962
- ```
963
-
964
- Error:
965
- ```tsx
966
- { isRunning: false, params: null, result: null, error: string }
967
- ```
968
-
969
- **Features:**
970
-
971
- - **Automatic cancellation** - Previous executions are cancelled when a new one starts
972
- - **Flexible control** - Use `run()` to clear old data or `rerun()` to keep it during loading
973
- - **Type-safe** - Full TypeScript inference for parameters and results
974
- - **Auto-run support** - Tasks without parameters run automatically on creation
975
- - **Generic primitive** - Build your own patterns on top (queries, mutations, etc.)
976
-
977
- **Usage Patterns:**
978
-
979
- `createTask` is a low-level primitive. Use it as a building block for any async pattern:
980
- - **Data fetching**: Fetch data on mount or based on dependencies
981
- - **Mutations**: Create, update, or delete data on the server
982
- - **Optimistic updates**: Update UI instantly with rollback on error
983
- - **Polling**: Periodically refetch data
984
- - **Debounced searches**: Wait for user input to settle
985
- - **Dependent queries**: Chain requests that depend on each other
986
- - **Parallel requests**: Run multiple requests simultaneously
987
- - **Custom patterns**: Build your own abstractions (queries, mutations, etc.)
988
-
989
- ---
990
-
991
- ### Routing
992
-
993
- #### `createRouter<T>(config, options?)`
994
-
995
- Creates a reactive router for client-side navigation. Built on [typed-client-router](https://github.com/christianalfoni/typed-client-router), it integrates seamlessly with RASK's reactive system for fully type-safe routing.
996
-
997
- ```tsx
998
- import { createRouter } from "rask-ui";
999
-
1000
- const routes = {
1001
- home: "/",
1002
- about: "/about",
1003
- user: "/users/:id",
1004
- post: "/posts/:id",
1005
- } as const;
1006
-
1007
- function App() {
1008
- const router = createRouter(routes);
1009
-
1010
- return () => {
1011
- // Match current route
1012
- if (router.route?.name === "home") {
1013
- return <Home />;
1014
- }
1015
-
1016
- if (router.route?.name === "user") {
1017
- return <User id={router.route.params.id} />;
1018
- }
1019
-
1020
- if (router.route?.name === "post") {
1021
- return <Post id={router.route.params.id} />;
1022
- }
1023
-
1024
- return <NotFound />;
1025
- };
1026
- }
1027
- ```
1028
-
1029
- **Parameters:**
1030
-
1031
- - `config: T` - Route configuration object mapping route names to path patterns
1032
- - `options?: { base?: string }` - Optional base path for all routes
1033
-
1034
- **Returns:** Router object with reactive state and navigation methods:
1035
-
1036
- **Properties:**
1037
- - `route?: Route` - Current active route with `name` and `params` properties (reactive)
1038
- - `queries: Record<string, string>` - Current URL query parameters (reactive)
1039
-
1040
- **Methods:**
1041
- - `push(name, params?, query?)` - Navigate to a route by name
1042
- - `replace(name, params?, query?)` - Replace current route (no history entry)
1043
- - `setQuery(query)` - Update query parameters
1044
- - `url(name, params?, query?)` - Generate URL for a route
1045
-
1046
- **Route Configuration:**
1047
-
1048
- Define routes with path patterns. Use `:param` for dynamic segments:
1049
-
1050
- ```tsx
1051
- const routes = {
1052
- home: "/",
1053
- users: "/users",
1054
- user: "/users/:id",
1055
- userPosts: "/users/:userId/posts/:postId",
1056
- } as const;
1057
- ```
1058
-
1059
- **Navigation:**
1060
-
1061
- Navigate programmatically using route names:
1062
-
1063
- ```tsx
1064
- function Navigation() {
1065
- const router = createRouter(routes);
1066
-
1067
- return () => (
1068
- <nav>
1069
- <button onClick={() => router.push("home")}>Home</button>
1070
- <button onClick={() => router.push("user", { id: "123" })}>
1071
- User 123
1072
- </button>
1073
- <button onClick={() => router.push("home", {}, { tab: "recent" })}>
1074
- Home (Recent)
1075
- </button>
1076
- </nav>
1077
- );
1078
- }
1079
- ```
1080
-
1081
- **Query Parameters:**
1082
-
1083
- Access and modify query parameters:
1084
-
1085
- ```tsx
1086
- function SearchPage() {
1087
- const router = createRouter(routes);
1088
-
1089
- return () => (
1090
- <div>
1091
- <p>Search: {router.queries.q || "none"}</p>
1092
- <input
1093
- value={router.queries.q || ""}
1094
- onInput={(e) => router.setQuery({ q: e.target.value })}
1095
- />
1096
- </div>
1097
- );
1098
- }
1099
- ```
1100
-
1101
- **Reactive Routing:**
1102
-
1103
- The router integrates with RASK's reactivity system. Accessing `router.route` or `router.queries` automatically tracks dependencies:
1104
-
1105
- ```tsx
1106
- function App() {
1107
- const router = createRouter(routes);
1108
- const state = createState({ posts: [] });
1109
-
1110
- // Effect runs when route changes
1111
- createEffect(() => {
1112
- if (router.route?.name === "user") {
1113
- // Fetch user data when route changes
1114
- fetch(`/api/users/${router.route.params.id}`)
1115
- .then((r) => r.json())
1116
- .then((data) => (state.posts = data.posts));
1117
- }
1118
- });
1119
-
1120
- return () => (
1121
- <div>
1122
- {router.route?.name === "user" && (
1123
- <div>
1124
- <h1>User {router.route.params.id}</h1>
1125
- <ul>
1126
- {state.posts.map((post) => (
1127
- <li key={post.id}>{post.title}</li>
1128
- ))}
1129
- </ul>
1130
- </div>
1131
- )}
1132
- </div>
1133
- );
1134
- }
1135
- ```
1136
-
1137
- **Type Safety:**
1138
-
1139
- Routes are fully type-safe. TypeScript will infer parameter types from route patterns:
1140
-
1141
- ```tsx
1142
- const routes = {
1143
- user: "/users/:id",
1144
- post: "/posts/:postId/:commentId",
1145
- } as const;
1146
-
1147
- const router = createRouter(routes);
1148
-
1149
- // ✅ Type-safe - id is required
1150
- router.push("user", { id: "123" });
1151
-
1152
- // ❌ Type error - missing required params
1153
- router.push("post", { postId: "1" }); // Error: missing commentId
1154
-
1155
- // ✅ Type-safe params access
1156
- if (router.route?.name === "post") {
1157
- const postId = router.route.params.postId; // string
1158
- const commentId = router.route.params.commentId; // string
1159
- }
1160
- ```
1161
-
1162
- **Context Pattern:**
1163
-
1164
- Share router across components using context:
1165
-
1166
- ```tsx
1167
- import { createRouter, createContext } from "rask-ui";
1168
-
1169
- const routes = {
1170
- home: "/",
1171
- about: "/about",
1172
- } as const;
1173
-
1174
- const RouterContext = createContext<Router<typeof routes>>();
1175
-
1176
- function App() {
1177
- const router = createRouter(routes);
1178
-
1179
- RouterContext.inject(router);
1180
-
1181
- return () => <Content />;
1182
- }
1183
-
1184
- function Content() {
1185
- const router = RouterContext.get();
1186
-
1187
- return () => (
1188
- <nav>
1189
- <button onClick={() => router.push("home")}>Home</button>
1190
- <button onClick={() => router.push("about")}>About</button>
1191
- </nav>
1192
- );
1193
- }
1194
- ```
1195
-
1196
- **Features:**
1197
-
1198
- - **Type-safe** - Full TypeScript inference for routes and parameters
1199
- - **Reactive** - Automatically tracks route changes
1200
- - **Declarative** - Navigate using route names, not URLs
1201
- - **Query parameters** - Built-in query string management
1202
- - **No special components** - Use standard conditionals and component composition
1203
- - **History API** - Built on the browser's History API
1204
- - **Automatic cleanup** - Router listener cleaned up when component unmounts
1205
-
1206
- ---
1207
-
1208
- ### Developer Tools
1209
-
1210
- #### `inspect(root, callback)`
1211
-
1212
- Enables inspection of reactive state, computed values, and actions for building devtools. This API is **development-only** and has zero overhead in production builds.
1213
-
1214
- ```tsx
1215
- import { inspect } from "rask-ui";
1216
-
1217
- function DevToolsIntegration() {
1218
- const state = createState({ count: 0, name: "Example" });
1219
- const computed = createComputed({
1220
- double: () => state.count * 2,
1221
- });
1222
-
1223
- const actions = {
1224
- increment: () => state.count++,
1225
- reset: () => state.count = 0,
1226
- };
1227
-
1228
- const view = createView(state, computed, actions);
1229
-
1230
- // Inspect all reactive events
1231
- inspect(view, (event) => {
1232
- console.log(event);
1233
- // Send to devtools panel, logging service, etc.
1234
- });
1235
-
1236
- return () => (
1237
- <div>
1238
- <h1>{view.name}: {view.count}</h1>
1239
- <p>Double: {view.double}</p>
1240
- <button onClick={view.increment}>+</button>
1241
- <button onClick={view.reset}>Reset</button>
1242
- </div>
1243
- );
1244
- }
1245
- ```
1246
-
1247
- **Parameters:**
1248
-
1249
- - `root: any` - The reactive object to inspect (state, view, computed, etc.)
1250
- - `callback: (event: InspectEvent) => void` - Callback receiving inspection events
1251
-
1252
- **Event Types:**
1253
-
1254
- ```tsx
1255
- type InspectEvent =
1256
- | {
1257
- type: "mutation";
1258
- path: string[]; // Property path, e.g., ["user", "name"]
1259
- value: any; // New value
1260
- }
1261
- | {
1262
- type: "action";
1263
- path: string[]; // Function name path, e.g., ["increment"]
1264
- params: any[]; // Function parameters
1265
- }
1266
- | {
1267
- type: "computed";
1268
- path: string[]; // Computed property path
1269
- isDirty: boolean; // true when invalidated, false when recomputed
1270
- value: any; // Current or recomputed value
1271
- };
1272
- ```
1273
-
1274
- **Features:**
1275
-
1276
- - **Zero production overhead** - Completely eliminated in production builds via tree-shaking
1277
- - **Deep tracking** - Tracks nested state mutations with full property paths
1278
- - **Action tracking** - Captures function calls with parameters
1279
- - **Computed lifecycle** - Observes when computed values become dirty and when they recompute
1280
- - **Nested view support** - Compose deeply nested state trees and track full paths
1281
- - **JSON serialization** - Use `JSON.stringify()` to extract state snapshots
1282
- - **Flexible integration** - Build custom devtools, time-travel debugging, or logging systems
1283
-
1284
- **Use Cases:**
1285
-
1286
- - Building browser devtools extensions
1287
- - Creating debugging panels for development
1288
- - Implementing time-travel debugging
1289
- - Logging state changes for debugging
1290
- - Integrating with external monitoring tools
1291
- - Building replay systems for bug reports
1292
-
1293
- **Production Builds:**
1294
-
1295
- The inspector is automatically stripped from production builds using Vite's `import.meta.env.DEV` constant. This means:
1296
-
1297
- - No runtime checks in production code
1298
- - No function wrapping overhead
1299
- - No symbol property lookups
1300
- - Smaller bundle size
1301
- - Zero performance impact
1302
-
1303
- ```tsx
1304
- // In development
1305
- inspect(view, console.log); // ✅ Works, logs all events
1306
-
1307
- // In production build
1308
- inspect(view, console.log); // No-op, zero overhead, removed by tree-shaking
1309
- ```
1310
-
1311
- **Nested State Trees:**
1312
-
1313
- The inspector automatically tracks nested property paths. Compose deeply nested views to create organized state trees:
1314
-
1315
- ```tsx
1316
- function App() {
1317
- // Create nested state structure
1318
- const userState = createState({
1319
- profile: { name: "Alice", email: "alice@example.com" },
1320
- preferences: { theme: "dark", notifications: true },
1321
- });
1322
-
1323
- const cartState = createState({
1324
- items: [],
1325
- total: 0,
1326
- });
1327
-
1328
- // Compose into a state tree
1329
- const appState = createView({
1330
- user: createView(userState),
1331
- cart: createView(cartState),
1332
- });
1333
-
1334
- inspect(appState, (event) => {
1335
- console.log(event);
1336
- });
1337
-
1338
- // When you do: appState.user.profile.name = "Bob"
1339
- // You receive: { type: "mutation", path: ["user", "profile", "name"], value: "Bob" }
1340
-
1341
- return () => (
1342
- <div>
1343
- <h1>Welcome, {appState.user.profile.name}!</h1>
1344
- <p>Theme: {appState.user.preferences.theme}</p>
1345
- <p>Cart items: {appState.cart.items.length}</p>
1346
- </div>
1347
- );
1348
- }
1349
- ```
1350
-
1351
- **JSON Serialization:**
1352
-
1353
- Use `JSON.stringify()` to extract state snapshots for devtools, persistence, or debugging:
1354
-
1355
- ```tsx
1356
- function App() {
1357
- const state = createState({
1358
- user: { id: 1, name: "Alice" },
1359
- todos: [
1360
- { id: 1, text: "Learn RASK", done: false },
1361
- { id: 2, text: "Build app", done: true },
1362
- ],
1363
- });
1364
-
1365
- const computed = createComputed({
1366
- completedCount: () => state.todos.filter((t) => t.done).length,
1367
- });
1368
-
1369
- const view = createView(state, computed);
1370
-
1371
- // Serialize to JSON - includes computed values
1372
- const snapshot = JSON.stringify(view);
1373
- // {
1374
- // "user": { "id": 1, "name": "Alice" },
1375
- // "todos": [...],
1376
- // "completedCount": 1
1377
- // }
1378
-
1379
- // Send initial state and updates to devtools
1380
- if (import.meta.env.DEV) {
1381
- window.postMessage({
1382
- type: "RASK_DEVTOOLS_INIT",
1383
- initialState: JSON.parse(snapshot),
1384
- }, "*");
1385
-
1386
- inspect(view, (event) => {
1387
- window.postMessage({
1388
- type: "RASK_DEVTOOLS_EVENT",
1389
- event,
1390
- snapshot: JSON.parse(JSON.stringify(view)),
1391
- }, "*");
1392
- });
1393
- }
1394
-
1395
- return () => <div>{/* Your app */}</div>;
1396
- }
1397
- ```
1398
-
1399
- ---
1400
-
1401
- ### Error Handling
1402
-
1403
- #### `ErrorBoundary`
1404
-
1405
- Component that catches errors from child components.
1406
-
1407
- ```tsx
1408
- import { ErrorBoundary } from "rask-ui";
1409
-
1410
- function App() {
1411
- return () => (
1412
- <ErrorBoundary
1413
- error={(err) => (
1414
- <div>
1415
- <h1>Something went wrong</h1>
1416
- <pre>{String(err)}</pre>
1417
- </div>
1418
- )}
1419
- >
1420
- <MyComponent />
1421
- </ErrorBoundary>
1422
- );
1423
- }
1424
-
1425
- function MyComponent() {
1426
- const state = createState({ count: 0 });
1427
-
1428
- return () => {
1429
- if (state.count > 5) {
1430
- throw new Error("Count too high!");
1431
- }
1432
-
1433
- return <button onClick={() => state.count++}>{state.count}</button>;
1434
- };
1435
- }
1436
- ```
1437
-
1438
- **Props:**
1439
-
1440
- - `error: (error: unknown) => ChildNode | ChildNode[]` - Render function for error state
1441
- - `children` - Child components to protect
1442
-
1443
- **Notes:**
1444
-
1445
- - Catches errors during render phase
1446
- - Errors bubble up to nearest ErrorBoundary
1447
- - Can be nested for granular error handling
1448
-
1449
- ---
1450
-
1451
- ## Advanced Patterns
1452
-
1453
- ### Lists and Keys
1454
-
1455
- Use keys to maintain component identity across re-renders:
1456
-
1457
- ```tsx
1458
- function TodoList() {
1459
- const state = createState({
1460
- todos: [
1461
- { id: 1, text: "Learn RASK" },
1462
- { id: 2, text: "Build app" },
1463
- ],
1464
- });
1465
-
1466
- return () => (
1467
- <ul>
1468
- {state.todos.map((todo) => (
1469
- <TodoItem key={todo.id} todo={todo} />
1470
- ))}
1471
- </ul>
1472
- );
1473
- }
1474
-
1475
- function TodoItem(props) {
1476
- const state = createState({ editing: false });
1477
-
1478
- return () => (
1479
- <li>
1480
- {state.editing ? (
1481
- <input value={props.todo.text} />
1482
- ) : (
1483
- <span onClick={() => (state.editing = true)}>{props.todo.text}</span>
1484
- )}
1485
- </li>
1486
- );
1487
- }
1488
- ```
1489
-
1490
- Keys prevent component recreation when list order changes.
1491
-
1492
- ### Computed Values
1493
-
1494
- You can create computed values in two ways:
1495
-
1496
- **1. Simple computed functions** - For basic derived values:
1497
-
1498
- ```tsx
1499
- function ShoppingCart() {
1500
- const state = createState({
1501
- items: [
1502
- { id: 1, price: 10, quantity: 2 },
1503
- { id: 2, price: 20, quantity: 1 },
1504
- ],
1505
- });
1506
-
1507
- const total = () =>
1508
- state.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
1509
-
1510
- return () => (
1511
- <div>
1512
- <ul>
1513
- {state.items.map((item) => (
1514
- <li key={item.id}>
1515
- ${item.price} x {item.quantity}
1516
- </li>
1517
- ))}
1518
- </ul>
1519
- <p>Total: ${total()}</p>
1520
- </div>
1521
- );
1522
- }
1523
- ```
1524
-
1525
- Computed functions automatically track dependencies when called during render.
1526
-
1527
- **2. Using `createComputed`** - For cached, efficient computed values with automatic dependency tracking:
1528
-
1529
- ```tsx
1530
- function ShoppingCart() {
1531
- const state = createState({
1532
- items: [
1533
- { id: 1, price: 10, quantity: 2 },
1534
- { id: 2, price: 20, quantity: 1 },
1535
- ],
1536
- taxRate: 0.1,
1537
- });
1538
-
1539
- const computed = createComputed({
1540
- subtotal: () =>
1541
- state.items.reduce((sum, item) => sum + item.price * item.quantity, 0),
1542
- tax: () => computed.subtotal * state.taxRate,
1543
- total: () => computed.subtotal + computed.tax,
1544
- });
1545
-
1546
- return () => (
1547
- <div>
1548
- <ul>
1549
- {state.items.map((item) => (
1550
- <li key={item.id}>
1551
- ${item.price} x {item.quantity}
1552
- </li>
1553
- ))}
1554
- </ul>
1555
- <p>Subtotal: ${computed.subtotal}</p>
1556
- <p>Tax: ${computed.tax}</p>
1557
- <p>Total: ${computed.total}</p>
1558
- </div>
1559
- );
1560
- }
1561
- ```
1562
-
1563
- Benefits of `createComputed`:
1564
-
1565
- - **Cached** - Only recalculates when dependencies change
1566
- - **Lazy** - Only calculates when accessed
1567
- - **Composable** - Computed properties can depend on other computed properties
1568
- - **Efficient** - Better performance for expensive calculations
1569
-
1570
- ### Composition
1571
-
1572
- Compose complex logic by combining state and methods using `createView`:
1573
-
1574
- ```tsx
1575
- function createAuthStore() {
1576
- const state = createState({
1577
- user: null,
1578
- isAuthenticated: false,
1579
- isLoading: false,
1580
- });
1581
-
1582
- const login = async (username, password) => {
1583
- state.isLoading = true;
1584
- try {
1585
- const user = await fetch("/api/login", {
1586
- method: "POST",
1587
- body: JSON.stringify({ username, password }),
1588
- }).then((r) => r.json());
1589
- state.user = user;
1590
- state.isAuthenticated = true;
1591
- } finally {
1592
- state.isLoading = false;
1593
- }
1594
- };
1595
-
1596
- const logout = () => {
1597
- state.user = null;
1598
- state.isAuthenticated = false;
1599
- };
1600
-
1601
- return createView(state, { login, logout });
1602
- }
1603
-
1604
- function App() {
1605
- const auth = createAuthStore();
1606
-
1607
- return () => (
1608
- <div>
1609
- {auth.isAuthenticated ? (
1610
- <div>
1611
- <p>Welcome, {auth.user.name}!</p>
1612
- <button onClick={auth.logout}>Logout</button>
1613
- </div>
1614
- ) : (
1615
- <button onClick={() => auth.login("user", "pass")}>Login</button>
1616
- )}
1617
- </div>
1618
- );
1619
- }
1620
- ```
1621
-
1622
- This pattern is great for organizing complex business logic while keeping both state and methods accessible through a single object.
1623
-
1624
- ### External State Management
1625
-
1626
- Share state across components:
1627
-
1628
- ```tsx
1629
- // store.ts
1630
- export const store = createState({
1631
- user: null,
1632
- theme: "light",
1633
- });
1634
-
1635
- // App.tsx
1636
- import { store } from "./store";
1637
-
1638
- function Header() {
1639
- return () => <div>Theme: {store.theme}</div>;
1640
- }
1641
-
1642
- function Settings() {
1643
- return () => (
1644
- <button
1645
- onClick={() => (store.theme = store.theme === "light" ? "dark" : "light")}
1646
- >
1647
- Toggle Theme
1648
- </button>
1649
- );
1650
- }
1651
- ```
1652
-
1653
- Any component accessing `store` will re-render when it changes.
1654
-
1655
- ### Conditional Rendering
1656
-
1657
- ```tsx
1658
- function Conditional() {
1659
- const state = createState({ show: false });
1660
-
1661
- return () => (
1662
- <div>
1663
- <button onClick={() => (state.show = !state.show)}>Toggle</button>
1664
- {state.show && <ExpensiveComponent />}
1665
- </div>
1666
- );
1667
- }
1668
- ```
1669
-
1670
- Components are only created when rendered, and automatically cleaned up when removed.
1671
-
1672
- ## TypeScript Support
1673
-
1674
- RASK is written in TypeScript and provides full type inference:
1675
-
1676
- ```tsx
1677
- import { createState, Component } from "rask-ui";
1678
-
1679
- interface Todo {
1680
- id: number;
1681
- text: string;
1682
- done: boolean;
1683
- }
1684
-
1685
- interface TodoItemProps {
1686
- todo: Todo;
1687
- onToggle: (id: number) => void;
1688
- }
1689
-
1690
- const TodoItem: Component<TodoItemProps> = (props) => {
1691
- return () => (
1692
- <li onClick={() => props.onToggle(props.todo.id)}>{props.todo.text}</li>
1693
- );
1694
- };
1695
-
1696
- function TodoList() {
1697
- const state = createState<{ todos: Todo[] }>({
1698
- todos: [],
1699
- });
1700
-
1701
- const toggle = (id: number) => {
1702
- const todo = state.todos.find((t) => t.id === id);
1703
- if (todo) todo.done = !todo.done;
1704
- };
1705
-
1706
- return () => (
1707
- <ul>
1708
- {state.todos.map((todo) => (
1709
- <TodoItem key={todo.id} todo={todo} onToggle={toggle} />
1710
- ))}
1711
- </ul>
1712
- );
1713
- }
1714
- ```
1715
-
1716
- ## Performance
1717
-
1718
- RASK is designed for performance:
1719
-
1720
- - **Fine-grained reactivity**: Only components that access changed state re-render
1721
- - **No wasted renders**: Components skip re-render if reactive dependencies haven't changed
1722
- - **Efficient DOM updates**: Powered by Inferno's highly optimized virtual DOM reconciler
1723
- - **No reconciler overhead for state**: State changes are direct, no diffing required
1724
- - **Automatic cleanup**: Components and effects cleaned up automatically
1725
-
1726
- ## Comparison with Other Frameworks
1727
-
1728
- | Feature | React | Solid | RASK |
1729
- | ----------------- | ------------------------- | ------------------------ | ---------------- |
1730
- | State management | Complex (hooks, closures) | Simple (signals) | Simple (proxies) |
1731
- | UI expression | Excellent | Limited | Excellent |
1732
- | Reactivity | Coarse (component level) | Fine-grained | Fine-grained |
1733
- | Reconciler | Yes | Limited | Yes (Inferno) |
1734
- | Syntax | JSX | JSX + special components | JSX |
1735
- | Compiler required | No | Yes | No |
1736
- | Learning curve | Steep | Moderate | Gentle |
1737
- | Access pattern | Direct | Function calls `count()` | Direct |
1738
- | Mental model | Complex | Simple (with rules) | Simple |
1739
-
1740
- ## Examples
1741
-
1742
- Check out the demo app in `packages/demo` for more examples.
1743
-
1744
- ## Contributing
1745
-
1746
- Contributions are welcome! This is an early-stage project.
1747
-
1748
- ## License
58
+ ## License
1749
59
 
1750
60
  MIT
1751
-
1752
- ## Why "RASK"?
1753
-
1754
- The name comes from Norwegian meaning "fast" - which captures the essence of this library: fast to write, fast to understand, and fast to run.