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 +20 -1714
- package/dist/component.d.ts +2 -2
- package/dist/component.d.ts.map +1 -1
- package/dist/component.js +4 -6
- package/dist/createComputed.d.ts.map +1 -1
- package/dist/createComputed.js +2 -10
- package/dist/createEffect.d.ts.map +1 -1
- package/dist/createEffect.js +8 -13
- package/dist/createRouter.d.ts.map +1 -1
- package/dist/createRouter.js +4 -1
- package/dist/createState.d.ts.map +1 -1
- package/dist/createState.js +4 -0
- package/dist/createTask.d.ts +2 -2
- package/dist/createTask.d.ts.map +1 -1
- package/dist/createTask.js +7 -1
- package/dist/createView.d.ts.map +1 -1
- package/dist/createView.js +4 -0
- package/package.json +1 -1
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
81
|
-
|
|
13
|
+
```bash
|
|
14
|
+
npm install rask-ui
|
|
82
15
|
```
|
|
83
16
|
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
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.
|