@evolu/react 10.4.0 → 11.0.0-next.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.
Files changed (63) hide show
  1. package/README.md +2 -2
  2. package/dist/src/Task.d.ts +42 -0
  3. package/dist/src/Task.d.ts.map +1 -0
  4. package/dist/src/Task.js +48 -0
  5. package/dist/src/index.d.ts +4 -0
  6. package/dist/src/index.d.ts.map +1 -0
  7. package/dist/src/index.js +3 -0
  8. package/dist/src/local-first/createEvoluBinding.d.ts +64 -0
  9. package/dist/src/local-first/createEvoluBinding.d.ts.map +1 -0
  10. package/dist/src/local-first/createEvoluBinding.js +76 -0
  11. package/dist/src/local-first/useIsSsr.d.ts.map +1 -0
  12. package/dist/{useIsSsr.js → src/local-first/useIsSsr.js} +4 -5
  13. package/package.json +22 -24
  14. package/src/Task.tsx +58 -0
  15. package/src/index.ts +3 -11
  16. package/src/local-first/createEvoluBinding.tsx +205 -0
  17. package/src/{useIsSsr.ts → local-first/useIsSsr.ts} +3 -4
  18. package/LICENSE +0 -21
  19. package/dist/EvoluContext.d.ts +0 -3
  20. package/dist/EvoluContext.d.ts.map +0 -1
  21. package/dist/EvoluContext.js +0 -2
  22. package/dist/EvoluProvider.d.ts +0 -7
  23. package/dist/EvoluProvider.d.ts.map +0 -1
  24. package/dist/EvoluProvider.js +0 -4
  25. package/dist/createUseEvolu.d.ts +0 -13
  26. package/dist/createUseEvolu.d.ts.map +0 -1
  27. package/dist/createUseEvolu.js +0 -14
  28. package/dist/index.d.ts +0 -11
  29. package/dist/index.d.ts.map +0 -1
  30. package/dist/index.js +0 -11
  31. package/dist/useEvolu.d.ts +0 -9
  32. package/dist/useEvolu.d.ts.map +0 -1
  33. package/dist/useEvolu.js +0 -15
  34. package/dist/useEvoluError.d.ts +0 -4
  35. package/dist/useEvoluError.d.ts.map +0 -1
  36. package/dist/useEvoluError.js +0 -8
  37. package/dist/useIsSsr.d.ts.map +0 -1
  38. package/dist/useOwner.d.ts +0 -9
  39. package/dist/useOwner.d.ts.map +0 -1
  40. package/dist/useOwner.js +0 -16
  41. package/dist/useQueries.d.ts +0 -13
  42. package/dist/useQueries.d.ts.map +0 -1
  43. package/dist/useQueries.js +0 -31
  44. package/dist/useQuery.d.ts +0 -37
  45. package/dist/useQuery.d.ts.map +0 -1
  46. package/dist/useQuery.js +0 -45
  47. package/dist/useQuerySubscription.d.ts +0 -10
  48. package/dist/useQuerySubscription.d.ts.map +0 -1
  49. package/dist/useQuerySubscription.js +0 -18
  50. package/dist/useSyncState.d.ts +0 -4
  51. package/dist/useSyncState.d.ts.map +0 -1
  52. package/dist/useSyncState.js +0 -11
  53. package/src/EvoluContext.ts +0 -4
  54. package/src/EvoluProvider.tsx +0 -13
  55. package/src/createUseEvolu.ts +0 -17
  56. package/src/useEvolu.ts +0 -20
  57. package/src/useEvoluError.ts +0 -9
  58. package/src/useOwner.ts +0 -18
  59. package/src/useQueries.ts +0 -52
  60. package/src/useQuery.ts +0 -57
  61. package/src/useQuerySubscription.ts +0 -38
  62. package/src/useSyncState.ts +0 -13
  63. /package/dist/{useIsSsr.d.ts → src/local-first/useIsSsr.d.ts} +0 -0
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Evolu for React
2
2
 
3
- [Evolu](https://github.com/evoluhq/evolu) for [React](https://react.dev).
3
+ This package provides universal [React](https://react.dev) functionality for Evolu that works across all React environments (excluding React Native and React Web, which have their own packages).
4
4
 
5
5
  ## Documentation
6
6
 
@@ -12,4 +12,4 @@ The Evolu community is on [GitHub Discussions](https://github.com/evoluhq/evolu/
12
12
 
13
13
  To chat with other community members, you can join the [Evolu Discord](https://discord.gg/2J8yyyyxtZ).
14
14
 
15
- [![Twitter URL](https://img.shields.io/twitter/url/https/twitter.com/evoluhq.svg?style=social&label=Follow%20%40evoluhq)](https://twitter.com/evoluhq)
15
+ [![X](https://img.shields.io/twitter/url/https/x.com/evoluhq.svg?style=social&label=Follow%20%40evoluhq)](https://x.com/evoluhq)
@@ -0,0 +1,42 @@
1
+ import { type Run } from "@evolu/common";
2
+ import { type ReactNode } from "react";
3
+ /**
4
+ * Creates typed React Context and hook for {@link Run}.
5
+ *
6
+ * The `run` argument is used to infer the deps type for the returned API.
7
+ *
8
+ * `useRun` throws when the provider is missing.
9
+ *
10
+ * ### Example
11
+ *
12
+ * ```tsx
13
+ * const run = createRun(createEvoluDeps());
14
+ * const { RunContext, useRun } = createRunBinding(run);
15
+ *
16
+ * <RunContext value={run}>
17
+ * <App />
18
+ * </RunContext>;
19
+ *
20
+ * // In a component
21
+ * const run = useRun();
22
+ * ```
23
+ *
24
+ * ### Testing
25
+ *
26
+ * ```tsx
27
+ * const testRun = testCreateRun({ api: testApi });
28
+ * const { RunContext } = createRunBinding(testRun);
29
+ *
30
+ * <RunContext value={testRun}>
31
+ * <MyComponent />
32
+ * </RunContext>;
33
+ * ```
34
+ */
35
+ export declare const createRunBinding: <D>(run: Run<D>) => {
36
+ readonly RunContext: React.FC<{
37
+ readonly value: Run<D>;
38
+ readonly children?: ReactNode;
39
+ }>;
40
+ readonly useRun: () => Run<D>;
41
+ };
42
+ //# sourceMappingURL=Task.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Task.d.ts","sourceRoot":"","sources":["../../src/Task.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAU,KAAK,GAAG,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAsB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAE3D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,eAAO,MAAM,gBAAgB,GAAI,CAAC,EAEhC,KAAK,GAAG,CAAC,CAAC,CAAC,KACV;IACD,QAAQ,CAAC,UAAU,EAAE,KAAK,CAAC,EAAE,CAAC;QAC5B,QAAQ,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;QACvB,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,CAAC;KAC/B,CAAC,CAAC;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC;CAY/B,CAAC"}
@@ -0,0 +1,48 @@
1
+ "use client";
2
+ import { assert } from "@evolu/common";
3
+ import { createContext, use } from "react";
4
+ /**
5
+ * Creates typed React Context and hook for {@link Run}.
6
+ *
7
+ * The `run` argument is used to infer the deps type for the returned API.
8
+ *
9
+ * `useRun` throws when the provider is missing.
10
+ *
11
+ * ### Example
12
+ *
13
+ * ```tsx
14
+ * const run = createRun(createEvoluDeps());
15
+ * const { RunContext, useRun } = createRunBinding(run);
16
+ *
17
+ * <RunContext value={run}>
18
+ * <App />
19
+ * </RunContext>;
20
+ *
21
+ * // In a component
22
+ * const run = useRun();
23
+ * ```
24
+ *
25
+ * ### Testing
26
+ *
27
+ * ```tsx
28
+ * const testRun = testCreateRun({ api: testApi });
29
+ * const { RunContext } = createRunBinding(testRun);
30
+ *
31
+ * <RunContext value={testRun}>
32
+ * <MyComponent />
33
+ * </RunContext>;
34
+ * ```
35
+ */
36
+ export const createRunBinding = (
37
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
38
+ run) => {
39
+ const RunContext = createContext(null);
40
+ return {
41
+ RunContext,
42
+ useRun: () => {
43
+ const run = use(RunContext);
44
+ assert(run, "RunContext is missing.");
45
+ return run;
46
+ },
47
+ };
48
+ };
@@ -0,0 +1,4 @@
1
+ export * from "./local-first/createEvoluBinding.js";
2
+ export * from "./local-first/useIsSsr.js";
3
+ export * from "./Task.js";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qCAAqC,CAAC;AACpD,cAAc,2BAA2B,CAAC;AAC1C,cAAc,WAAW,CAAC"}
@@ -0,0 +1,3 @@
1
+ export * from "./local-first/createEvoluBinding.js";
2
+ export * from "./local-first/useIsSsr.js";
3
+ export * from "./Task.js";
@@ -0,0 +1,64 @@
1
+ import type { Evolu, EvoluSchema, Queries, QueriesToQueryRows, QueriesToQueryRowsPromises, Query, QueryRows, Row } from "@evolu/common/local-first";
2
+ import { type ReactNode } from "react";
3
+ export interface ReactBinding<S extends EvoluSchema = EvoluSchema> {
4
+ /** Provides {@link Evolu} to React descendants consumed via `useEvolu`. */
5
+ readonly EvoluContext: React.FC<{
6
+ readonly value: Evolu<S>;
7
+ readonly children?: ReactNode;
8
+ }>;
9
+ /** Returns the current {@link Evolu} instance from `EvoluContext`. */
10
+ readonly useEvolu: () => Evolu<S>;
11
+ /**
12
+ * Load and subscribe to the Query, and return an object with `rows` and `row`
13
+ * properties that are automatically updated when data changes.
14
+ *
15
+ * Note that `useQuery` uses React Suspense. It means every usage of
16
+ * `useQuery` blocks rendering until loading is completed. To avoid loading
17
+ * waterfall with more queries, use `useQueries`.
18
+ *
19
+ * The `promise` option allows preloading queries before rendering, which can
20
+ * be useful for complex queries that might take noticeable time even with
21
+ * local data. However, this is rarely needed as local queries are typically
22
+ * fast.
23
+ */
24
+ readonly useQuery: <R extends Row>(query: Query<S, R>, options?: Partial<{
25
+ /** Without subscribing to changes. */
26
+ readonly once: boolean;
27
+ /** Reuse existing promise instead of loading so query will not suspense. */
28
+ readonly promise: Promise<QueryRows<R>>;
29
+ }>) => QueryRows<R>;
30
+ /**
31
+ * The same as `useQuery`, but for many queries.
32
+ *
33
+ * The number of queries must remain stable across renders.
34
+ */
35
+ readonly useQueries: <Q extends Queries<S>, OQ extends Queries<S>>(queries: [...Q], options?: Partial<{
36
+ /** Queries that should be only loaded, not subscribed to. */
37
+ readonly once: [...OQ];
38
+ /** Reuse existing promises instead of loading so query will not suspense. */
39
+ readonly promises: [
40
+ ...QueriesToQueryRowsPromises<Q>,
41
+ ...QueriesToQueryRowsPromises<OQ>
42
+ ];
43
+ }>) => [...QueriesToQueryRows<Q>, ...QueriesToQueryRows<OQ>];
44
+ /** Subscribe to {@link Query} {@link QueryRows} changes. */
45
+ readonly useQuerySubscription: <R extends Row>(query: Query<S, R>, options?: Partial<{
46
+ /**
47
+ * Only subscribe and get the current value once. Subscribed query will
48
+ * not invoke React Suspense after a mutation.
49
+ */
50
+ readonly once: boolean;
51
+ }>) => QueryRows<R>;
52
+ /** Calls {@link Evolu.useOwner} on the current {@link Evolu} instance. */
53
+ readonly useOwner: (owner: Parameters<Evolu<S>["useOwner"]>[0], transports?: Parameters<Evolu<S>["useOwner"]>[1]) => ReturnType<Evolu<S>["useOwner"]>;
54
+ }
55
+ /**
56
+ * Creates a React binding for a specific {@link EvoluSchema}.
57
+ *
58
+ * The created binding contains the context component and all hooks needed by
59
+ * React components using an {@link Evolu} instance created from that schema.
60
+ *
61
+ * TODO: Example from playground example.
62
+ */
63
+ export declare const createEvoluBinding: <S extends EvoluSchema>(schema: S) => ReactBinding<S>;
64
+ //# sourceMappingURL=createEvoluBinding.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createEvoluBinding.d.ts","sourceRoot":"","sources":["../../../src/local-first/createEvoluBinding.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,KAAK,EACL,WAAW,EACX,OAAO,EACP,kBAAkB,EAClB,0BAA0B,EAC1B,KAAK,EACL,SAAS,EACT,GAAG,EACJ,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAOL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAGf,MAAM,WAAW,YAAY,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW;IAC/D,2EAA2E;IAC3E,QAAQ,CAAC,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC;QAC9B,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;QACzB,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,CAAC;KAC/B,CAAC,CAAC;IAEH,sEAAsE;IACtE,QAAQ,CAAC,QAAQ,EAAE,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC;IAElC;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC,SAAS,GAAG,EAC/B,KAAK,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAClB,OAAO,CAAC,EAAE,OAAO,CAAC;QAChB,sCAAsC;QACtC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;QAEvB,4EAA4E;QAC5E,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;KACzC,CAAC,KACC,SAAS,CAAC,CAAC,CAAC,CAAC;IAElB;;;;OAIG;IACH,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC,SAAS,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,SAAS,OAAO,CAAC,CAAC,CAAC,EAC/D,OAAO,EAAE,CAAC,GAAG,CAAC,CAAC,EACf,OAAO,CAAC,EAAE,OAAO,CAAC;QAChB,6DAA6D;QAC7D,QAAQ,CAAC,IAAI,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC;QAEvB,6EAA6E;QAC7E,QAAQ,CAAC,QAAQ,EAAE;YACjB,GAAG,0BAA0B,CAAC,CAAC,CAAC;YAChC,GAAG,0BAA0B,CAAC,EAAE,CAAC;SAClC,CAAC;KACH,CAAC,KACC,CAAC,GAAG,kBAAkB,CAAC,CAAC,CAAC,EAAE,GAAG,kBAAkB,CAAC,EAAE,CAAC,CAAC,CAAC;IAE3D,4DAA4D;IAC5D,QAAQ,CAAC,oBAAoB,EAAE,CAAC,CAAC,SAAS,GAAG,EAC3C,KAAK,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAClB,OAAO,CAAC,EAAE,OAAO,CAAC;QAChB;;;WAGG;QACH,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;KACxB,CAAC,KACC,SAAS,CAAC,CAAC,CAAC,CAAC;IAElB,0EAA0E;IAC1E,QAAQ,CAAC,QAAQ,EAAE,CACjB,KAAK,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,EAC1C,UAAU,CAAC,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,KAC7C,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC;CACvC;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,GAAI,CAAC,SAAS,WAAW,EAEtD,QAAQ,CAAC,KACR,YAAY,CAAC,CAAC,CAiGhB,CAAC"}
@@ -0,0 +1,76 @@
1
+ "use client";
2
+ import { assert, emptyArray, lazyVoid } from "@evolu/common";
3
+ import { createContext, use, useEffect, useMemo, useRef, useSyncExternalStore, } from "react";
4
+ import { useIsSsr } from "./useIsSsr.js";
5
+ /**
6
+ * Creates a React binding for a specific {@link EvoluSchema}.
7
+ *
8
+ * The created binding contains the context component and all hooks needed by
9
+ * React components using an {@link Evolu} instance created from that schema.
10
+ *
11
+ * TODO: Example from playground example.
12
+ */
13
+ export const createEvoluBinding = (
14
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
15
+ schema) => {
16
+ const EvoluContext = createContext(null);
17
+ const useEvolu = () => {
18
+ const evolu = use(EvoluContext);
19
+ assert(evolu, "EvoluContext is missing.");
20
+ return evolu;
21
+ };
22
+ const useQuerySubscription = (query, options = {}) => {
23
+ const evolu = useEvolu();
24
+ const { once } = useRef(options).current;
25
+ if (once) {
26
+ /* eslint-disable react-hooks/rules-of-hooks */
27
+ useEffect(() => evolu.subscribeQuery(query)(lazyVoid), [evolu, query]);
28
+ return evolu.getQueryRows(query);
29
+ }
30
+ return useSyncExternalStore(useMemo(() => evolu.subscribeQuery(query), [evolu, query]), useMemo(() => () => evolu.getQueryRows(query), [evolu, query]), () => emptyArray);
31
+ };
32
+ const useQuery = (query, options = {}) => {
33
+ const evolu = useEvolu();
34
+ const isSSR = useIsSsr();
35
+ if (isSSR) {
36
+ if (!options.promise)
37
+ void evolu.loadQuery(query);
38
+ }
39
+ else {
40
+ use(options.promise ?? evolu.loadQuery(query));
41
+ }
42
+ return useQuerySubscription(query, options);
43
+ };
44
+ const useQueries = (queries, options = {}) => {
45
+ const evolu = useEvolu();
46
+ const once = useRef(options).current.once;
47
+ const allQueries = once ? queries.concat(once) : queries;
48
+ const wasSSR = useIsSsr();
49
+ if (wasSSR) {
50
+ if (!options.promises)
51
+ void evolu.loadQueries(allQueries);
52
+ }
53
+ else {
54
+ if (options.promises)
55
+ options.promises.map(use);
56
+ else
57
+ evolu.loadQueries(allQueries).map(use);
58
+ }
59
+ return allQueries.map((query, index) =>
60
+ // Safe until the number of queries is stable.
61
+ // eslint-disable-next-line react-hooks/rules-of-hooks
62
+ useQuerySubscription(query, { once: index > queries.length - 1 }));
63
+ };
64
+ const useOwner = (owner, transports) => {
65
+ const evolu = useEvolu();
66
+ return evolu.useOwner(owner, transports);
67
+ };
68
+ return {
69
+ EvoluContext,
70
+ useEvolu,
71
+ useQuery,
72
+ useQueries,
73
+ useQuerySubscription,
74
+ useOwner,
75
+ };
76
+ };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useIsSsr.d.ts","sourceRoot":"","sources":["../../../src/local-first/useIsSsr.ts"],"names":[],"mappings":"AAMA;;;;GAIG;AACH,eAAO,MAAM,QAAQ,QAAO,OAE+B,CAAC"}
@@ -1,4 +1,4 @@
1
- import { constFalse, constTrue } from "@evolu/common";
1
+ import { lazyFalse, lazyTrue } from "@evolu/common";
2
2
  import { useSyncExternalStore } from "react";
3
3
  // eslint-disable-next-line @typescript-eslint/no-empty-function
4
4
  const emptySubscribe = () => () => { };
@@ -7,7 +7,6 @@ const emptySubscribe = () => () => { };
7
7
  *
8
8
  * @see https://kurtextrem.de/posts/react-uses-hydration
9
9
  */
10
- export const useIsSsr = () => {
11
- // TODO: Consider useDeferredValue(isSSRSync);
12
- return useSyncExternalStore(emptySubscribe, constFalse, constTrue);
13
- };
10
+ export const useIsSsr = () =>
11
+ // TODO: Consider useDeferredValue(isSSRSync);
12
+ useSyncExternalStore(emptySubscribe, lazyFalse, lazyTrue);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evolu/react",
3
- "version": "10.4.0",
3
+ "version": "11.0.0-next.0",
4
4
  "description": "Evolu for React",
5
5
  "keywords": [
6
6
  "evolu",
@@ -15,44 +15,42 @@
15
15
  },
16
16
  "homepage": "https://evolu.dev",
17
17
  "type": "module",
18
- "types": "./dist/index.d.ts",
18
+ "types": "./dist/src/index.d.ts",
19
19
  "exports": {
20
20
  ".": {
21
- "types": "./dist/index.d.ts",
22
- "import": "./dist/index.js",
23
- "browser": "./dist/index.js",
24
- "react-native": "./dist/index.js"
21
+ "types": "./dist/src/index.d.ts",
22
+ "import": "./dist/src/index.js",
23
+ "browser": "./dist/src/index.js",
24
+ "react-native": "./dist/src/index.js"
25
25
  }
26
26
  },
27
27
  "files": [
28
- "dist/**",
28
+ "dist/src/**",
29
29
  "src/**",
30
30
  "README.md"
31
31
  ],
32
+ "scripts": {
33
+ "build": "tsc --build tsconfig.build.json",
34
+ "prepack": "npm run build",
35
+ "format": "prettier --write \"src/*.{ts,tsx,md}\""
36
+ },
32
37
  "devDependencies": {
33
- "@types/react": "~19.1.13",
34
- "@types/react-dom": "~19.1.9",
35
- "react": "19.1.0",
36
- "typescript": "^5.9.2",
37
- "vitest": "^4.0.4",
38
- "@evolu/common": "7.4.0",
39
- "@evolu/tsconfig": "0.0.2"
38
+ "@evolu/common": "8.0.0-next.0",
39
+ "@evolu/typescript-config": "*",
40
+ "@types/react": "^19.2.14",
41
+ "@types/react-dom": "^19.2.3",
42
+ "react": "^19.2.4",
43
+ "typescript": "^5.9.3"
40
44
  },
41
45
  "peerDependencies": {
42
- "@evolu/common": "^7.4.0",
46
+ "@evolu/common": "^8.0.0-next.0",
43
47
  "react": ">=19"
44
48
  },
45
49
  "publishConfig": {
46
50
  "access": "public"
47
51
  },
48
52
  "engines": {
49
- "node": ">=22.0.0"
53
+ "node": ">=24.0.0"
50
54
  },
51
- "sideEffects": [],
52
- "scripts": {
53
- "dev": "tsc --watch",
54
- "build": "rimraf dist && tsc",
55
- "clean": "rimraf .turbo node_modules dist",
56
- "format": "prettier --write \"src/*.{ts,tsx,md}\""
57
- }
58
- }
55
+ "sideEffects": false
56
+ }
package/src/Task.tsx ADDED
@@ -0,0 +1,58 @@
1
+ "use client";
2
+
3
+ import { assert, type Run } from "@evolu/common";
4
+ import { createContext, use, type ReactNode } from "react";
5
+
6
+ /**
7
+ * Creates typed React Context and hook for {@link Run}.
8
+ *
9
+ * The `run` argument is used to infer the deps type for the returned API.
10
+ *
11
+ * `useRun` throws when the provider is missing.
12
+ *
13
+ * ### Example
14
+ *
15
+ * ```tsx
16
+ * const run = createRun(createEvoluDeps());
17
+ * const { RunContext, useRun } = createRunBinding(run);
18
+ *
19
+ * <RunContext value={run}>
20
+ * <App />
21
+ * </RunContext>;
22
+ *
23
+ * // In a component
24
+ * const run = useRun();
25
+ * ```
26
+ *
27
+ * ### Testing
28
+ *
29
+ * ```tsx
30
+ * const testRun = testCreateRun({ api: testApi });
31
+ * const { RunContext } = createRunBinding(testRun);
32
+ *
33
+ * <RunContext value={testRun}>
34
+ * <MyComponent />
35
+ * </RunContext>;
36
+ * ```
37
+ */
38
+ export const createRunBinding = <D,>(
39
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
40
+ run: Run<D>,
41
+ ): {
42
+ readonly RunContext: React.FC<{
43
+ readonly value: Run<D>;
44
+ readonly children?: ReactNode;
45
+ }>;
46
+ readonly useRun: () => Run<D>;
47
+ } => {
48
+ const RunContext = createContext<Run<D> | null>(null);
49
+
50
+ return {
51
+ RunContext,
52
+ useRun: () => {
53
+ const run = use(RunContext);
54
+ assert(run, "RunContext is missing.");
55
+ return run;
56
+ },
57
+ };
58
+ };
package/src/index.ts CHANGED
@@ -1,11 +1,3 @@
1
- export * from "./createUseEvolu.js";
2
- export * from "./EvoluContext.js";
3
- export * from "./EvoluProvider.js";
4
- export * from "./useEvolu.js";
5
- export * from "./useEvoluError.js";
6
- export * from "./useOwner.js";
7
- export * from "./useQueries.js";
8
- export * from "./useQuery.js";
9
- export * from "./useQuerySubscription.js";
10
- // export * from "./useSyncState.js"; TODO: Update it for the owner-api
11
- export * from "./useIsSsr.js";
1
+ export * from "./local-first/createEvoluBinding.js";
2
+ export * from "./local-first/useIsSsr.js";
3
+ export * from "./Task.js";
@@ -0,0 +1,205 @@
1
+ "use client";
2
+
3
+ import { assert, emptyArray, lazyVoid } from "@evolu/common";
4
+ import type {
5
+ Evolu,
6
+ EvoluSchema,
7
+ Queries,
8
+ QueriesToQueryRows,
9
+ QueriesToQueryRowsPromises,
10
+ Query,
11
+ QueryRows,
12
+ Row,
13
+ } from "@evolu/common/local-first";
14
+ import {
15
+ createContext,
16
+ use,
17
+ useEffect,
18
+ useMemo,
19
+ useRef,
20
+ useSyncExternalStore,
21
+ type ReactNode,
22
+ } from "react";
23
+ import { useIsSsr } from "./useIsSsr.js";
24
+
25
+ export interface ReactBinding<S extends EvoluSchema = EvoluSchema> {
26
+ /** Provides {@link Evolu} to React descendants consumed via `useEvolu`. */
27
+ readonly EvoluContext: React.FC<{
28
+ readonly value: Evolu<S>;
29
+ readonly children?: ReactNode;
30
+ }>;
31
+
32
+ /** Returns the current {@link Evolu} instance from `EvoluContext`. */
33
+ readonly useEvolu: () => Evolu<S>;
34
+
35
+ /**
36
+ * Load and subscribe to the Query, and return an object with `rows` and `row`
37
+ * properties that are automatically updated when data changes.
38
+ *
39
+ * Note that `useQuery` uses React Suspense. It means every usage of
40
+ * `useQuery` blocks rendering until loading is completed. To avoid loading
41
+ * waterfall with more queries, use `useQueries`.
42
+ *
43
+ * The `promise` option allows preloading queries before rendering, which can
44
+ * be useful for complex queries that might take noticeable time even with
45
+ * local data. However, this is rarely needed as local queries are typically
46
+ * fast.
47
+ */
48
+ readonly useQuery: <R extends Row>(
49
+ query: Query<S, R>,
50
+ options?: Partial<{
51
+ /** Without subscribing to changes. */
52
+ readonly once: boolean;
53
+
54
+ /** Reuse existing promise instead of loading so query will not suspense. */
55
+ readonly promise: Promise<QueryRows<R>>;
56
+ }>,
57
+ ) => QueryRows<R>;
58
+
59
+ /**
60
+ * The same as `useQuery`, but for many queries.
61
+ *
62
+ * The number of queries must remain stable across renders.
63
+ */
64
+ readonly useQueries: <Q extends Queries<S>, OQ extends Queries<S>>(
65
+ queries: [...Q],
66
+ options?: Partial<{
67
+ /** Queries that should be only loaded, not subscribed to. */
68
+ readonly once: [...OQ];
69
+
70
+ /** Reuse existing promises instead of loading so query will not suspense. */
71
+ readonly promises: [
72
+ ...QueriesToQueryRowsPromises<Q>,
73
+ ...QueriesToQueryRowsPromises<OQ>,
74
+ ];
75
+ }>,
76
+ ) => [...QueriesToQueryRows<Q>, ...QueriesToQueryRows<OQ>];
77
+
78
+ /** Subscribe to {@link Query} {@link QueryRows} changes. */
79
+ readonly useQuerySubscription: <R extends Row>(
80
+ query: Query<S, R>,
81
+ options?: Partial<{
82
+ /**
83
+ * Only subscribe and get the current value once. Subscribed query will
84
+ * not invoke React Suspense after a mutation.
85
+ */
86
+ readonly once: boolean;
87
+ }>,
88
+ ) => QueryRows<R>;
89
+
90
+ /** Calls {@link Evolu.useOwner} on the current {@link Evolu} instance. */
91
+ readonly useOwner: (
92
+ owner: Parameters<Evolu<S>["useOwner"]>[0],
93
+ transports?: Parameters<Evolu<S>["useOwner"]>[1],
94
+ ) => ReturnType<Evolu<S>["useOwner"]>;
95
+ }
96
+
97
+ /**
98
+ * Creates a React binding for a specific {@link EvoluSchema}.
99
+ *
100
+ * The created binding contains the context component and all hooks needed by
101
+ * React components using an {@link Evolu} instance created from that schema.
102
+ *
103
+ * TODO: Example from playground example.
104
+ */
105
+ export const createEvoluBinding = <S extends EvoluSchema>(
106
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
107
+ schema: S,
108
+ ): ReactBinding<S> => {
109
+ const EvoluContext = createContext<Evolu<S> | null>(null);
110
+
111
+ const useEvolu = (): Evolu<S> => {
112
+ const evolu = use(EvoluContext);
113
+ assert(evolu, "EvoluContext is missing.");
114
+ return evolu;
115
+ };
116
+
117
+ const useQuerySubscription = <R extends Row>(
118
+ query: Query<S, R>,
119
+ options: Partial<{
120
+ readonly once: boolean;
121
+ }> = {},
122
+ ): QueryRows<R> => {
123
+ const evolu = useEvolu();
124
+ const { once } = useRef(options).current;
125
+
126
+ if (once) {
127
+ /* eslint-disable react-hooks/rules-of-hooks */
128
+ useEffect(() => evolu.subscribeQuery(query)(lazyVoid), [evolu, query]);
129
+ return evolu.getQueryRows(query);
130
+ }
131
+
132
+ return useSyncExternalStore(
133
+ useMemo(() => evolu.subscribeQuery(query), [evolu, query]),
134
+ useMemo(() => () => evolu.getQueryRows(query), [evolu, query]),
135
+ () => emptyArray as QueryRows<R>,
136
+ /* eslint-enable react-hooks/rules-of-hooks */
137
+ );
138
+ };
139
+
140
+ const useQuery = <R extends Row>(
141
+ query: Query<S, R>,
142
+ options: Partial<{
143
+ readonly once: boolean;
144
+ readonly promise: Promise<QueryRows<R>>;
145
+ }> = {},
146
+ ): QueryRows<R> => {
147
+ const evolu = useEvolu();
148
+ const isSSR = useIsSsr();
149
+
150
+ if (isSSR) {
151
+ if (!options.promise) void evolu.loadQuery(query);
152
+ } else {
153
+ use(options.promise ?? evolu.loadQuery(query));
154
+ }
155
+
156
+ return useQuerySubscription(query, options);
157
+ };
158
+
159
+ const useQueries = <Q extends Queries<S>, OQ extends Queries<S>>(
160
+ queries: [...Q],
161
+ options: Partial<{
162
+ readonly once: [...OQ];
163
+ readonly promises: [
164
+ ...QueriesToQueryRowsPromises<Q>,
165
+ ...QueriesToQueryRowsPromises<OQ>,
166
+ ];
167
+ }> = {},
168
+ ): [...QueriesToQueryRows<Q>, ...QueriesToQueryRows<OQ>] => {
169
+ const evolu = useEvolu();
170
+ const once = useRef(options).current.once;
171
+ const allQueries = once ? queries.concat(once) : queries;
172
+
173
+ const wasSSR = useIsSsr();
174
+ if (wasSSR) {
175
+ if (!options.promises) void evolu.loadQueries(allQueries);
176
+ } else {
177
+ if (options.promises) options.promises.map(use);
178
+ else evolu.loadQueries(allQueries).map(use);
179
+ }
180
+
181
+ return allQueries.map((query, index) =>
182
+ // Safe until the number of queries is stable.
183
+ // eslint-disable-next-line react-hooks/rules-of-hooks
184
+ useQuerySubscription(query, { once: index > queries.length - 1 }),
185
+ ) as never;
186
+ };
187
+
188
+ const useOwner = (
189
+ owner: Parameters<Evolu<S>["useOwner"]>[0],
190
+ transports?: Parameters<Evolu<S>["useOwner"]>[1],
191
+ ): ReturnType<Evolu<S>["useOwner"]> => {
192
+ const evolu = useEvolu();
193
+
194
+ return evolu.useOwner(owner, transports);
195
+ };
196
+
197
+ return {
198
+ EvoluContext,
199
+ useEvolu,
200
+ useQuery,
201
+ useQueries,
202
+ useQuerySubscription,
203
+ useOwner,
204
+ };
205
+ };
@@ -1,4 +1,4 @@
1
- import { constFalse, constTrue } from "@evolu/common";
1
+ import { lazyFalse, lazyTrue } from "@evolu/common";
2
2
  import { useSyncExternalStore } from "react";
3
3
 
4
4
  // eslint-disable-next-line @typescript-eslint/no-empty-function
@@ -9,7 +9,6 @@ const emptySubscribe = () => () => {};
9
9
  *
10
10
  * @see https://kurtextrem.de/posts/react-uses-hydration
11
11
  */
12
- export const useIsSsr = (): boolean => {
12
+ export const useIsSsr = (): boolean =>
13
13
  // TODO: Consider useDeferredValue(isSSRSync);
14
- return useSyncExternalStore(emptySubscribe, constFalse, constTrue);
15
- };
14
+ useSyncExternalStore(emptySubscribe, lazyFalse, lazyTrue);