fetchium 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -5
- package/dist/cjs/development/QueryClient-CLi3ONNM.js +2 -0
- package/dist/cjs/development/QueryClient-CLi3ONNM.js.map +1 -0
- package/dist/cjs/development/QueryController-BQA49OYU.js +2 -0
- package/dist/cjs/development/QueryController-BQA49OYU.js.map +1 -0
- package/dist/cjs/development/index.js +1 -1
- package/dist/cjs/development/index.js.map +1 -1
- package/dist/cjs/development/mutation-CikIl_6k.js +2 -0
- package/dist/cjs/development/mutation-CikIl_6k.js.map +1 -0
- package/dist/cjs/development/react/index.js +1 -1
- package/dist/cjs/development/rest/index.js +2 -0
- package/dist/cjs/development/rest/index.js.map +1 -0
- package/dist/cjs/development/topic/index.js +2 -0
- package/dist/cjs/development/topic/index.js.map +1 -0
- package/dist/cjs/production/QueryClient-N0MJmuHW.js +2 -0
- package/dist/cjs/production/QueryClient-N0MJmuHW.js.map +1 -0
- package/dist/cjs/production/QueryController-BQA49OYU.js +2 -0
- package/dist/cjs/production/QueryController-BQA49OYU.js.map +1 -0
- package/dist/cjs/production/index.js +1 -1
- package/dist/cjs/production/index.js.map +1 -1
- package/dist/cjs/production/mutation-P_Yb4LI9.js +2 -0
- package/dist/cjs/production/mutation-P_Yb4LI9.js.map +1 -0
- package/dist/cjs/production/react/index.js +1 -1
- package/dist/cjs/production/rest/index.js +2 -0
- package/dist/cjs/production/rest/index.js.map +1 -0
- package/dist/cjs/production/topic/index.js +2 -0
- package/dist/cjs/production/topic/index.js.map +1 -0
- package/dist/esm/MutationResult.d.ts +0 -1
- package/dist/esm/MutationResult.d.ts.map +1 -1
- package/dist/esm/QueryClient.d.ts +26 -4
- package/dist/esm/QueryClient.d.ts.map +1 -1
- package/dist/esm/QueryController.d.ts +49 -0
- package/dist/esm/QueryController.d.ts.map +1 -0
- package/dist/esm/QueryResult.d.ts +10 -10
- package/dist/esm/QueryResult.d.ts.map +1 -1
- package/dist/esm/development/QueryClient-Dtde3pss.js +2572 -0
- package/dist/esm/development/QueryClient-Dtde3pss.js.map +1 -0
- package/dist/esm/development/QueryController-Ch_ncxiI.js +14 -0
- package/dist/esm/development/QueryController-Ch_ncxiI.js.map +1 -0
- package/dist/esm/development/index.js +29 -100
- package/dist/esm/development/index.js.map +1 -1
- package/dist/esm/development/mutation-UZshUQAf.js +58 -0
- package/dist/esm/development/mutation-UZshUQAf.js.map +1 -0
- package/dist/esm/development/react/index.js +1 -1
- package/dist/esm/development/rest/index.js +142 -0
- package/dist/esm/development/rest/index.js.map +1 -0
- package/dist/esm/development/{shared-Dq2yW78d.js → shared-DcuVH8Pf.js} +5 -5
- package/dist/esm/development/{shared-Dq2yW78d.js.map → shared-DcuVH8Pf.js.map} +1 -1
- package/dist/esm/development/stores/async.js +6 -6
- package/dist/esm/development/stores/sync.js +5 -5
- package/dist/esm/development/topic/index.js +86 -0
- package/dist/esm/development/topic/index.js.map +1 -0
- package/dist/esm/index.d.ts +5 -4
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/mutation.d.ts +6 -19
- package/dist/esm/mutation.d.ts.map +1 -1
- package/dist/esm/production/{QueryClient-BP0Z1rQV.js → QueryClient-YqnBxFy1.js} +972 -968
- package/dist/esm/production/QueryClient-YqnBxFy1.js.map +1 -0
- package/dist/esm/production/QueryController-Ch_ncxiI.js +14 -0
- package/dist/esm/production/QueryController-Ch_ncxiI.js.map +1 -0
- package/dist/esm/production/index.js +29 -100
- package/dist/esm/production/index.js.map +1 -1
- package/dist/esm/production/mutation-pgFl1uIY.js +58 -0
- package/dist/esm/production/mutation-pgFl1uIY.js.map +1 -0
- package/dist/esm/production/react/index.js +1 -1
- package/dist/esm/production/rest/index.js +142 -0
- package/dist/esm/production/rest/index.js.map +1 -0
- package/dist/esm/production/{shared-Dq2yW78d.js → shared-DcuVH8Pf.js} +5 -5
- package/dist/esm/production/{shared-Dq2yW78d.js.map → shared-DcuVH8Pf.js.map} +1 -1
- package/dist/esm/production/stores/async.js +6 -6
- package/dist/esm/production/stores/sync.js +5 -5
- package/dist/esm/production/topic/index.js +86 -0
- package/dist/esm/production/topic/index.js.map +1 -0
- package/dist/esm/query-types.d.ts +2 -4
- package/dist/esm/query-types.d.ts.map +1 -1
- package/dist/esm/query.d.ts +17 -39
- package/dist/esm/query.d.ts.map +1 -1
- package/dist/esm/rest/RESTMutation.d.ts +18 -0
- package/dist/esm/rest/RESTMutation.d.ts.map +1 -0
- package/dist/esm/rest/RESTQuery.d.ts +24 -0
- package/dist/esm/rest/RESTQuery.d.ts.map +1 -0
- package/dist/esm/rest/RESTQueryController.d.ts +34 -0
- package/dist/esm/rest/RESTQueryController.d.ts.map +1 -0
- package/dist/esm/rest/index.d.ts +5 -0
- package/dist/esm/rest/index.d.ts.map +1 -0
- package/dist/esm/stores/shared.d.ts.map +1 -1
- package/dist/esm/testing/MockClient.d.ts +64 -0
- package/dist/esm/testing/MockClient.d.ts.map +1 -0
- package/dist/esm/testing/auto-generate.d.ts +20 -0
- package/dist/esm/testing/auto-generate.d.ts.map +1 -0
- package/dist/esm/testing/entity-factory.d.ts +13 -0
- package/dist/esm/testing/entity-factory.d.ts.map +1 -0
- package/dist/esm/testing/index.d.ts +6 -0
- package/dist/esm/testing/index.d.ts.map +1 -0
- package/dist/esm/testing/types.d.ts +37 -0
- package/dist/esm/testing/types.d.ts.map +1 -0
- package/dist/esm/topic/TopicQuery.d.ts +10 -0
- package/dist/esm/topic/TopicQuery.d.ts.map +1 -0
- package/dist/esm/topic/TopicQueryController.d.ts +43 -0
- package/dist/esm/topic/TopicQueryController.d.ts.map +1 -0
- package/dist/esm/topic/index.d.ts +3 -0
- package/dist/esm/topic/index.d.ts.map +1 -0
- package/dist/esm/typeDefs.d.ts +1 -1
- package/dist/esm/types.d.ts +9 -4
- package/dist/esm/types.d.ts.map +1 -1
- package/package.json +51 -4
- package/plugin/.claude-plugin/plugin.json +10 -0
- package/plugin/agents/fetchium.md +168 -0
- package/plugin/docs/api/fetchium-react.md +135 -0
- package/plugin/docs/api/fetchium.md +674 -0
- package/plugin/docs/api/stores-async.md +219 -0
- package/plugin/docs/api/stores-sync.md +133 -0
- package/plugin/docs/core/entities.md +351 -0
- package/plugin/docs/core/queries.md +600 -0
- package/plugin/docs/core/streaming.md +550 -0
- package/plugin/docs/core/types.md +374 -0
- package/plugin/docs/data/caching.md +298 -0
- package/plugin/docs/data/live-data.md +435 -0
- package/plugin/docs/data/mutations.md +465 -0
- package/plugin/docs/guides/auth.md +318 -0
- package/plugin/docs/guides/error-handling.md +351 -0
- package/plugin/docs/guides/offline.md +270 -0
- package/plugin/docs/guides/testing.md +301 -0
- package/plugin/docs/quickstart.md +170 -0
- package/plugin/docs/reference/pagination.md +519 -0
- package/plugin/docs/reference/rest-queries.md +107 -0
- package/plugin/docs/reference/why-signalium.md +364 -0
- package/plugin/docs/setup/project-setup.md +319 -0
- package/plugin/install.mjs +88 -0
- package/plugin/skills/design/SKILL.md +140 -0
- package/plugin/skills/teach/SKILL.md +105 -0
- package/dist/cjs/development/QueryClient-CpmwggOn.js +0 -2
- package/dist/cjs/development/QueryClient-CpmwggOn.js.map +0 -1
- package/dist/cjs/production/QueryClient-qi3bR0eD.js +0 -2
- package/dist/cjs/production/QueryClient-qi3bR0eD.js.map +0 -1
- package/dist/esm/development/QueryClient-DRZtPKFD.js +0 -2568
- package/dist/esm/development/QueryClient-DRZtPKFD.js.map +0 -1
- package/dist/esm/production/QueryClient-BP0Z1rQV.js.map +0 -1
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: REST Queries Reference
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
This page covers the advanced configuration options for `RESTQuery`. For the fundamentals --- params, result, defining queries, and the query class rules --- see the [Queries](/core/queries) guide.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Override Methods
|
|
10
|
+
|
|
11
|
+
Every static field on `RESTQuery` has a corresponding `get*()` method for dynamic logic. When both a static field and a `get*` method are defined, the method takes priority.
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
class GetUserPosts extends RESTQuery {
|
|
15
|
+
params = {
|
|
16
|
+
userId: t.number,
|
|
17
|
+
status: t.optional(t.string),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
path = `/users/${this.params.userId}/posts`;
|
|
21
|
+
|
|
22
|
+
result = {
|
|
23
|
+
posts: t.array(t.object({ title: t.string, body: t.string })),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
getSearchParams() {
|
|
27
|
+
const status = this.params.status;
|
|
28
|
+
return status ? { status } : undefined;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Method reference
|
|
34
|
+
|
|
35
|
+
| Method | Returns | Description |
|
|
36
|
+
| --------------------- | ----------------------------- | ---------------------------------- |
|
|
37
|
+
| `getPath()` | `string \| undefined` | Dynamic path override |
|
|
38
|
+
| `getMethod()` | `string` | Dynamic HTTP method |
|
|
39
|
+
| `getSearchParams()` | `Record \| undefined` | Dynamic search params |
|
|
40
|
+
| `getBody()` | `Record \| undefined` | Dynamic request body |
|
|
41
|
+
| `getHeaders()` | `HeadersInit \| undefined` | Dynamic request headers |
|
|
42
|
+
| `getRequestOptions()` | `RequestOptions \| undefined` | Dynamic fetch options |
|
|
43
|
+
| `getConfig()` | `ConfigOptions \| undefined` | Dynamic cache/retry/network config |
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Identity Keys
|
|
48
|
+
|
|
49
|
+
Each query instance is identified by a identity key, which determines its cache identity. Two query instances with the same identity key share the same cache entry and are deduplicated --- only one network request is made at a time.
|
|
50
|
+
|
|
51
|
+
By default, `RESTQuery` computes the key as:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
${method}:${interpolatedPath}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
For example, `GetUser` with `{ id: 42 }` produces the key `GET:/users/42`.
|
|
58
|
+
|
|
59
|
+
### Custom identity keys
|
|
60
|
+
|
|
61
|
+
Override `getIdentityKey()` when the default key doesn't capture all the inputs that make a query unique:
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
class SearchUsers extends RESTQuery {
|
|
65
|
+
params = {
|
|
66
|
+
query: t.string,
|
|
67
|
+
filters: t.optional(t.object({ role: t.string })),
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
path = '/users/search';
|
|
71
|
+
|
|
72
|
+
result = {
|
|
73
|
+
users: t.array(t.object({ name: t.string, email: t.string })),
|
|
74
|
+
total: t.number,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
getIdentityKey() {
|
|
78
|
+
return `search:${this.params.query}:${this.params.filters?.role ?? 'all'}`;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
This is useful when search params or body fields affect the response but don't appear in the path.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Dynamic Config with getConfig()
|
|
88
|
+
|
|
89
|
+
For runtime-dependent configuration, override `getConfig()`. This is useful when caching, network behavior, or retry logic should vary based on the query's params or other runtime state:
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
class GetDashboard extends RESTQuery {
|
|
93
|
+
path = '/dashboard';
|
|
94
|
+
|
|
95
|
+
result = { stats: t.object({ visits: t.number }) };
|
|
96
|
+
|
|
97
|
+
getConfig() {
|
|
98
|
+
return {
|
|
99
|
+
staleTime: 60_000,
|
|
100
|
+
gcTime: 30,
|
|
101
|
+
networkMode: NetworkMode.OfflineFirst,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
When both a static `config` field and `getConfig()` are defined, the method takes priority. See the [Caching & Refetching](/data/caching) guide for details on `staleTime` and `gcTime`, the [Offline & Persistence](/guides/offline) guide for network modes, and the [Error Handling](/guides/error-handling) guide for retry configuration.
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Why Signalium?
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Fetchium works great with plain React hooks via `useQuery`. You can use it in any function component without learning anything about signals or reactive programming. But Fetchium is built on [Signalium](https://signaliumjs.dev), and opting into Signalium's reactive model unlocks additional capabilities --- automatic memoization, reactive composition, fine-grained reactivity, and natural async/await support.
|
|
6
|
+
|
|
7
|
+
This page explains what you gain by using Signalium's reactive primitives alongside Fetchium, and when it makes sense to reach for them.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Two Modes
|
|
12
|
+
|
|
13
|
+
Fetchium supports two approaches to data fetching. You can use either one, or mix them in the same application.
|
|
14
|
+
|
|
15
|
+
### React hooks mode
|
|
16
|
+
|
|
17
|
+
Use `useQuery` inside regular function components. This is the simplest approach and works with existing React patterns, state management, and component libraries.
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
import { useQuery } from 'fetchium/react';
|
|
21
|
+
|
|
22
|
+
function UserProfile({ userId }: { userId: number }) {
|
|
23
|
+
const result = useQuery(GetUser, { id: userId });
|
|
24
|
+
|
|
25
|
+
if (!result.isReady) return <div>Loading...</div>;
|
|
26
|
+
if (result.isRejected) return <div>Error: {result.error.message}</div>;
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div>
|
|
30
|
+
<h1>{result.value.name}</h1>
|
|
31
|
+
<p>{result.value.email}</p>
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
This mode gives you automatic caching, deduplication, entity normalization, and live data --- all the core Fetchium features. For many applications, this is all you need.
|
|
38
|
+
|
|
39
|
+
### Signalium mode
|
|
40
|
+
|
|
41
|
+
Wrap your components with `component()` from Signalium and use `fetchQuery()` directly. This gives you full reactive composition, automatic memoization, and fine-grained dependency tracking.
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
import { component } from 'signalium/react';
|
|
45
|
+
import { fetchQuery } from 'fetchium';
|
|
46
|
+
|
|
47
|
+
const UserProfile = component(({ userId }: { userId: number }) => {
|
|
48
|
+
const user = fetchQuery(GetUser, { id: userId });
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div>
|
|
52
|
+
<h1>{user.name}</h1>
|
|
53
|
+
<p>{user.email}</p>
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The difference is subtle in simple cases, but becomes significant as your data requirements grow.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## What Signalium Adds
|
|
64
|
+
|
|
65
|
+
### Automatic memoization
|
|
66
|
+
|
|
67
|
+
Signalium's `component()` wrapper automatically memoizes your component. It only re-renders when the specific reactive values it reads actually change --- not on every parent re-render.
|
|
68
|
+
|
|
69
|
+
With standard React, parent re-renders cascade to all children unless you manually wrap components in `React.memo` and memoize props with `useMemo` and `useCallback`. With Signalium, this optimization is automatic:
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
// This component only re-renders when `user.name` or `user.email` changes.
|
|
73
|
+
// Parent re-renders are ignored unless they change the `userId` prop.
|
|
74
|
+
const UserProfile = component(({ userId }: { userId: number }) => {
|
|
75
|
+
const user = fetchQuery(GetUser, { id: userId });
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div>
|
|
79
|
+
<h1>{user.name}</h1>
|
|
80
|
+
<p>{user.email}</p>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
This is especially valuable in list views and deeply nested component trees, where unnecessary re-renders are a common performance problem.
|
|
87
|
+
|
|
88
|
+
### Reactive composition
|
|
89
|
+
|
|
90
|
+
With `fetchQuery()` directly, you can compose queries inside reactive functions that live **outside** of components:
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
import { reactive } from 'signalium';
|
|
94
|
+
import { fetchQuery } from 'fetchium';
|
|
95
|
+
|
|
96
|
+
const getUserWithPosts = reactive((userId: number) => {
|
|
97
|
+
const user = fetchQuery(GetUser, { id: userId });
|
|
98
|
+
const posts = fetchQuery(GetUserPosts, { userId });
|
|
99
|
+
return { user, posts };
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
This reactive function is **cached and shared**. If multiple components call `getUserWithPosts(42)`, they all read from the same cached computation. The queries are deduplicated, and the derived result is computed once.
|
|
104
|
+
|
|
105
|
+
This is powerful for building a **data layer** that sits between your API and your components:
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
// data/user.ts --- shared reactive data layer
|
|
109
|
+
import { reactive } from 'signalium';
|
|
110
|
+
import { fetchQuery } from 'fetchium';
|
|
111
|
+
|
|
112
|
+
export const getFullUser = reactive((userId: number) => {
|
|
113
|
+
const user = fetchQuery(GetUser, { id: userId });
|
|
114
|
+
const posts = fetchQuery(GetUserPosts, { userId });
|
|
115
|
+
const followers = fetchQuery(GetFollowers, { userId });
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
user,
|
|
119
|
+
posts,
|
|
120
|
+
followers,
|
|
121
|
+
postCount: posts.length,
|
|
122
|
+
isPopular: followers.length > 1000,
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// components/UserProfile.tsx
|
|
127
|
+
const UserProfile = component(({ userId }: { userId: number }) => {
|
|
128
|
+
const { user, isPopular } = getFullUser(userId);
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<div>
|
|
132
|
+
<h1>
|
|
133
|
+
{user.name} {isPopular ? '(Popular)' : ''}
|
|
134
|
+
</h1>
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// components/UserPosts.tsx
|
|
140
|
+
const UserPosts = component(({ userId }: { userId: number }) => {
|
|
141
|
+
const { posts, postCount } = getFullUser(userId);
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<div>
|
|
145
|
+
<h2>{postCount} Posts</h2>
|
|
146
|
+
{posts.map((post) => (
|
|
147
|
+
<PostCard key={post.id} post={post} />
|
|
148
|
+
))}
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Both components call `getFullUser(userId)`, but the queries execute only once. The reactive function caches its result and returns the same object to all consumers.
|
|
155
|
+
|
|
156
|
+
### Fine-grained reactivity
|
|
157
|
+
|
|
158
|
+
Fetchium entities are reactive Proxy objects. When you access a property on an entity, Signalium tracks that access as a dependency. Only the specific properties your component reads trigger re-renders.
|
|
159
|
+
|
|
160
|
+
```tsx
|
|
161
|
+
const UserAvatar = component(({ userId }: { userId: number }) => {
|
|
162
|
+
const user = fetchQuery(GetUser, { id: userId });
|
|
163
|
+
|
|
164
|
+
// Only reads `avatarUrl` --- changes to `name`, `email`, etc. do NOT
|
|
165
|
+
// cause this component to re-render
|
|
166
|
+
return <img src={user.avatarUrl} />;
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const UserName = component(({ userId }: { userId: number }) => {
|
|
170
|
+
const user = fetchQuery(GetUser, { id: userId });
|
|
171
|
+
|
|
172
|
+
// Only reads `name` --- changes to `avatarUrl`, `email`, etc. do NOT
|
|
173
|
+
// cause this component to re-render
|
|
174
|
+
return <span>{user.name}</span>;
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Both components fetch the same user entity (deduplicated by the cache), but they re-render independently based on which properties they actually read. If a mutation updates `user.name`, only `UserName` re-renders. `UserAvatar` is unaffected.
|
|
179
|
+
|
|
180
|
+
This property-level tracking works automatically through Signalium's reactive system. There is no need to select specific fields or use selector functions.
|
|
181
|
+
|
|
182
|
+
### Async/await support
|
|
183
|
+
|
|
184
|
+
Signalium's Babel transform rewrites `async` reactive functions to use generators internally, enabling pause/resume semantics. This lets you `await` reactive promises naturally --- including query results:
|
|
185
|
+
|
|
186
|
+
```tsx
|
|
187
|
+
import { reactive } from 'signalium';
|
|
188
|
+
import { fetchQuery } from 'fetchium';
|
|
189
|
+
|
|
190
|
+
const getUserProfile = reactive(async (userId: number) => {
|
|
191
|
+
const user = await fetchQuery(GetUser, { id: userId });
|
|
192
|
+
const posts = await fetchQuery(GetUserPosts, { userId: user.id });
|
|
193
|
+
|
|
194
|
+
// Sequential fetch: posts depend on the user's ID from the first query
|
|
195
|
+
return { user, posts };
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Without the transform, reactive functions are synchronous and return `ReactivePromise` objects. With the transform, you can write natural async/await code that Signalium converts into reactive computations behind the scenes. Dependencies are still tracked automatically, and the result updates when upstream data changes.
|
|
200
|
+
|
|
201
|
+
{% callout %}
|
|
202
|
+
The async transform requires the Signalium Babel preset. See [Babel Transform Setup](#babel-transform-setup) below for configuration.
|
|
203
|
+
{% /callout %}
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## When to Use Which
|
|
208
|
+
|
|
209
|
+
Start with **React hooks mode** (`useQuery`). It is simpler, requires no build configuration, and covers the majority of use cases.
|
|
210
|
+
|
|
211
|
+
Consider **Signalium mode** when you need:
|
|
212
|
+
|
|
213
|
+
- **Cross-component reactive composition** --- multiple components sharing derived data from the same set of queries, without redundant computation or manual memoization.
|
|
214
|
+
- **Derived queries** --- queries whose parameters depend on the results of other queries (e.g., fetching a user's posts after fetching the user).
|
|
215
|
+
- **Fine-grained render optimization** --- large lists or complex UIs where property-level reactivity significantly reduces unnecessary re-renders.
|
|
216
|
+
- **Shared reactive computations** --- business logic that combines multiple data sources into a single reactive value, consumed by many parts of the UI.
|
|
217
|
+
|
|
218
|
+
You do not need to choose one mode for your entire application. It is common to use `useQuery` for simple data fetching in most components and reach for `component()` + `fetchQuery()` in performance-sensitive areas or where reactive composition simplifies the code.
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Incremental Adoption
|
|
223
|
+
|
|
224
|
+
Adopting Signalium is incremental. You do not need to rewrite your application.
|
|
225
|
+
|
|
226
|
+
### Step 1: Wrap components with `component()`
|
|
227
|
+
|
|
228
|
+
The simplest first step is wrapping existing components with `component()`. This gives you automatic memoization with zero other changes:
|
|
229
|
+
|
|
230
|
+
```tsx
|
|
231
|
+
// Before
|
|
232
|
+
function UserList({ users }: { users: User[] }) {
|
|
233
|
+
return (
|
|
234
|
+
<ul>
|
|
235
|
+
{users.map((u) => (
|
|
236
|
+
<li key={u.id}>{u.name}</li>
|
|
237
|
+
))}
|
|
238
|
+
</ul>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// After --- automatic memoization, no other changes needed
|
|
243
|
+
const UserList = component(({ users }: { users: User[] }) => {
|
|
244
|
+
return (
|
|
245
|
+
<ul>
|
|
246
|
+
{users.map((u) => (
|
|
247
|
+
<li key={u.id}>{u.name}</li>
|
|
248
|
+
))}
|
|
249
|
+
</ul>
|
|
250
|
+
);
|
|
251
|
+
});
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Step 2: Extract shared reactive functions
|
|
255
|
+
|
|
256
|
+
When you notice multiple components fetching the same data or computing the same derived values, extract a `reactive()` function:
|
|
257
|
+
|
|
258
|
+
```tsx
|
|
259
|
+
// Shared across components
|
|
260
|
+
const getDashboardData = reactive((orgId: string) => {
|
|
261
|
+
const org = fetchQuery(GetOrg, { id: orgId });
|
|
262
|
+
const members = fetchQuery(GetMembers, { orgId });
|
|
263
|
+
const projects = fetchQuery(GetProjects, { orgId });
|
|
264
|
+
|
|
265
|
+
return { org, members, projects };
|
|
266
|
+
});
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Step 3: Use async reactive functions for complex flows
|
|
270
|
+
|
|
271
|
+
For data flows where queries depend on each other, add the Babel transform and use async/await:
|
|
272
|
+
|
|
273
|
+
```tsx
|
|
274
|
+
const getProjectDetails = reactive(async (projectId: string) => {
|
|
275
|
+
const project = await fetchQuery(GetProject, { id: projectId });
|
|
276
|
+
const owner = await fetchQuery(GetUser, { id: project.ownerId });
|
|
277
|
+
const tasks = await fetchQuery(GetTasks, { projectId });
|
|
278
|
+
|
|
279
|
+
return { project, owner, tasks };
|
|
280
|
+
});
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## Babel Transform Setup
|
|
286
|
+
|
|
287
|
+
The Signalium Babel transform is needed for async reactive functions. It rewrites `async` functions used with `reactive()` into generator-based coroutines that Signalium can pause and resume.
|
|
288
|
+
|
|
289
|
+
### Installation
|
|
290
|
+
|
|
291
|
+
The transform is included in the `signalium` package. No additional dependencies are needed.
|
|
292
|
+
|
|
293
|
+
### Configuration
|
|
294
|
+
|
|
295
|
+
Add the Signalium preset to your Babel configuration:
|
|
296
|
+
|
|
297
|
+
```js
|
|
298
|
+
// babel.config.js
|
|
299
|
+
module.exports = {
|
|
300
|
+
presets: ['signalium/transform'],
|
|
301
|
+
};
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
If you are using other Babel presets (e.g., for React or TypeScript), add `signalium/transform` alongside them:
|
|
305
|
+
|
|
306
|
+
```js
|
|
307
|
+
// babel.config.js
|
|
308
|
+
module.exports = {
|
|
309
|
+
presets: [
|
|
310
|
+
'@babel/preset-react',
|
|
311
|
+
'@babel/preset-typescript',
|
|
312
|
+
'signalium/transform',
|
|
313
|
+
],
|
|
314
|
+
};
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### What the transform does
|
|
318
|
+
|
|
319
|
+
The transform applies three rewrites:
|
|
320
|
+
|
|
321
|
+
1. **Async transform** --- rewrites `async` functions passed to `reactive()` into generators, enabling Signalium to track dependencies across `await` boundaries.
|
|
322
|
+
2. **Callback transform** --- wraps callback arguments in `callback()` for reactive tracking inside event handlers and closures.
|
|
323
|
+
3. **Promise methods transform** --- replaces `Promise.all`, `Promise.race`, and related methods with `ReactivePromise` equivalents, so concurrent data fetching integrates with the reactive system.
|
|
324
|
+
|
|
325
|
+
{% callout %}
|
|
326
|
+
The transform only affects code that uses Signalium APIs (`reactive`, `relay`, `task`, etc.). It does not modify unrelated async functions or Promise usage. Standard async/await outside of reactive contexts is untouched.
|
|
327
|
+
{% /callout %}
|
|
328
|
+
|
|
329
|
+
### Without the transform
|
|
330
|
+
|
|
331
|
+
If you prefer not to use a Babel transform, you can still use Signalium --- you just cannot use `async`/`await` inside reactive functions. Instead, access `ReactivePromise` values directly:
|
|
332
|
+
|
|
333
|
+
```tsx
|
|
334
|
+
const getUserProfile = reactive((userId: number) => {
|
|
335
|
+
const userPromise = fetchQuery(GetUser, { id: userId });
|
|
336
|
+
const postsPromise = fetchQuery(GetUserPosts, { userId });
|
|
337
|
+
|
|
338
|
+
// Access .value on the reactive promise (returns undefined while loading)
|
|
339
|
+
const user = userPromise.value;
|
|
340
|
+
const posts = postsPromise.value;
|
|
341
|
+
|
|
342
|
+
if (!user || !posts) return undefined;
|
|
343
|
+
|
|
344
|
+
return { user, posts };
|
|
345
|
+
});
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
This approach works without any build tooling but requires manual handling of loading states.
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## Getting Started with Signalium
|
|
353
|
+
|
|
354
|
+
For a deeper understanding of Signalium's reactive programming model --- including signals, reactive functions, relays, watchers, and contexts --- see the [Signalium documentation](https://signaliumjs.dev).
|
|
355
|
+
|
|
356
|
+
Key concepts to explore:
|
|
357
|
+
|
|
358
|
+
- **Signals** --- mutable state primitives (`signal()`)
|
|
359
|
+
- **Reactive functions** --- derived computations that automatically track dependencies (`reactive()`)
|
|
360
|
+
- **Relays** --- async computations with lifecycle management (`relay()`)
|
|
361
|
+
- **Watchers** --- side-effect subscriptions to reactive values (`watcher()`)
|
|
362
|
+
- **Contexts** --- dependency-injection-style scoping (`context()`, `getContext()`)
|
|
363
|
+
|
|
364
|
+
Fetchium's `fetchQuery()` returns a `ReactivePromise` (a Signalium async primitive), so understanding how Signalium handles async values will help you get the most out of the integration.
|