react-fate 0.0.7 → 0.1.0
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 +233 -127
- package/lib/index.d.mts +17 -15
- package/lib/index.mjs +24 -14
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
- **Async React:** fate uses modern Async React features like Actions, Suspense, and `use` to support concurrent rendering and enable a seamless user experience.
|
|
19
19
|
- **Lists & Pagination:** fate provides built-in support for connection-style lists with cursor-based pagination, making it easy to implement infinite scrolling and "load-more" functionality.
|
|
20
20
|
- **Optimistic Updates:** fate supports declarative optimistic updates for mutations, allowing the UI to update immediately while the server request is in-flight. If the request fails, the cache and its associated views are rolled back to their previous state.
|
|
21
|
-
- **AI-Ready:** fate's minimal, predictable API and explicit data selection enable local reasoning,
|
|
21
|
+
- **AI-Ready:** fate's minimal, predictable API and explicit data selection enable local reasoning, enabling humans and AI tools to generate stable, type-safe data-fetching code.
|
|
22
22
|
|
|
23
23
|
### A modern data client for React & tRPC
|
|
24
24
|
|
|
@@ -26,39 +26,60 @@
|
|
|
26
26
|
|
|
27
27
|
GraphQL and Relay introduced several novel ideas: fragments co‑located with components, [a normalized cache](https://relay.dev/docs/principles-and-architecture/thinking-in-graphql/#caching-a-graph) keyed by global identifiers, and a compiler that hoists fragments into a single network request. These innovations made it possible to build large applications where data requirements are modular and self‑contained.
|
|
28
28
|
|
|
29
|
-
Nakazawa Tech builds apps primarily with GraphQL and Relay. We advocate for these technologies in [talks](https://www.youtube.com/watch?v=rxPTEko8J7c&t=36s) and provide templates ([server](https://github.com/nkzw-tech/server-template), [client](https://github.com/nkzw-tech/web-app-template/tree/with-relay)) to help developers get started quickly.
|
|
29
|
+
[Nakazawa Tech](https://nakazawa.tech) builds apps and [games](https://athenacrisis.com) primarily with GraphQL and Relay. We advocate for these technologies in [talks](https://www.youtube.com/watch?v=rxPTEko8J7c&t=36s) and provide templates ([server](https://github.com/nkzw-tech/server-template), [client](https://github.com/nkzw-tech/web-app-template/tree/with-relay)) to help developers get started quickly.
|
|
30
30
|
|
|
31
31
|
However, GraphQL comes with its own type system and query language. If you are already using tRPC or another type‑safe RPC framework, it's a significant investment to adopt and implement GraphQL on the backend. This investment often prevents teams from adopting Relay on the frontend.
|
|
32
32
|
|
|
33
33
|
Many React data frameworks lack Relay's ergonomics, especially fragment composition, co-located data requirements, predictable caching, and deep integration with modern React features. Optimistic updates usually require manually managing keys and imperative data updates, which is error-prone and tedious.
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
_fate_ takes the great ideas from Relay and puts them on top of tRPC. You get the best of both worlds: type safety between the client and server, and GraphQL-like ergonomics for data fetching. Using _fate_ usually looks like this:
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
```tsx
|
|
38
|
+
export const PostView = view<Post>()({
|
|
39
|
+
author: UserView,
|
|
40
|
+
content: true,
|
|
41
|
+
id: true,
|
|
42
|
+
title: true,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export const PostCard = ({ post: postRef }: { post: ViewRef<'Post'> }) => {
|
|
46
|
+
const post = useView(PostView, postRef);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Card>
|
|
50
|
+
<h2>{post.title}</h2>
|
|
51
|
+
<p>{post.content}</p>
|
|
52
|
+
<UserCard user={post.author} />
|
|
53
|
+
</Card>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
_[Learn more](/docs/guide/getting-started.md) about fate's core concepts or get started with [a ready-made template](https://github.com/nkzw-tech/fate-template#readme)._
|
|
38
59
|
|
|
39
60
|
## Getting Started
|
|
40
61
|
|
|
41
62
|
### Template
|
|
42
63
|
|
|
43
|
-
Get started with [a ready-made template](https://github.com/nkzw-tech/fate-template) quickly:
|
|
64
|
+
Get started with [a ready-made template](https://github.com/nkzw-tech/fate-template#readme) quickly:
|
|
44
65
|
|
|
45
66
|
::: code-group
|
|
46
67
|
|
|
47
|
-
```npm
|
|
68
|
+
```bash [npm]
|
|
48
69
|
npx giget@latest gh:nkzw-tech/fate-template
|
|
49
70
|
```
|
|
50
71
|
|
|
51
|
-
```pnpm
|
|
72
|
+
```bash [pnpm]
|
|
52
73
|
pnpx giget@latest gh:nkzw-tech/fate-template
|
|
53
74
|
```
|
|
54
75
|
|
|
55
|
-
```yarn
|
|
76
|
+
```bash [yarn]
|
|
56
77
|
yarn dlx giget@latest gh:nkzw-tech/fate-template
|
|
57
78
|
```
|
|
58
79
|
|
|
59
80
|
:::
|
|
60
81
|
|
|
61
|
-
|
|
82
|
+
`fate-template` comes with a simple tRPC backend and a React frontend using **_fate_**. It features modern tools to deliver an incredibly fast development experience. Follow its [README.md](https://github.com/nkzw-tech/fate-template#fate-quick-start-template) to get started.
|
|
62
83
|
|
|
63
84
|
### Manual Installation
|
|
64
85
|
|
|
@@ -66,15 +87,15 @@ The `fate-template` comes with a simple tRPC backend and a React frontend using
|
|
|
66
87
|
|
|
67
88
|
::: code-group
|
|
68
89
|
|
|
69
|
-
```npm
|
|
90
|
+
```bash [npm]
|
|
70
91
|
npm add react-fate
|
|
71
92
|
```
|
|
72
93
|
|
|
73
|
-
```pnpm
|
|
94
|
+
```bash [pnpm]
|
|
74
95
|
pnpm add react-fate
|
|
75
96
|
```
|
|
76
97
|
|
|
77
|
-
```yarn
|
|
98
|
+
```bash [yarn]
|
|
78
99
|
yarn add react-fate
|
|
79
100
|
```
|
|
80
101
|
|
|
@@ -84,15 +105,15 @@ And for your server, install the core `@nkzw/fate` package:
|
|
|
84
105
|
|
|
85
106
|
::: code-group
|
|
86
107
|
|
|
87
|
-
```npm
|
|
108
|
+
```bash [npm]
|
|
88
109
|
npm add @nkzw/fate
|
|
89
110
|
```
|
|
90
111
|
|
|
91
|
-
```pnpm
|
|
112
|
+
```bash [pnpm]
|
|
92
113
|
pnpm add @nkzw/fate
|
|
93
114
|
```
|
|
94
115
|
|
|
95
|
-
```yarn
|
|
116
|
+
```bash [yarn]
|
|
96
117
|
yarn add @nkzw/fate
|
|
97
118
|
```
|
|
98
119
|
|
|
@@ -126,6 +147,9 @@ Traditionally, React apps are built with components and hooks. fate introduces a
|
|
|
126
147
|
|
|
127
148
|
With fate, you no longer worry about _when_ to fetch data, how to coordinate loading states, or how to handle errors imperatively. You avoid overfetching, stop passing unnecessary data down the tree, and eliminate boilerplate types created solely for passing server data to child components.
|
|
128
149
|
|
|
150
|
+
> [!NOTE]
|
|
151
|
+
> Views in _fate_ are what fragments are in GraphQL.
|
|
152
|
+
|
|
129
153
|
## Views
|
|
130
154
|
|
|
131
155
|
### Defining Views
|
|
@@ -178,34 +202,20 @@ Components using `useView` listen to changes for all selected fields. When data
|
|
|
178
202
|
|
|
179
203
|
### Fetching Data with `useRequest`
|
|
180
204
|
|
|
181
|
-
Now that we defined our view and component, we fetch the data from the server using the `useRequest` hook from fate. This hook allows us to declare what data we need for a specific screen or component tree. At the root of our
|
|
205
|
+
Now that we defined our view and component, we fetch the data from the server using the `useRequest` hook from fate. This hook allows us to declare what data we need for a specific screen or component tree. At the root of our app, we can request a list of posts like this:
|
|
182
206
|
|
|
183
207
|
```tsx
|
|
184
208
|
import { useRequest } from 'react-fate';
|
|
185
209
|
import { PostCard, PostView } from './PostCard.tsx';
|
|
186
210
|
|
|
187
|
-
export function
|
|
188
|
-
const { posts } = useRequest({
|
|
189
|
-
posts: { root: PostView, type: 'Post' },
|
|
190
|
-
} as const);
|
|
211
|
+
export function App() {
|
|
212
|
+
const { posts } = useRequest({ posts: { list: PostView } });
|
|
191
213
|
|
|
192
214
|
return posts.map((post) => <PostCard key={post.id} post={post} />);
|
|
193
215
|
}
|
|
194
216
|
```
|
|
195
217
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
```tsx
|
|
199
|
-
<ErrorBoundary FallbackComponent={ErrorComponent}>
|
|
200
|
-
<Suspense fallback={<div>Loading…</div>}>
|
|
201
|
-
<HomePage />
|
|
202
|
-
</Suspense>
|
|
203
|
-
</ErrorBoundary>
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
> [!NOTE]
|
|
207
|
-
>
|
|
208
|
-
> `useRequest` might issue multiple requests which are automatically batched together by tRPC's [HTTP Batch Link](https://trpc.io/docs/client/links/httpBatchLink).
|
|
218
|
+
_Learn more about `useRequest` in the [Requests Guide](/docs/guide/requests.md)._
|
|
209
219
|
|
|
210
220
|
### Composing Views
|
|
211
221
|
|
|
@@ -213,7 +223,7 @@ In the above example we are defining a single view for a `Post`. One of fate's c
|
|
|
213
223
|
|
|
214
224
|
```tsx
|
|
215
225
|
import { Suspense } from 'react';
|
|
216
|
-
import {
|
|
226
|
+
import { useView, ViewRef } from 'react-fate';
|
|
217
227
|
|
|
218
228
|
export const PostView = view<Post>()({
|
|
219
229
|
author: {
|
|
@@ -419,23 +429,62 @@ const PostDetail = ({ post: postRef }: { post: ViewRef<'Post'> }) => {
|
|
|
419
429
|
|
|
420
430
|
ViewRefs carry a set of view names they can resolve. `useView` throws if a ref does not include the required view.
|
|
421
431
|
|
|
422
|
-
|
|
432
|
+
## Requests
|
|
423
433
|
|
|
424
|
-
|
|
434
|
+
### Requesting Lists
|
|
425
435
|
|
|
426
|
-
|
|
427
|
-
- `stale-while-revalidate`: Returns data from the cache and simultaneously fetches fresh data from the network.
|
|
428
|
-
- `network-only`: Always fetches data from the network, bypassing the cache.
|
|
436
|
+
The `useRequest` hook can be used to declare our data needs for a specific screen or component tree. At the root of our app, we can request a list of posts like this:
|
|
429
437
|
|
|
430
|
-
|
|
438
|
+
```tsx
|
|
439
|
+
import { useRequest } from 'react-fate';
|
|
440
|
+
import { PostCard, PostView } from './PostCard.tsx';
|
|
441
|
+
|
|
442
|
+
export function App() {
|
|
443
|
+
const { posts } = useRequest({ posts: { list: PostView } });
|
|
444
|
+
return posts.map((post) => <PostCard key={post.id} post={post} />);
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
This component suspends or throws errors, which bubble up to the nearest error boundary. Wrap your component tree with `ErrorBoundary` and `Suspense` components to show error and loading states:
|
|
431
449
|
|
|
432
450
|
```tsx
|
|
433
|
-
|
|
434
|
-
{
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
451
|
+
<ErrorBoundary FallbackComponent={ErrorComponent}>
|
|
452
|
+
<Suspense fallback={<div>Loading…</div>}>
|
|
453
|
+
<App />
|
|
454
|
+
</Suspense>
|
|
455
|
+
</ErrorBoundary>
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
> [!NOTE]
|
|
459
|
+
>
|
|
460
|
+
> `useRequest` might issue multiple requests which are automatically batched together by tRPC's [HTTP Batch Link](https://trpc.io/docs/client/links/httpBatchLink) into a single network request.
|
|
461
|
+
|
|
462
|
+
### Requesting Objects by ID
|
|
463
|
+
|
|
464
|
+
If you want to fetch data for a single object instead of a list, you can specify the `id` and the associated `view` like this:
|
|
465
|
+
|
|
466
|
+
```tsx
|
|
467
|
+
const { post } = useRequest({
|
|
468
|
+
post: { id: '12', view: PostView },
|
|
469
|
+
});
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
If you want to fetch multiple objects by their IDs, you can use the `ids` field:
|
|
473
|
+
|
|
474
|
+
```tsx
|
|
475
|
+
const { posts } = useRequest({
|
|
476
|
+
posts: { ids: ['6', '7'], view: PostView },
|
|
477
|
+
});
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
### Other Types of Requests
|
|
481
|
+
|
|
482
|
+
For any other queries, pass only the `type` and `view`:
|
|
483
|
+
|
|
484
|
+
```tsx
|
|
485
|
+
const { viewer } = useRequest({
|
|
486
|
+
viewer: { view: UserView },
|
|
487
|
+
});
|
|
439
488
|
```
|
|
440
489
|
|
|
441
490
|
### Request Arguments
|
|
@@ -446,12 +495,32 @@ You can pass arguments to `useRequest` calls. This is useful for pagination, fil
|
|
|
446
495
|
const { posts } = useRequest({
|
|
447
496
|
posts: {
|
|
448
497
|
args: { first: 10 },
|
|
449
|
-
|
|
450
|
-
type: 'Post',
|
|
498
|
+
list: PostView,
|
|
451
499
|
},
|
|
452
500
|
});
|
|
453
501
|
```
|
|
454
502
|
|
|
503
|
+
### Request Modes
|
|
504
|
+
|
|
505
|
+
`useRequest` supports different request modes to control caching and data freshness. The available modes are:
|
|
506
|
+
|
|
507
|
+
- `cache-first` (_default_): Returns data from the cache if available, otherwise fetches from the network.
|
|
508
|
+
- `stale-while-revalidate`: Returns data from the cache and simultaneously fetches fresh data from the network.
|
|
509
|
+
- `network-only`: Always fetches data from the network, bypassing the cache.
|
|
510
|
+
|
|
511
|
+
You can pass the request mode as an option to `useRequest`:
|
|
512
|
+
|
|
513
|
+
```tsx
|
|
514
|
+
const { posts } = useRequest(
|
|
515
|
+
{
|
|
516
|
+
posts: { list: PostView },
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
mode: 'stale-while-revalidate',
|
|
520
|
+
},
|
|
521
|
+
);
|
|
522
|
+
```
|
|
523
|
+
|
|
455
524
|
## List Views
|
|
456
525
|
|
|
457
526
|
### Pagination with `useListView`
|
|
@@ -473,7 +542,7 @@ const CommentConnectionView = {
|
|
|
473
542
|
items: {
|
|
474
543
|
node: CommentView,
|
|
475
544
|
},
|
|
476
|
-
}
|
|
545
|
+
};
|
|
477
546
|
|
|
478
547
|
const PostView = view<Post>()({
|
|
479
548
|
comments: CommentConnectionView,
|
|
@@ -525,6 +594,9 @@ Mutations in your tRPC backend are made available as actions and mutations by fa
|
|
|
525
594
|
Let's assume that our `Post` entity has a tRPC mutation for liking a post called `post.like`. A `LikeButton` component using fate Actions and an async component library could then look like this:
|
|
526
595
|
|
|
527
596
|
```tsx
|
|
597
|
+
import { useActionState } from 'react';
|
|
598
|
+
import { useFateClient } from 'react-fate';
|
|
599
|
+
|
|
528
600
|
const LikeButton = ({ post }: { post: { id: string; likes: number } }) => {
|
|
529
601
|
const fate = useFateClient();
|
|
530
602
|
const [result, like] = useActionState(fate.actions.post.like, null);
|
|
@@ -663,16 +735,17 @@ export const postRouter = router({
|
|
|
663
735
|
view: postDataView,
|
|
664
736
|
});
|
|
665
737
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
738
|
+
return resolve(
|
|
739
|
+
await ctx.prisma.post.update({
|
|
740
|
+
data: {
|
|
741
|
+
likes: {
|
|
742
|
+
increment: 1,
|
|
743
|
+
},
|
|
670
744
|
},
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
return resolve(updated as unknown as PostItem);
|
|
745
|
+
select,
|
|
746
|
+
where: { id: input.id },
|
|
747
|
+
} as PostUpdateArgs),
|
|
748
|
+
);
|
|
676
749
|
}),
|
|
677
750
|
});
|
|
678
751
|
```
|
|
@@ -741,6 +814,26 @@ useEffect(() => {
|
|
|
741
814
|
}, [like, result]);
|
|
742
815
|
```
|
|
743
816
|
|
|
817
|
+
### Controlling List Insertion Behavior
|
|
818
|
+
|
|
819
|
+
When inserting new objects into lists, the default behavior is to append the new object to the list. You can provide an `insert` option with `before`, `after` or `none` values to customize this behavior and specify where the new object should be inserted in the list:
|
|
820
|
+
|
|
821
|
+
```tsx
|
|
822
|
+
addComment({
|
|
823
|
+
input: { content: 'New Comment text', postId: post.id },
|
|
824
|
+
insert: 'before', // Insert the new comment at the beginning of the list.
|
|
825
|
+
});
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
Or, use the `none` option if you want to ignore inserting the new object into any lists:
|
|
829
|
+
|
|
830
|
+
```tsx
|
|
831
|
+
addComment({
|
|
832
|
+
input: { content: 'New Comment text', postId: post.id },
|
|
833
|
+
insert: 'none', // Do not insert the new comment into any lists.
|
|
834
|
+
});
|
|
835
|
+
```
|
|
836
|
+
|
|
744
837
|
## Server Integration
|
|
745
838
|
|
|
746
839
|
Until now, we have focused on the client-side API of fate. You'll need a tRPC backend that follows some conventions so you can generate a typed client using fate's CLI. At the moment _fate_ is designed to work with tRPC and Prisma, but the framework is not coupled to any particular ORM or database, it's just what we are starting with.
|
|
@@ -787,35 +880,27 @@ _Note: Currently, fate provides helpers to integrate with Prisma, but the framew
|
|
|
787
880
|
We can apply the above data view in our tRPC router and resolve the client's selection against it using `createResolver`. Here is an example implementation of the `byId` query for the `User` type which allows fetching multiple users by `id`:
|
|
788
881
|
|
|
789
882
|
```tsx
|
|
790
|
-
import {
|
|
883
|
+
import { byIdInput, createResolver } from '@nkzw/fate/server';
|
|
791
884
|
import { z } from 'zod';
|
|
792
885
|
import type { UserFindManyArgs } from '../../prisma/prisma-client/models.ts';
|
|
793
886
|
import { procedure, router } from '../init.ts';
|
|
794
887
|
import { userDataView } from '../views.ts';
|
|
795
888
|
|
|
796
889
|
export const userRouter = router({
|
|
797
|
-
byId: procedure
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
const users = await ctx.prisma.user.findMany({
|
|
813
|
-
select: select,
|
|
814
|
-
where: { id: { in: input.ids } },
|
|
815
|
-
} as UserFindManyArgs);
|
|
816
|
-
|
|
817
|
-
return await resolveMany(users);
|
|
818
|
-
}),
|
|
890
|
+
byId: procedure.input(byIdInput).query(async ({ ctx, input }) => {
|
|
891
|
+
const { resolveMany, select } = createResolver({
|
|
892
|
+
...input,
|
|
893
|
+
ctx,
|
|
894
|
+
view: userDataView,
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
const users = await ctx.prisma.user.findMany({
|
|
898
|
+
select: select,
|
|
899
|
+
where: { id: { in: input.ids } },
|
|
900
|
+
} as UserFindManyArgs);
|
|
901
|
+
|
|
902
|
+
return await resolveMany(users);
|
|
903
|
+
}),
|
|
819
904
|
});
|
|
820
905
|
```
|
|
821
906
|
|
|
@@ -875,7 +960,7 @@ export const postDataView = dataView<PostItem>('Post')({
|
|
|
875
960
|
content: true,
|
|
876
961
|
id: true,
|
|
877
962
|
title: true,
|
|
878
|
-
}
|
|
963
|
+
});
|
|
879
964
|
```
|
|
880
965
|
|
|
881
966
|
### Data View Lists
|
|
@@ -896,30 +981,39 @@ export const postDataView = dataView<PostItem>('Post')({
|
|
|
896
981
|
});
|
|
897
982
|
```
|
|
898
983
|
|
|
899
|
-
We can
|
|
984
|
+
We can define extra root-level lists and queries by exporting a `Root` object from our `views.ts` file using the same view syntax as everywhere else:
|
|
900
985
|
|
|
901
986
|
```tsx
|
|
902
|
-
export const
|
|
903
|
-
|
|
987
|
+
export const Root = {
|
|
988
|
+
categories: list(categoryDataView),
|
|
989
|
+
commentSearch: { procedure: 'search', view: list(commentDataView) },
|
|
990
|
+
events: list(eventDataView),
|
|
991
|
+
posts: list(postDataView),
|
|
992
|
+
viewer: userDataView,
|
|
904
993
|
};
|
|
905
994
|
```
|
|
906
995
|
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
#### Custom Root Lists
|
|
996
|
+
Entries that wrap their view in `list(...)` are treated as list resolvers and use the `procedure` name when calling the corresponding router procedure, defaulting to `list`. If you omit `list(...)`, fate treats the entry as a standard query and uses the view type name to infer the router name.
|
|
910
997
|
|
|
911
|
-
|
|
998
|
+
For the above `Root` definitions, you can make the following requests using `useRequest`:
|
|
912
999
|
|
|
913
1000
|
```tsx
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
//
|
|
918
|
-
}
|
|
1001
|
+
const query = 'Apple';
|
|
1002
|
+
|
|
1003
|
+
const { posts, categories, viewer } = useRequest({
|
|
1004
|
+
// Explicit Root queries:
|
|
1005
|
+
categories: { list: categoryView },
|
|
1006
|
+
commentSearch: { args: { query }, list: commentView },
|
|
1007
|
+
events: { list: eventView },
|
|
1008
|
+
posts: { list: postView },
|
|
1009
|
+
viewer: { view: userView },
|
|
1010
|
+
|
|
1011
|
+
// Queries by id, if those entities have a `byId` query defined:
|
|
1012
|
+
post: { id: '12', view: postView },
|
|
1013
|
+
comment: { ids: ['6', '7'], view: commentView },
|
|
1014
|
+
});
|
|
919
1015
|
```
|
|
920
1016
|
|
|
921
|
-
This maps the `postSearch` list to a `search` procedure on your post router.
|
|
922
|
-
|
|
923
1017
|
### Data View Resolvers
|
|
924
1018
|
|
|
925
1019
|
fate data views support resolvers for computed fields. If we want to add a `commentCount` field to our `Post` data view, we can use the `resolver` helper that defines a Prisma selection for the database query together with a `resolve` function:
|
|
@@ -927,19 +1021,33 @@ fate data views support resolvers for computed fields. If we want to add a `comm
|
|
|
927
1021
|
```tsx
|
|
928
1022
|
export const postDataView = dataView<PostItem>('Post')({
|
|
929
1023
|
author: userDataView,
|
|
930
|
-
commentCount: resolver<PostItem>({
|
|
931
|
-
resolve: ({
|
|
1024
|
+
commentCount: resolver<PostItem, number>({
|
|
1025
|
+
resolve: ({ _count }) => _count?.comments ?? 0,
|
|
932
1026
|
select: () => ({
|
|
933
1027
|
_count: { select: { comments: true } },
|
|
934
1028
|
}),
|
|
935
1029
|
}),
|
|
936
1030
|
comments: list(commentDataView),
|
|
937
1031
|
id: true,
|
|
938
|
-
}
|
|
1032
|
+
});
|
|
939
1033
|
```
|
|
940
1034
|
|
|
941
1035
|
This definition makes the `commentCount` field available to your client-side views.
|
|
942
1036
|
|
|
1037
|
+
### Authorization in Resolvers
|
|
1038
|
+
|
|
1039
|
+
You might want to restrict access to certain fields based on the current user or other contextual information. You can do this by adding an `authorize` function to your resolver definition:
|
|
1040
|
+
|
|
1041
|
+
```tsx
|
|
1042
|
+
export const userDataView = dataView<UserItem>('User')({
|
|
1043
|
+
email: resolver<UserItem, string | null, { sessionUser: string }>({
|
|
1044
|
+
authorize: ({ id }, context) => context?.sessionUserId === id,
|
|
1045
|
+
resolve: ({ email }) => email,
|
|
1046
|
+
}),
|
|
1047
|
+
id: true,
|
|
1048
|
+
});
|
|
1049
|
+
```
|
|
1050
|
+
|
|
943
1051
|
### Generating a typed client
|
|
944
1052
|
|
|
945
1053
|
Now that we have defined our client views and our tRPC server, we need to connect them with some glue code. We recommend using fate's CLI for convenience.
|
|
@@ -971,34 +1079,30 @@ _Note: fate uses the specified server module name to extract the server types it
|
|
|
971
1079
|
|
|
972
1080
|
### Creating a _fate_ Client
|
|
973
1081
|
|
|
974
|
-
Now that we have generated the client types, all that remains is creating
|
|
975
|
-
|
|
976
|
-
Create a `fate.ts` file:
|
|
977
|
-
|
|
978
|
-
```tsx
|
|
979
|
-
import { createFateClient } from './lib/fate.generated';
|
|
980
|
-
|
|
981
|
-
export const fate = createFateClient({
|
|
982
|
-
links: [
|
|
983
|
-
httpBatchLink({
|
|
984
|
-
fetch: (input, init) =>
|
|
985
|
-
fetch(input, {
|
|
986
|
-
...init,
|
|
987
|
-
credentials: 'include',
|
|
988
|
-
}),
|
|
989
|
-
url: `${env('SERVER_URL')}/trpc`,
|
|
990
|
-
}),
|
|
991
|
-
],
|
|
992
|
-
});
|
|
993
|
-
```
|
|
994
|
-
|
|
995
|
-
Now wrap your app with the `FateClient` provider:
|
|
1082
|
+
Now that we have generated the client types, all that remains is creating an instance of the fate client, and using it in our React app using the `FateClient` context provider:
|
|
996
1083
|
|
|
997
1084
|
```tsx
|
|
1085
|
+
import { httpBatchLink } from '@trpc/client';
|
|
998
1086
|
import { FateClient } from 'react-fate';
|
|
999
|
-
import {
|
|
1087
|
+
import { createFateClient } from './fate.ts';
|
|
1000
1088
|
|
|
1001
1089
|
export function App() {
|
|
1090
|
+
const fate = useMemo(
|
|
1091
|
+
() =>
|
|
1092
|
+
createFateClient({
|
|
1093
|
+
links: [
|
|
1094
|
+
httpBatchLink({
|
|
1095
|
+
fetch: (input, init) =>
|
|
1096
|
+
fetch(input, {
|
|
1097
|
+
...init,
|
|
1098
|
+
credentials: 'include',
|
|
1099
|
+
}),
|
|
1100
|
+
url: `${env('SERVER_URL')}/trpc`,
|
|
1101
|
+
}),
|
|
1102
|
+
],
|
|
1103
|
+
}),
|
|
1104
|
+
[],
|
|
1105
|
+
);
|
|
1002
1106
|
return <FateClient client={fate}>{/* Components go here */}</FateClient>;
|
|
1003
1107
|
}
|
|
1004
1108
|
```
|
|
@@ -1026,18 +1130,20 @@ Probably. One day. _Maybe._
|
|
|
1026
1130
|
### How was fate built?
|
|
1027
1131
|
|
|
1028
1132
|
> [!NOTE]
|
|
1029
|
-
> 80% of _fate_'s code was written by OpenAI's Codex – four versions per task, carefully curated by a human. The remaining 20% was written by [@cnakazawa](https://x.com/cnakazawa).
|
|
1133
|
+
> 80% of _fate_'s code was written by OpenAI's Codex – four versions per task, carefully curated by a human. The remaining 20% was written by [@cnakazawa](https://x.com/cnakazawa). _You get to decide which parts are the good ones!_ The docs were 100% written by a human.
|
|
1134
|
+
>
|
|
1135
|
+
> If you contribute to _fate_, we [require you to disclose your use of AI tools](https://github.com/nkzw-tech/fate/blob/main/CONTRIBUTING.md#ai-assistance-notice).
|
|
1030
1136
|
|
|
1031
1137
|
## Future
|
|
1032
1138
|
|
|
1033
|
-
**_fate_** is not complete yet.
|
|
1139
|
+
**_fate_** is not complete yet. The library lacks core features such as garbage collection, a compiler to extract view definitions statically ahead of time, and there is too much backend boilerplate. The current implementation of _fate_ is not tied to tRPC or Prisma, those are just the ones we are starting with. We welcome contributions and ideas to improve fate. Here are some features we'd like to add:
|
|
1034
1140
|
|
|
1035
1141
|
- Support for Drizzle
|
|
1036
1142
|
- Support backends other than tRPC
|
|
1143
|
+
- Persistent storage for offline support
|
|
1144
|
+
- Implement garbage collection for the cache
|
|
1037
1145
|
- Better code generation and less type repetition
|
|
1038
1146
|
- Support for live views and real-time updates via `useLiveView` and SSE
|
|
1039
|
-
- Implement garbage collection for the cache
|
|
1040
|
-
- Add persistent storage for offline support
|
|
1041
1147
|
|
|
1042
1148
|
## Acknowledgements
|
|
1043
1149
|
|
|
@@ -1045,4 +1151,4 @@ Probably. One day. _Maybe._
|
|
|
1045
1151
|
- [Ricky Hanlon](https://x.com/rickyfm) for guidance on Async React
|
|
1046
1152
|
- [Anthony Powell](https://x.com/Cephalization) for testing fate and providing feedback
|
|
1047
1153
|
|
|
1048
|
-
**_fate_** was created by [@cnakazawa](https://x.com/cnakazawa) and is maintained by [Nakazawa Tech](https://nakazawa.tech/).
|
|
1154
|
+
**_fate_** was created by [@cnakazawa](https://x.com/cnakazawa) and is maintained by [Nakazawa Tech](https://nakazawa.tech/).
|
package/lib/index.d.mts
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
|
-
import { ConnectionRef, FateClient as FateClient$1, FateMutations, Pagination, Request, RequestOptions, RequestResult, View, ViewData, ViewEntity, ViewEntityName, ViewRef, ViewRef as ViewRef$1, ViewSelection, createClient, createTRPCTransport, mutation, view } from "@nkzw/fate";
|
|
1
|
+
import { ConnectionRef, FateClient as FateClient$1, FateMutations, FateRoots, Pagination, Request, RequestOptions, RequestResult, View, ViewData, ViewEntity, ViewEntityName, ViewRef, ViewRef as ViewRef$1, ViewSelection, clientRoot, createClient, createTRPCTransport, mutation, toEntityId, view } from "@nkzw/fate";
|
|
2
2
|
import { ReactNode } from "react";
|
|
3
3
|
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
4
4
|
|
|
5
|
+
//#region src/useRequest.d.ts
|
|
6
|
+
type Roots = keyof ClientRoots extends never ? FateRoots : ClientRoots;
|
|
7
|
+
/**
|
|
8
|
+
* Declares the data a screen needs and kicks off fetching, suspending while the
|
|
9
|
+
* request resolves.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* const { posts } = useRequest({ posts: { list: PostView } });
|
|
13
|
+
*/
|
|
14
|
+
declare function useRequest<R extends Request, O extends FateRoots = Roots>(request: R, options?: RequestOptions): RequestResult<O, R>;
|
|
15
|
+
//#endregion
|
|
5
16
|
//#region src/context.d.ts
|
|
6
17
|
type Mutations = keyof ClientMutations extends never ? FateMutations : ClientMutations;
|
|
7
18
|
/**
|
|
@@ -12,12 +23,12 @@ declare function FateClient({
|
|
|
12
23
|
client
|
|
13
24
|
}: {
|
|
14
25
|
children: ReactNode;
|
|
15
|
-
client: FateClient$1<any>;
|
|
26
|
+
client: FateClient$1<any, any>;
|
|
16
27
|
}): react_jsx_runtime0.JSX.Element;
|
|
17
28
|
/**
|
|
18
29
|
* Returns the nearest `FateClient` from context.
|
|
19
30
|
*/
|
|
20
|
-
declare function useFateClient<
|
|
31
|
+
declare function useFateClient<T extends [Roots, Mutations]>(): FateClient$1<T[0], T[1]>;
|
|
21
32
|
//#endregion
|
|
22
33
|
//#region src/useView.d.ts
|
|
23
34
|
type ViewEntityWithTypename<V extends View<any, any>> = ViewEntity<V> & {
|
|
@@ -29,17 +40,7 @@ type ViewEntityWithTypename<V extends View<any, any>> = ViewEntity<V> & {
|
|
|
29
40
|
* @example
|
|
30
41
|
* const post = useView(PostView, postRef);
|
|
31
42
|
*/
|
|
32
|
-
declare function useView<V extends View<any, any>>(view: V, ref:
|
|
33
|
-
//#endregion
|
|
34
|
-
//#region src/useRequest.d.ts
|
|
35
|
-
/**
|
|
36
|
-
* Declares the data a screen needs and kicks off fetching, suspending while the
|
|
37
|
-
* request resolves.
|
|
38
|
-
*
|
|
39
|
-
* @example
|
|
40
|
-
* const { posts } = useRequest({ posts: { root: PostView, type: 'Post' } as const });
|
|
41
|
-
*/
|
|
42
|
-
declare function useRequest<R extends Request>(request: R, options?: RequestOptions): RequestResult<R>;
|
|
43
|
+
declare function useView<V extends View<any, any>, R extends ViewRef$1<ViewEntityName<V>> | null>(view: V, ref: R): R extends null ? null : ViewData<ViewEntityWithTypename<V>, ViewSelection<V>>;
|
|
43
44
|
//#endregion
|
|
44
45
|
//#region src/useListView.d.ts
|
|
45
46
|
type ConnectionItems<C> = C extends {
|
|
@@ -62,5 +63,6 @@ declare function useListView<C extends {
|
|
|
62
63
|
//#endregion
|
|
63
64
|
//#region src/index.d.ts
|
|
64
65
|
interface ClientMutations {}
|
|
66
|
+
interface ClientRoots {}
|
|
65
67
|
//#endregion
|
|
66
|
-
export { ClientMutations, type ConnectionRef, FateClient, type ViewRef, createClient, createTRPCTransport, mutation, useFateClient, useListView, useRequest, useView, view };
|
|
68
|
+
export { ClientMutations, ClientRoots, type ConnectionRef, FateClient, type ViewRef, clientRoot, createClient, createTRPCTransport, mutation, toEntityId, useFateClient, useListView, useRequest, useView, view };
|
package/lib/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ConnectionTag, createClient, createTRPCTransport, isViewTag, mutation, view } from "@nkzw/fate";
|
|
1
|
+
import { ConnectionTag, clientRoot, createClient, createTRPCTransport, isViewTag, mutation, toEntityId, view } from "@nkzw/fate";
|
|
2
2
|
import { createContext, use, useCallback, useDeferredValue, useEffect, useMemo, useRef, useSyncExternalStore } from "react";
|
|
3
3
|
import { jsx } from "react/jsx-runtime";
|
|
4
4
|
|
|
@@ -24,25 +24,34 @@ function useFateClient() {
|
|
|
24
24
|
|
|
25
25
|
//#endregion
|
|
26
26
|
//#region src/useView.tsx
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
const nullSnapshot = {
|
|
28
|
+
status: "fulfilled",
|
|
29
|
+
then(onfulfilled, onrejected) {
|
|
30
|
+
return Promise.resolve(null).then(onfulfilled, onrejected);
|
|
31
|
+
},
|
|
32
|
+
value: null
|
|
33
|
+
};
|
|
33
34
|
function useView(view$1, ref) {
|
|
34
35
|
const client = useFateClient();
|
|
35
36
|
const snapshotRef = useRef(null);
|
|
36
37
|
const getSnapshot = useCallback(() => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
if (ref === null) {
|
|
39
|
+
snapshotRef.current = null;
|
|
40
|
+
return nullSnapshot;
|
|
41
|
+
}
|
|
42
|
+
const snapshot$1 = client.readView(view$1, ref);
|
|
43
|
+
snapshotRef.current = snapshot$1.status === "fulfilled" ? snapshot$1.value : null;
|
|
44
|
+
return snapshot$1;
|
|
40
45
|
}, [
|
|
41
46
|
client,
|
|
42
47
|
view$1,
|
|
43
48
|
ref
|
|
44
49
|
]);
|
|
45
|
-
|
|
50
|
+
const snapshot = use(useDeferredValue(useSyncExternalStore(useCallback((onStoreChange) => {
|
|
51
|
+
if (ref === null) {
|
|
52
|
+
snapshotRef.current = null;
|
|
53
|
+
return () => {};
|
|
54
|
+
}
|
|
46
55
|
const subscriptions = /* @__PURE__ */ new Map();
|
|
47
56
|
const onChange = () => {
|
|
48
57
|
updateSubscriptions();
|
|
@@ -68,7 +77,8 @@ function useView(view$1, ref) {
|
|
|
68
77
|
for (const unsubscribe of subscriptions.values()) unsubscribe();
|
|
69
78
|
subscriptions.clear();
|
|
70
79
|
};
|
|
71
|
-
}, [client.store]), getSnapshot, getSnapshot)))
|
|
80
|
+
}, [client.store, ref]), getSnapshot, getSnapshot)));
|
|
81
|
+
return snapshot ? snapshot.data : null;
|
|
72
82
|
}
|
|
73
83
|
|
|
74
84
|
//#endregion
|
|
@@ -78,7 +88,7 @@ function useView(view$1, ref) {
|
|
|
78
88
|
* request resolves.
|
|
79
89
|
*
|
|
80
90
|
* @example
|
|
81
|
-
* const { posts } = useRequest({ posts: {
|
|
91
|
+
* const { posts } = useRequest({ posts: { list: PostView } });
|
|
82
92
|
*/
|
|
83
93
|
function useRequest(request, options) {
|
|
84
94
|
const client = useFateClient();
|
|
@@ -173,4 +183,4 @@ function useListView(selection, connection) {
|
|
|
173
183
|
}
|
|
174
184
|
|
|
175
185
|
//#endregion
|
|
176
|
-
export { FateClient, createClient, createTRPCTransport, mutation, useFateClient, useListView, useRequest, useView, view };
|
|
186
|
+
export { FateClient, clientRoot, createClient, createTRPCTransport, mutation, toEntityId, useFateClient, useListView, useRequest, useView, view };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-fate",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "fate is a modern data client for React.",
|
|
5
5
|
"homepage": "https://github.com/nkzw-tech/fate",
|
|
6
6
|
"repository": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"lib"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@nkzw/fate": "^0.0
|
|
31
|
+
"@nkzw/fate": "^0.1.0"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@types/react": "^19.2.7",
|