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 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, allowing AI tools to generate stable, type-safe data-fetching code.
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
- 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.
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
- _[Learn more](/docs/guide/getting-started.md) about fate's core concepts and features._
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
- The `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.
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 `HomePage` component, we can request a list of posts like this:
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 HomePage() {
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
- 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:
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 { useRequest, useView, ViewRef } from 'react-fate';
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
- ### Request Modes
432
+ ## Requests
423
433
 
424
- `useRequest` supports different request modes to control caching and data freshness. The available modes are:
434
+ ### Requesting Lists
425
435
 
426
- - `cache-first` (_default_): Returns data from the cache if available, otherwise fetches from the network.
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
- You can pass the request mode as an option to `useRequest`:
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
- const { posts } = useRequest(
434
- {
435
- posts: { root: PostView, type: 'Post' },
436
- },
437
- { mode: 'stale-while-revalidate' },
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
- root: PostView,
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
- } as const;
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
- const updated = await ctx.prisma.post.update({
667
- data: {
668
- likes: {
669
- increment: 1,
738
+ return resolve(
739
+ await ctx.prisma.post.update({
740
+ data: {
741
+ likes: {
742
+ increment: 1,
743
+ },
670
744
  },
671
- },
672
- select,
673
- where: { id: input.id },
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 { connectionArgs, createResolver } from '@nkzw/fate/server';
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
- .input(
799
- z.object({
800
- args: connectionArgs,
801
- ids: z.array(z.string().min(1)).nonempty(),
802
- select: z.array(z.string()),
803
- }),
804
- )
805
- .query(async ({ ctx, input }) => {
806
- const { resolveMany, select } = createResolver({
807
- ...input,
808
- ctx,
809
- view: userDataView,
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
- } as const;
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 also define root-level lists by exporting a `Lists` object from our `views.ts` file:
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 Lists = {
903
- posts: postDataView,
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
- This makes it possible to fetch a list of posts from the client using `useRequest`.
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
- You might want to define custom root lists that don't directly map to a single data view. For example, a search endpoint that returns a list of posts based on a search query:
998
+ For the above `Root` definitions, you can make the following requests using `useRequest`:
912
999
 
913
1000
  ```tsx
914
- export const Lists = {
915
- // …
916
- postSearch: { procedure: 'search', view: postDataView },
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: ({ item }) => item._count?.comments ?? 0,
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
- } as const;
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 the instance of the fate client, and using it in our React app using the `FateClient` context provider.
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 { fate } from './fate.ts';
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). You get to decide which parts are the good ones. The docs were 100% written by a human.
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. It lacks core features such as garbage collection, a compiler to extract view definitions statically and 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:
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<M extends Mutations>(): FateClient$1<M>;
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: ViewRef$1<ViewEntityName<V>>): ViewData<ViewEntityWithTypename<V>, ViewSelection<V>>;
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
- * Resolves a reference against a view and subscribes to updates for that selection.
29
- *
30
- * @example
31
- * const post = useView(PostView, postRef);
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
- const snapshot = client.readView(view$1, ref);
38
- snapshotRef.current = snapshot.status === "fulfilled" ? snapshot.value : null;
39
- return snapshot;
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
- return use(useDeferredValue(useSyncExternalStore(useCallback((onStoreChange) => {
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))).data;
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: { root: PostView, type: 'Post' } as const });
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.7",
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.7"
31
+ "@nkzw/fate": "^0.1.0"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@types/react": "^19.2.7",