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