diff --git a/apps/web/components/NextJsExample.tsx b/apps/web/components/NextJsExample.tsx index 8810d6ca5..a43a8d11e 100644 --- a/apps/web/components/NextJsExample.tsx +++ b/apps/web/components/NextJsExample.tsx @@ -58,6 +58,8 @@ const { useUpdate, useOwner, useEvolu, + // useQuerySubscription, + // use, // useQueries, } = Evolu.create(Database, { reloadUrl: "/examples/nextjs", @@ -68,24 +70,6 @@ const { export const NextJsExample: FC = () => { const [todosShown, setTodosShown] = useState(true); - // const evolu = useEvolu(); - - // const todoId = createQuery((db) => db.selectFrom("todo").select(["id"])); - - // evolu.loadQueries([todoId, todosWithCategories]).then(([a, b]) => { - // // - // }); - - // const [a, b, c, d, e] = useQueries( - // [todoId, todosWithCategories, todoCategories], - // [todoCategories], - // [todoId], - // ); - - // a.rows[0]. - - // // eslint-disable-next-line no-console - // console.log(a, b, c, d, e); // https://react.dev/reference/react/useTransition#building-a-suspense-enabled-router const handleTabClick = (): void => diff --git a/packages/evolu-common-react/src/index.tsx b/packages/evolu-common-react/src/index.tsx index 907c1e1de..77ddb7b3a 100644 --- a/packages/evolu-common-react/src/index.tsx +++ b/packages/evolu-common-react/src/index.tsx @@ -3,7 +3,6 @@ import { Evolu, EvoluError, Owner, - PlatformName, Queries, Query, QueryResult, @@ -11,17 +10,17 @@ import { Row, Schema, SyncState, - emptyRows, - queryResultFromRows, } from "@evolu/common"; import { Context, Effect, Function, Layer } from "effect"; import ReactExports, { FC, ReactNode, + Usable, createContext, useContext, useEffect, useMemo, + useRef, useSyncExternalStore, } from "react"; @@ -29,6 +28,9 @@ export interface EvoluCommonReact { /** TODO: Docs */ readonly evolu: Evolu; + /** A React 19 `use` polyfill. */ + readonly use: (usable: Usable) => T; + /** TODO: Docs */ readonly useEvolu: () => Evolu; @@ -39,38 +41,46 @@ export interface EvoluCommonReact { readonly createQuery: Evolu["createQuery"]; /** - * It's like React `use` Hook but for React 18. It uses React `use` with React 19. + * TODO: Docs + * 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. */ - readonly useQueryPromise: ( - promise: Promise>, - ) => QueryResult; - - /** TODO: Docs */ readonly useQuerySubscription: ( query: Query, + options?: Partial<{ + /** TODO: Docs, exaplain why once is useful. */ + readonly once: boolean; + }>, ) => QueryResult; /** TODO: Docs */ - readonly useQuery: (query: Query) => QueryResult; - - /** TODO: Docs */ - readonly useQueryOnce: (query: Query) => QueryResult; + readonly useQuery: ( + query: Query, + options?: Partial<{ + readonly once: boolean; + }>, + ) => QueryResult; - /** TODO: Docs */ + /** + * TODO: Docs + * For more than one query, always use useQueries Hook to avoid loading waterfalls + * and to cache loading promises. + * This is possible of course: + * const foo = use(useEvolu().loadQuery(todos)() + * but it will not cache loading promise nor subscribe updates. + * That's why we have useQuery and useQueries. + * + */ readonly useQueries: < R extends Row, Q1 extends Queries, Q2 extends Queries, - Q3 extends Queries, >( queries: [...Q1], loadOnlyQueries?: [...Q2], - subscribeOnlyQueries?: [...Q3], - ) => [ - ...QueryResultsFromQueries, - ...QueryResultsFromQueries, - ...QueryResultsFromQueries, - ]; + ) => [...QueryResultsFromQueries, ...QueryResultsFromQueries]; /** TODO: Docs */ readonly useCreate: () => Evolu["create"]; @@ -102,33 +112,40 @@ export const EvoluCommonReact = Context.Tag(); export const EvoluCommonReactLive = Layer.effect( EvoluCommonReact, Effect.gen(function* (_) { - const platformName = yield* _(PlatformName); const evolu = yield* _(Evolu); const EvoluContext = createContext(evolu); const useEvolu: EvoluCommonReact["useEvolu"] = () => useContext(EvoluContext); - // TODO: Accept also Promise>> - const useQueryPromise = ( - promise: Promise>, - ): QueryResult => - platformName === "server" - ? queryResultFromRows(emptyRows()) - : use(promise); - - const useQuerySubscription = ( - query: Query, - ): QueryResult => { + const useQuerySubscription: EvoluCommonReact["useQuerySubscription"] = ( + query, + options = {}, + ) => { const evolu = useEvolu(); + // The options can't be change, hence useRef. + const optionsRef = useRef(options).current; + + /* eslint-disable react-hooks/rules-of-hooks */ + if (optionsRef.once) { + // No useSyncExternalStore, no unnecessary updates. + useEffect( + () => evolu.subscribeQuery(query)(Function.constVoid), + [evolu, query], + ); + return evolu.getQuery(query); + } + return useSyncExternalStore( useMemo(() => evolu.subscribeQuery(query), [evolu, query]), useMemo(() => () => evolu.getQuery(query), [evolu, query]), ); + /* eslint-enable react-hooks/rules-of-hooks */ }; return EvoluCommonReact.of({ evolu, + use, useEvolu, useEvoluError: () => { @@ -142,29 +159,14 @@ export const EvoluCommonReactLive = Layer.effect( createQuery: evolu.createQuery, useQuerySubscription, - useQueryPromise, - - useQuery: (query) => { - const evolu = useEvolu(); - useQueryPromise(evolu.loadQuery(query)); - return useQuerySubscription(query); - }, - useQueryOnce: (query) => { + useQuery: (query, options) => { 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. - useEffect( - () => evolu.subscribeQuery(query)(Function.constVoid), - [evolu, query], - ); - return result; + use(evolu.loadQuery(query)); + return useQuerySubscription(query, options); }, - useQueries: (_queries, _loadOnlyQueries, _subscribeOnlyQueries) => { + useQueries: (_queries, _loadOnlyQueries) => { // const evolu = useEvolu(); // const promise = evolu.loadQueries( // queries.concat(loadOnlyQueries || []), @@ -174,8 +176,8 @@ export const EvoluCommonReactLive = Layer.effect( throw ""; }, - useCreate: () => useContext(EvoluContext).create, - useUpdate: () => useContext(EvoluContext).update, + useCreate: () => useEvolu().create, + useUpdate: () => useEvolu().update, useOwner: () => { const evolu = useEvolu(); diff --git a/packages/evolu-react/src/index.ts b/packages/evolu-react/src/index.ts index 85ab46fae..f54793c94 100644 --- a/packages/evolu-react/src/index.ts +++ b/packages/evolu-react/src/index.ts @@ -1,8 +1,8 @@ import * as S from "@effect/schema/Schema"; import { Config, ConfigLive, Schema } from "@evolu/common"; import { EvoluCommonReact, EvoluCommonReactLive } from "@evolu/common-react"; -import { EvoluCommonWebLive, PlatformNameLive } from "@evolu/common-web"; -import { Effect, Layer } from "effect"; +import { EvoluCommonWebLive } from "@evolu/common-web"; +import { Effect } from "effect"; export * from "@evolu/common/public"; @@ -15,12 +15,9 @@ export const create = ( ): EvoluCommonReact => { if (!fastRefreshRef) fastRefreshRef = EvoluCommonReact.pipe( - Effect.provide( - EvoluCommonReactLive.pipe( - Layer.use(Layer.merge(EvoluCommonWebLive, PlatformNameLive)), - Layer.use(ConfigLive(config)), - ), - ), + Effect.provide(EvoluCommonReactLive), + Effect.provide(EvoluCommonWebLive), + Effect.provide(ConfigLive(config)), Effect.runSync, ); fastRefreshRef.evolu.ensureSchema(schema);