From 11ac41c91fe96a67b90ac6dabe1768844f672670 Mon Sep 17 00:00:00 2001 From: Daniel Steigerwald Date: Sun, 19 Nov 2023 14:24:02 +0100 Subject: [PATCH] Complete EvoluProvider, add dev comments --- apps/web/components/NextJsExample.tsx | 5 +- packages/evolu-common-react/src/index.tsx | 385 +++++++++++----------- packages/evolu-react/src/index.ts | 3 +- 3 files changed, 204 insertions(+), 189 deletions(-) diff --git a/apps/web/components/NextJsExample.tsx b/apps/web/components/NextJsExample.tsx index 1f837984b..be61a9d58 100644 --- a/apps/web/components/NextJsExample.tsx +++ b/apps/web/components/NextJsExample.tsx @@ -51,7 +51,6 @@ const Database = S.struct({ type Database = S.Schema.To; const { - EvoluProvider, useEvoluError, createQuery, useQuery, @@ -76,7 +75,7 @@ export const NextJsExample: FC = () => { }); return ( - + <> {todosShown ? : } - + ); }; diff --git a/packages/evolu-common-react/src/index.tsx b/packages/evolu-common-react/src/index.tsx index bd071bdd9..af30f1cd5 100644 --- a/packages/evolu-common-react/src/index.tsx +++ b/packages/evolu-common-react/src/index.tsx @@ -1,191 +1,206 @@ /// import { - Evolu, - EvoluError, - Owner, - PlatformName, - Query, - QueryResult, - Row, - Schema, - SyncState, - emptyRows, - queryResultFromRows, - } from "@evolu/common"; - import { Context, Effect, Function, Layer } from "effect"; - import ReactExports, { - FC, - ReactNode, - createContext, - useContext, - useEffect, - useMemo, - useSyncExternalStore, - } from "react"; - - export interface EvoluCommonReact { - /** TODO: Docs */ - readonly evolu: Evolu; - - /** TODO: Docs */ - readonly EvoluProvider: FC<{ - readonly children?: ReactNode | undefined; - }>; - - /** TODO: Docs */ - readonly useEvolu: () => Evolu; - - /** TODO: Docs */ - readonly useEvoluError: () => EvoluError | null; - - /** TODO: Docs */ - readonly createQuery: Evolu["createQuery"]; - - /** - * It's like React `use` Hook but for React 18. It will use React `use` with React 19. - */ - readonly useQueryPromise: ( + Evolu, + EvoluError, + Owner, + PlatformName, + Query, + QueryResult, + Row, + Schema, + SyncState, + emptyRows, + queryResultFromRows, +} from "@evolu/common"; +import { Context, Effect, Function, Layer } from "effect"; +import ReactExports, { + FC, + ReactNode, + createContext, + useContext, + useEffect, + useMemo, + useSyncExternalStore, +} from "react"; + +export interface EvoluCommonReact { + /** TODO: Docs */ + readonly evolu: Evolu; + + /** + * The default value of EvoluContext is an Evolu instance, so we don't have + * to use EvoluProvider by default. However, EvoluProvider is helpful for + * testing, as we can inject memory-only Evolu. + */ + readonly EvoluProvider: FC<{ + readonly children?: ReactNode | undefined; + readonly value: Evolu; + }>; + + /** TODO: Docs */ + readonly useEvolu: () => Evolu; + + /** TODO: Docs */ + readonly useEvoluError: () => EvoluError | null; + + /** TODO: Docs */ + readonly createQuery: Evolu["createQuery"]; + + /** + * It's like React `use` Hook but for React 18. It uses React `use` with React 19. + */ + readonly useQueryPromise: ( + promise: Promise>, + ) => QueryResult; + + /** TODO: Docs */ + readonly useQuerySubscription: ( + query: Query, + ) => QueryResult; + + /** TODO: Docs */ + readonly useQuery: (query: Query) => QueryResult; + + /** TODO: Docs */ + readonly useQueryOnce: (query: Query) => QueryResult; + + /** TODO: Docs */ + readonly useCreate: () => Evolu["create"]; + + /** TODO: Docs */ + readonly useUpdate: () => Evolu["update"]; + + readonly useOwner: () => Owner | null; + + readonly useSyncState: () => SyncState; + + // const { } = useQueries([todos, blas]) + // readonly useQueries: +} + +export const EvoluCommonReact = Context.Tag(); + +export const EvoluCommonReactLive = Layer.effect( + EvoluCommonReact, + Effect.gen(function* (_) { + const evolu = yield* _(Evolu); + + const EvoluContext = createContext(evolu); + + const EvoluProvider: EvoluCommonReact["EvoluProvider"] = ({ + children, + value, + }) => ( + {children} + ); + + const useEvolu: EvoluCommonReact["useEvolu"] = () => + useContext(EvoluContext); + + const useEvoluError: EvoluCommonReact["useEvoluError"] = () => { + const evolu = useEvolu(); + return useSyncExternalStore( + evolu.subscribeError, + evolu.getError, + Function.constNull, + ); + }; + + const platformName = yield* _(PlatformName); + + const useQueryPromise = ( promise: Promise>, - ) => QueryResult; - - /** TODO: Docs */ - readonly useQuerySubscription: ( + ): QueryResult => + platformName === "server" + ? queryResultFromRows(emptyRows()) + : use(promise); + + const useQuerySubscription = ( query: Query, - ) => QueryResult; - - /** TODO: Docs */ - readonly useQuery: (query: Query) => QueryResult; - - /** TODO: Docs */ - readonly useQueryOnce: (query: Query) => QueryResult; - - /** TODO: Docs */ - readonly useCreate: () => Evolu["create"]; - - /** TODO: Docs */ - readonly useUpdate: () => Evolu["update"]; - - readonly useOwner: () => Owner | null; - - readonly useSyncState: () => SyncState; - } - - export const EvoluCommonReact = Context.Tag(); - - export const EvoluCommonReactLive = Layer.effect( - EvoluCommonReact, - Effect.gen(function* (_) { - const evolu = yield* _(Evolu); - const EvoluContext = createContext(evolu); - - const EvoluProvider: EvoluCommonReact["EvoluProvider"] = ({ children }) => ( - {children} + ): QueryResult => { + const evolu = useEvolu(); + return useSyncExternalStore( + useMemo(() => evolu.subscribeQuery(query), [evolu, query]), + useMemo(() => () => evolu.getQuery(query), [evolu, query]), + ); + }; + + const useQuery = (query: Query): QueryResult => { + useQueryPromise(useEvolu().loadQuery(query)); + return useQuerySubscription(query); + }; + + const useQueryOnce = (query: Query): QueryResult => { + const evolu = useEvolu(); + const result = useQueryPromise(evolu.loadQuery(query)); + // Loading promises are released on mutation by default, so loading the same + // query will be suspended again, which is undesirable if we already have such + // a query on a page. Luckily, subscribeQuery tracks subscribed queries to be + // automatically updated on mutation while unsubscribed queries are released. + // It's probably how the future React Cache will work. + useEffect( + () => evolu.subscribeQuery(query)(Function.constVoid), + [evolu, query], ); - - const useEvolu: EvoluCommonReact["useEvolu"] = () => - useContext(EvoluContext); - - const useEvoluError: EvoluCommonReact["useEvoluError"] = () => { - const evolu = useEvolu(); - return useSyncExternalStore( - evolu.subscribeError, - evolu.getError, - Function.constNull, - ); - }; - - const platformName = yield* _(PlatformName); - - const useQueryPromise = ( - promise: Promise>, - ): QueryResult => - platformName === "server" - ? queryResultFromRows(emptyRows()) - : use(promise); - - const useQuerySubscription = ( - query: Query, - ): QueryResult => { - const evolu = useEvolu(); - return useSyncExternalStore( - useMemo(() => evolu.subscribeQuery(query), [evolu, query]), - useMemo(() => () => evolu.getQuery(query), [evolu, query]), - ); - }; - - const useQuery = (query: Query): QueryResult => { - useQueryPromise(useEvolu().loadQuery(query)); - return useQuerySubscription(query); - }; - - const useQueryOnce = (query: Query): QueryResult => { - const evolu = useEvolu(); - const result = useQueryPromise(evolu.loadQuery(query)); - // Subscribed queries are not released. - useEffect( - () => evolu.subscribeQuery(query)(Function.constVoid), - [evolu, query], - ); - return result; - }; - - const useOwner: EvoluCommonReact["useOwner"] = () => { - const evolu = useEvolu(); - return useSyncExternalStore(evolu.subscribeOwner, evolu.getOwner); - }; - - const useSyncState: EvoluCommonReact["useSyncState"] = () => { - const evolu = useEvolu(); - return useSyncExternalStore(evolu.subscribeSyncState, evolu.getSyncState); - }; - - return EvoluCommonReact.of({ - evolu, - EvoluProvider, - useEvolu, - useEvoluError, - createQuery: evolu.createQuery, - useQuerySubscription, - useQueryPromise, - useQuery, - useQueryOnce, - useCreate: () => useEvolu().create, - useUpdate: () => useEvolu().update, - useOwner, - useSyncState, - }); - }), - ); - - // https://github.com/acdlite/rfcs/blob/first-class-promises/text/0000-first-class-support-for-promises.md - const use = - ReactExports.use || - (( - promise: Promise & { - status?: "pending" | "fulfilled" | "rejected"; - value?: T; - reason?: unknown; - }, - ): T => { - if (promise.status === "pending") { - throw promise; - } else if (promise.status === "fulfilled") { - return promise.value as T; - } else if (promise.status === "rejected") { - throw promise.reason; - } else { - promise.status = "pending"; - promise.then( - (v) => { - promise.status = "fulfilled"; - promise.value = v; - }, - (e) => { - promise.status = "rejected"; - promise.reason = e; - }, - ); - throw promise; - } + return result; + }; + + const useOwner: EvoluCommonReact["useOwner"] = () => { + const evolu = useEvolu(); + return useSyncExternalStore(evolu.subscribeOwner, evolu.getOwner); + }; + + const useSyncState: EvoluCommonReact["useSyncState"] = () => { + const evolu = useEvolu(); + return useSyncExternalStore(evolu.subscribeSyncState, evolu.getSyncState); + }; + + return EvoluCommonReact.of({ + evolu, + EvoluProvider, + useEvolu, + useEvoluError, + createQuery: evolu.createQuery, + useQuerySubscription, + useQueryPromise, + useQuery, + useQueryOnce, + useCreate: () => useEvolu().create, + useUpdate: () => useEvolu().update, + useOwner, + useSyncState, }); - \ No newline at end of file + }), +); + +// https://github.com/acdlite/rfcs/blob/first-class-promises/text/0000-first-class-support-for-promises.md +const use = + ReactExports.use || + (( + promise: Promise & { + status?: "pending" | "fulfilled" | "rejected"; + value?: T; + reason?: unknown; + }, + ): T => { + if (promise.status === "pending") { + throw promise; + } else if (promise.status === "fulfilled") { + return promise.value as T; + } else if (promise.status === "rejected") { + throw promise.reason; + } else { + promise.status = "pending"; + promise.then( + (v) => { + promise.status = "fulfilled"; + promise.value = v; + }, + (e) => { + promise.status = "rejected"; + promise.reason = e; + }, + ); + throw promise; + } + }); diff --git a/packages/evolu-react/src/index.ts b/packages/evolu-react/src/index.ts index 5d87cae69..85ab46fae 100644 --- a/packages/evolu-react/src/index.ts +++ b/packages/evolu-react/src/index.ts @@ -24,5 +24,6 @@ export const create = ( Effect.runSync, ); fastRefreshRef.evolu.ensureSchema(schema); - return fastRefreshRef as EvoluCommonReact; + // The Effect team does not recommend generic services, hence casting. + return fastRefreshRef as unknown as EvoluCommonReact; };