@nativele/ts-dom 0.5.1 → 0.5.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/CHANGELOG.md +33 -0
- package/README.md +6 -1
- package/README.zh-TW.md +6 -1
- package/dist/cli.mjs +1 -1
- package/dist/index.mjs +3 -3
- package/dist/{lint-DfUAh7t3.mjs → lint-Bwytj7L8.mjs} +1 -1
- package/dist/oxlint-plugin.mjs +1 -1
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/skills/README.md +7 -4
- package/skills/nativele-integration/SKILL.md +92 -22
- package/skills/nativele-state-management/SKILL.md +319 -0
- package/skills/nativele-tailwind/SKILL.md +220 -0
- package/skills/nativele-tanstack-query/SKILL.md +302 -0
- package/skills/nativele-testing/SKILL.md +364 -0
- package/skills/nativele-zod/SKILL.md +345 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nativele-tanstack-query
|
|
3
|
+
description: "Fetch server data with TanStack Query (@tanstack/query-core) and show it in Nativele components: an adapter from QueryObserver and MutationObserver to bind, loading, error and refetching states with data-if, lists with data-each, query keys from props, unsubscribing on removal, and mutations with invalidation. Use when a project depends on @tanstack/query-core and @nativele/ts-dom."
|
|
4
|
+
metadata:
|
|
5
|
+
tier: experimental
|
|
6
|
+
nativele: ">=0.5.2 <0.6.0"
|
|
7
|
+
verified: "2026-11-05"
|
|
8
|
+
tools: "@tanstack/query-core@5.106.1 vite@8.3.6 typescript@7.0.2 create-vite@9.3.4"
|
|
9
|
+
author: "Nativele maintainers"
|
|
10
|
+
detects: "@tanstack/query-core"
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Nativele + TanStack Query
|
|
14
|
+
|
|
15
|
+
## When to use
|
|
16
|
+
|
|
17
|
+
- An app that already uses Nativele (`@nativele/ts-dom`) and fetches server data with TanStack Query's framework-agnostic core, `@tanstack/query-core`: caching, deduplication, refetching and mutations, shown in Nativele components.
|
|
18
|
+
- Signals: `@tanstack/query-core` and `@nativele/ts-dom` in `package.json`, components that call `fetch` in `connected()` and keep their own loading flags.
|
|
19
|
+
- Not for the framework packages (`@tanstack/react-query`, `@tanstack/vue-query`, ...): their hooks belong to their framework; this Skill was verified only with `query-core`.
|
|
20
|
+
- Not a guide to TanStack Query itself (query keys, `staleTime`, cache design): use its own documentation. Not for writing the components themselves: use `nativele-core` (`npx ts-dom skills add nativele-core`).
|
|
21
|
+
|
|
22
|
+
`bind` accepts any object with `get()` and `subscribe(fn)` that returns an unsubscribe function. `QueryObserver` and `MutationObserver` have `getCurrentResult()` and `subscribe(fn)`, so a three-line adapter connects them; Nativele needs nothing else.
|
|
23
|
+
|
|
24
|
+
## Versions
|
|
25
|
+
|
|
26
|
+
| Tool | Verified version |
|
|
27
|
+
|---|---|
|
|
28
|
+
| @nativele/ts-dom | 0.5.2 |
|
|
29
|
+
| @tanstack/query-core | 5.106.1 |
|
|
30
|
+
| vite | 8.3.6 |
|
|
31
|
+
| typescript | 7.0.2 |
|
|
32
|
+
| create-vite (`vanilla-ts` template, the starting point) | 9.3.4 |
|
|
33
|
+
|
|
34
|
+
Verified on 2026-11-05 in Google Chrome 148 against the Vite dev server, in an app made with `npm create vite@latest -- --template vanilla-ts` and set up with the `nativele-vite` Skill, with a local mock API and Playwright 1.64 holding, failing and counting requests. With a different major version of TanStack Query, treat every step as unverified.
|
|
35
|
+
|
|
36
|
+
## Set-up
|
|
37
|
+
|
|
38
|
+
Starting from a Vite app with Nativele already set up (`npx ts-dom skills nativele-vite`: `strict`, the `tsDom({ dirs: ["src/nv"] })` plugin, `"build": "ts-dom check src/nv && vite build"`):
|
|
39
|
+
|
|
40
|
+
1. `npm i @tanstack/query-core`.
|
|
41
|
+
2. Create one `QueryClient` for the whole app in its own module, call `mount()` once, and add the adapter next to it:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
// src/query.ts
|
|
45
|
+
import { QueryClient } from "@tanstack/query-core";
|
|
46
|
+
import type { ReadableStore } from "@nativele/ts-dom";
|
|
47
|
+
|
|
48
|
+
export const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
|
49
|
+
queryClient.mount();
|
|
50
|
+
|
|
51
|
+
export function observe<R>(observer: { getCurrentResult(): R; subscribe(fn: (result: R) => void): () => void }): ReadableStore<R> {
|
|
52
|
+
return { get: () => observer.getCurrentResult(), subscribe: (fn) => observer.subscribe(fn) };
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The app owns the client; components import it and never create their own. One client is one cache, so two components that ask for the same key share one request. Without `mount()`, the client does not refetch when the window regains focus or the network comes back (limitation 5). `retry: false` is a choice for this example, not a requirement (limitation 6).
|
|
57
|
+
|
|
58
|
+
3. In each component that shows a query, create the observer as a private field, `bind` it in `connected()`, and copy what the template shows into `state` (Example). `bind` calls the callback once with the current result, then on every change, and unsubscribes when the element is removed.
|
|
59
|
+
4. When the query depends on a prop, build the options from the prop in a function and call `setOptions` in `render` when the prop changed (the `user-detail` component in Example).
|
|
60
|
+
5. For a write, create a `MutationObserver` as a field the same way, call `mutate` from a `data-on-*` handler, invalidate the affected keys in `onSuccess`, and `bind` the mutation observer for its pending and error state.
|
|
61
|
+
|
|
62
|
+
## Example
|
|
63
|
+
|
|
64
|
+
Request helpers (your API; the paths are the app's own):
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
// src/api.ts
|
|
68
|
+
export interface User {
|
|
69
|
+
id: number;
|
|
70
|
+
name: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function getJson<T>(url: string, signal?: AbortSignal): Promise<T> {
|
|
74
|
+
const res = await fetch(url, { signal });
|
|
75
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
76
|
+
return (await res.json()) as T;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function postJson<T>(url: string, body: unknown): Promise<T> {
|
|
80
|
+
const res = await fetch(url, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
|
|
81
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
82
|
+
return (await res.json()) as T;
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
A list with a fixed key, and a mutation that invalidates it:
|
|
87
|
+
|
|
88
|
+
```html
|
|
89
|
+
<!-- src/nv/user-list.html -->
|
|
90
|
+
<p data-if="loading">Loading users...</p>
|
|
91
|
+
<p role="alert" data-if="failed">Could not load: {{ error }}</p>
|
|
92
|
+
<p data-if="refetching">Refreshing...</p>
|
|
93
|
+
<ul>
|
|
94
|
+
<template data-each="users">
|
|
95
|
+
<li>{{ name }}</li>
|
|
96
|
+
</template>
|
|
97
|
+
</ul>
|
|
98
|
+
<input data-ref="newName" aria-label="New user" />
|
|
99
|
+
<button type="button" data-on-click="add">Add</button>
|
|
100
|
+
<span data-if="saving">Saving...</span>
|
|
101
|
+
<p role="alert" data-if="saveFailed">Could not save: {{ saveError }}</p>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
// src/nv/user-list.ts
|
|
106
|
+
import { MutationObserver, QueryObserver } from "@tanstack/query-core";
|
|
107
|
+
import { Component, define } from "@nativele/ts-dom";
|
|
108
|
+
import { getJson, postJson, type User } from "../api.ts";
|
|
109
|
+
import { observe, queryClient } from "../query.ts";
|
|
110
|
+
import tpl from "./user-list.html.js";
|
|
111
|
+
|
|
112
|
+
export class UserList extends Component(tpl, {
|
|
113
|
+
state: { status: "pending", fetching: false, error: "", users: [] as readonly User[], saving: false, saveError: "" },
|
|
114
|
+
derive: {
|
|
115
|
+
loading: (s) => s.status === "pending",
|
|
116
|
+
failed: (s) => s.status === "error",
|
|
117
|
+
refetching: (s) => s.status === "success" && s.fetching,
|
|
118
|
+
saveFailed: (s) => s.saveError !== "",
|
|
119
|
+
},
|
|
120
|
+
}) {
|
|
121
|
+
#users = new QueryObserver(queryClient, {
|
|
122
|
+
queryKey: ["users"],
|
|
123
|
+
queryFn: ({ signal }) => getJson<User[]>("/api/users", signal),
|
|
124
|
+
});
|
|
125
|
+
#addUser = new MutationObserver(queryClient, {
|
|
126
|
+
mutationFn: (name: string) => postJson<User>("/api/users", { name }),
|
|
127
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["users"] }),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
override connected(): void {
|
|
131
|
+
this.bind(observe(this.#users), (r) =>
|
|
132
|
+
this.set({ status: r.status, fetching: r.isFetching, error: r.error?.message ?? "", users: r.data ?? [] }),
|
|
133
|
+
);
|
|
134
|
+
this.bind(observe(this.#addUser), (m) => this.set({ saving: m.isPending, saveError: m.error?.message ?? "" }));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
add(): void {
|
|
138
|
+
const name = this.refs.newName.value.trim();
|
|
139
|
+
if (!name) return;
|
|
140
|
+
this.#addUser.mutate(name).then(() => (this.refs.newName.value = ""), () => {});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
override render(view: { users: readonly User[] }, changed: ReadonlySet<"users">): void {
|
|
144
|
+
if (changed.has("users")) this.lists.users.replace(view.users, (u) => u.id, (u, { text }) => (text.name = u.name));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
define("user-list", UserList);
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
A detail whose query key comes from a prop:
|
|
151
|
+
|
|
152
|
+
```html
|
|
153
|
+
<!-- src/nv/user-detail.html -->
|
|
154
|
+
<p data-if="loading">Loading user {{ userId }}...</p>
|
|
155
|
+
<p role="alert" data-if="failed">Could not load user {{ userId }}: {{ error }}</p>
|
|
156
|
+
<h2 data-if="ready">{{ name }}</h2>
|
|
157
|
+
<small data-if="refetching">Refreshing...</small>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
// src/nv/user-detail.ts
|
|
162
|
+
import { QueryObserver } from "@tanstack/query-core";
|
|
163
|
+
import { Component, define } from "@nativele/ts-dom";
|
|
164
|
+
import { getJson, type User } from "../api.ts";
|
|
165
|
+
import { observe, queryClient } from "../query.ts";
|
|
166
|
+
import tpl from "./user-detail.html.js";
|
|
167
|
+
|
|
168
|
+
const userQuery = (id: number) => ({
|
|
169
|
+
queryKey: ["user", id],
|
|
170
|
+
queryFn: ({ signal }: { signal: AbortSignal }) => getJson<User>(`/api/users/${id}`, signal),
|
|
171
|
+
enabled: id > 0,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
export class UserDetail extends Component(tpl, {
|
|
175
|
+
props: { userId: 0 },
|
|
176
|
+
state: { status: "pending", fetching: false, error: "", name: "" },
|
|
177
|
+
derive: {
|
|
178
|
+
loading: (s) => s.status === "pending",
|
|
179
|
+
failed: (s) => s.status === "error",
|
|
180
|
+
ready: (s) => s.status === "success",
|
|
181
|
+
refetching: (s) => s.status === "success" && s.fetching,
|
|
182
|
+
},
|
|
183
|
+
}) {
|
|
184
|
+
#user = new QueryObserver(queryClient, userQuery(0));
|
|
185
|
+
|
|
186
|
+
override connected(): void {
|
|
187
|
+
this.bind(observe(this.#user), (r) =>
|
|
188
|
+
this.set({ status: r.status, fetching: r.isFetching, error: r.error?.message ?? "", name: r.data?.name ?? "" }),
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
override render(view: { userId: number }, changed: ReadonlySet<"userId">): void {
|
|
193
|
+
if (changed.has("userId")) this.#user.setOptions(userQuery(view.userId));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
define("user-detail", UserDetail);
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The parent passes the prop, can remove the detail, and refreshes everything:
|
|
200
|
+
|
|
201
|
+
```html
|
|
202
|
+
<!-- src/nv/app-shell.html -->
|
|
203
|
+
<button type="button" data-on-click="refreshAll">Refresh all</button>
|
|
204
|
+
<user-list></user-list>
|
|
205
|
+
<select data-ref="pick" data-on-change="pickUser" aria-label="User">
|
|
206
|
+
<option value="1">1</option>
|
|
207
|
+
<option value="2">2</option>
|
|
208
|
+
</select>
|
|
209
|
+
<button type="button" data-on-click="toggle">Toggle detail</button>
|
|
210
|
+
<div data-if="showDetail">
|
|
211
|
+
<user-detail data-ref="detail"></user-detail>
|
|
212
|
+
</div>
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
// src/nv/app-shell.ts
|
|
217
|
+
import { Component, define } from "@nativele/ts-dom";
|
|
218
|
+
import { queryClient } from "../query.ts";
|
|
219
|
+
import "./user-list.ts";
|
|
220
|
+
import "./user-detail.ts";
|
|
221
|
+
import tpl from "./app-shell.html.js";
|
|
222
|
+
|
|
223
|
+
export class AppShell extends Component(tpl, { state: { userId: 1, showDetail: true } }) {
|
|
224
|
+
pickUser(): void {
|
|
225
|
+
this.set({ userId: Number(this.refs.pick.value) });
|
|
226
|
+
}
|
|
227
|
+
toggle(): void {
|
|
228
|
+
this.set((s) => ({ showDetail: !s.showDetail }));
|
|
229
|
+
}
|
|
230
|
+
refreshAll(): void {
|
|
231
|
+
void queryClient.invalidateQueries();
|
|
232
|
+
}
|
|
233
|
+
override render(view: { userId: number }, changed: ReadonlySet<"userId">): void {
|
|
234
|
+
if (changed.has("userId")) this.refs.detail.userId = view.userId;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
define("app-shell", AppShell);
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
`src/main.ts` imports `./nv/app-shell.ts`, and `index.html` has `<app-shell></app-shell>`. What the example does, as verified:
|
|
241
|
+
|
|
242
|
+
- On load, each component shows its loading block, then the data; the list is filled with `replace()`. One request per query.
|
|
243
|
+
- A refetch (invalidation, focus, a remount of stale data) keeps the data on screen and shows the `refetching` block until it ends.
|
|
244
|
+
- Changing `userId` switches the query: a key never loaded shows the loading block, not the previous user's data; a key already in the cache shows its data at once and refetches it.
|
|
245
|
+
- A failed request shows the error block (`Could not load user 99: HTTP 404`).
|
|
246
|
+
- Clicking Add shows `Saving...`, then the list is refetched once and shows the new user; a failed save shows its error, keeps the input and refetches nothing.
|
|
247
|
+
|
|
248
|
+
## Known limitations
|
|
249
|
+
|
|
250
|
+
1. **One observer per component instance; one client per app.** Create the observer as a field and `bind` it in `connected()`, not in the constructor: `bind` releases the subscription on removal and subscribes again when the element comes back (see the lists guide on connecting and subscriptions). A module-level observer shared by several components also works (each `bind` adds a listener; when the last one is removed the observer stops, and it starts again when one comes back), but only when its options do not depend on any one component: `setOptions` from one component would switch the query for all of them. Two components with their own observers on the same key share one request through the client's cache.
|
|
251
|
+
2. **Removing the element stops everything for it.** After the element leaves the page, invalidation and window focus do not refetch its query, and a request that finishes later does not reach the element. Pass `signal` from the query function's context to `fetch`: then removing the last component that observes a query while it is fetching aborts the request. Without `signal`, the request runs to the end and fills the cache, but the removed element still gets no update. Putting the element back (the same element, as `data-if` does) shows the cached data at once and refetches it (with the default `staleTime` of 0, cached data is always stale).
|
|
252
|
+
3. **Keys from props go through `setOptions` in `render`.** Guard it with `changed.has("userId")`. A prop change while the element is removed sends no request. When a parent shows the element again with `data-if` and gives it a new prop in the same update, the element connects first, with its old prop, and fetches that key before `render` switches to the new one; so when hiding it, also reset the prop to a value whose query is disabled (`productId: 0` with `enabled: id > 0`), and only the new key is fetched. Use `enabled` for values that must not be fetched (here `id > 0`); a disabled query stays `status: "pending"`, so the loading block shows until a real id arrives. Show it differently if that matters (for example derive `loading` from `userId > 0` as well).
|
|
253
|
+
4. **Map `status` and `isFetching`, not `isLoading`.** Before the first subscription the result is `status: "pending"` with `fetchStatus: "idle"` (`isLoading` is `false`), and `bind` calls the callback with that result first; subscribing starts the fetch and calls it again at once with `fetchStatus: "fetching"`. Deriving `loading` from `status === "pending"` shows the loading block from the first render on. Copy only what the template shows into state.
|
|
254
|
+
5. **Call `queryClient.mount()` once.** Without it, a `visibilitychange` on the window (focus) does not refetch active stale queries; with it, it does.
|
|
255
|
+
6. **Errors wait for retries by default.** With the default `retry`, a failing query showed its error after about 7 seconds (`failureCount` 4: the first attempt and three retries), and the loading block stays until then. Set `retry` in the client's `defaultOptions` or per query to what the app needs; tests that check error states need it low or off.
|
|
256
|
+
7. **Handle the promise of `mutate`.** `mutate()` returns a promise that rejects when the mutation fails; without a rejection handler the page reports an uncaught `Error: HTTP 500`. The `MutationObserver` result (bound above) already carries the error for the view, so the handler can be empty.
|
|
257
|
+
8. **Arrays from `data` are read-only in state.** Declare list state as `readonly T[]` (`[] as readonly User[]`) and pass `r.data ?? []` as it is; the view and `render` receive it read-only (see the state guide).
|
|
258
|
+
9. Not verified: infinite queries, `placeholderData` and keeping previous data while switching keys, `refetchInterval`, persistence, hydration and server rendering, offline and paused mutations, optimistic updates, the TanStack devtools, the framework packages, and versions of `@tanstack/query-core` other than 5.106.
|
|
259
|
+
|
|
260
|
+
## Verify
|
|
261
|
+
|
|
262
|
+
Run in the project:
|
|
263
|
+
|
|
264
|
+
```sh
|
|
265
|
+
npx ts-dom check src/nv # expect: ts-dom check: no issues
|
|
266
|
+
npx ts-dom doctor src/nv # expect: ts-dom doctor: the set-up looks right (info lines are suggestions)
|
|
267
|
+
npm run build # expect: ts-dom check: no issues, then ✓ built
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Then check in a browser against the dev server, without external network: point the queries at a local mock API (for example a Vite plugin with `configureServer` middleware, or the app's own backend in development), and make the states deterministic by holding requests in the test instead of adding delays. With Playwright, `page.route("**/api/**", ...)` can record every request, keep a matching one waiting until the test releases it with `route.continue()`, or answer it with `route.fulfill({ status: 500 })`. Check:
|
|
271
|
+
|
|
272
|
+
- while a request is held, the loading block shows; after release, the data and the list items show, with one request per query;
|
|
273
|
+
- with data on screen, invalidating holds a refetch: the `refetching` block shows and the data stays;
|
|
274
|
+
- changing the prop sends a request for the new key and shows its loading block, not the previous data;
|
|
275
|
+
- after the component is removed, invalidation and a `visibilitychange` event on `window` send no request for its query, and a held request released after removal does not change the removed element (keep a reference to it); with `signal` passed to `fetch`, a request held when the component is removed fails with `requestfailed`, as long as no other component still observes the same key (another observer keeps the query, and its request, alive);
|
|
276
|
+
- after putting the element back, it shows the cached data and its query refetches;
|
|
277
|
+
- a held mutation shows its pending block; after release the list is refetched once and shows the new item; a failed mutation shows its error;
|
|
278
|
+
- no console errors or warnings, apart from the browser's `Failed to load resource` line for a status you return on purpose.
|
|
279
|
+
|
|
280
|
+
## Troubleshooting
|
|
281
|
+
|
|
282
|
+
| Symptom | Cause | Fix |
|
|
283
|
+
|---|---|---|
|
|
284
|
+
| Queries never refetch when the window regains focus | `queryClient.mount()` was not called | call it once where the client is created |
|
|
285
|
+
| The error block shows only after several seconds | the default `retry` (three retries with growing delays) | set `retry` in `defaultOptions` or per query |
|
|
286
|
+
| Console: uncaught `Error: HTTP 500` after a failed mutation | the promise from `mutate()` has no rejection handler | add one: `.then(onDone, () => {})` |
|
|
287
|
+
| A component with no id shows its loading block forever and sends no request | `enabled` is false for that value, so the query stays `pending` | set the prop, or derive the view from the prop as well |
|
|
288
|
+
| The prop is set but the loading block stays and no request is sent | `setOptions` is not called in `render`, so the observer keeps its initial (disabled) options | call `this.#user.setOptions(userQuery(view.userId))` when `changed.has("userId")` |
|
|
289
|
+
| After the element is removed and put back, it never refetches or updates | the observer was subscribed in a field or the constructor and unsubscribed in `disconnected()`, so nothing subscribes again | subscribe with `this.bind(observe(observer), ...)` in `connected()` |
|
|
290
|
+
| `ts-dom check`: `[missing-binding] "isFetching" is used by {{ isFetching }} ...`, followed by `TS7006: Parameter 'r' implicitly has an 'any' type` in the `bind` callback | the template names a result field that is not in `state` or `derive`; the second error follows from the first | copy the field into state in the `bind` callback, or derive it |
|
|
291
|
+
|
|
292
|
+
## See also
|
|
293
|
+
|
|
294
|
+
- `bind`, stores and read-only sources: https://github.com/Pixmi/nativele/blob/main/docs/components.md#state-store-when-not-using-state
|
|
295
|
+
- `state`, `derive`, `render` and `changed`: https://github.com/Pixmi/nativele/blob/main/docs/components.md#data-and-view-state-optional
|
|
296
|
+
- Props from the parent: https://github.com/Pixmi/nativele/blob/main/docs/components.md#data-flow-props-down-events-up
|
|
297
|
+
- Loading and error states without a library: https://github.com/Pixmi/nativele/blob/main/docs/recipes.md#loading-and-error-states
|
|
298
|
+
- Copying store data into state: https://github.com/Pixmi/nativele/blob/main/docs/recipes.md#showing-data-from-a-store
|
|
299
|
+
- Lists with `replace()`: https://github.com/Pixmi/nativele/blob/main/docs/lists.md#items-as-plain-elements-replace
|
|
300
|
+
- Lists of item components (`items = ...`): https://github.com/Pixmi/nativele/blob/main/docs/lists.md#items-as-components-items--
|
|
301
|
+
- Why subscriptions belong in `connected()`: https://github.com/Pixmi/nativele/blob/main/docs/lists.md#moving-items-connecting-and-subscriptions
|
|
302
|
+
- Setting up Vite: `npx ts-dom skills nativele-vite`
|