@evolu/react 10.4.0 → 11.0.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 (62) hide show
  1. package/README.md +2 -2
  2. package/dist/src/Task.d.ts +43 -0
  3. package/dist/src/Task.d.ts.map +1 -0
  4. package/dist/src/Task.js +47 -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 +72 -0
  9. package/dist/src/local-first/createEvoluBinding.d.ts.map +1 -0
  10. package/dist/src/local-first/createEvoluBinding.js +82 -0
  11. package/dist/src/local-first/useIsSsr.d.ts.map +1 -0
  12. package/dist/{useIsSsr.js → src/local-first/useIsSsr.js} +3 -4
  13. package/package.json +21 -21
  14. package/src/Task.tsx +60 -0
  15. package/src/index.ts +3 -11
  16. package/src/local-first/createEvoluBinding.tsx +212 -0
  17. package/src/{useIsSsr.ts → local-first/useIsSsr.ts} +2 -3
  18. package/dist/EvoluContext.d.ts +0 -3
  19. package/dist/EvoluContext.d.ts.map +0 -1
  20. package/dist/EvoluContext.js +0 -2
  21. package/dist/EvoluProvider.d.ts +0 -7
  22. package/dist/EvoluProvider.d.ts.map +0 -1
  23. package/dist/EvoluProvider.js +0 -4
  24. package/dist/createUseEvolu.d.ts +0 -13
  25. package/dist/createUseEvolu.d.ts.map +0 -1
  26. package/dist/createUseEvolu.js +0 -14
  27. package/dist/index.d.ts +0 -11
  28. package/dist/index.d.ts.map +0 -1
  29. package/dist/index.js +0 -11
  30. package/dist/useEvolu.d.ts +0 -9
  31. package/dist/useEvolu.d.ts.map +0 -1
  32. package/dist/useEvolu.js +0 -15
  33. package/dist/useEvoluError.d.ts +0 -4
  34. package/dist/useEvoluError.d.ts.map +0 -1
  35. package/dist/useEvoluError.js +0 -8
  36. package/dist/useIsSsr.d.ts.map +0 -1
  37. package/dist/useOwner.d.ts +0 -9
  38. package/dist/useOwner.d.ts.map +0 -1
  39. package/dist/useOwner.js +0 -16
  40. package/dist/useQueries.d.ts +0 -13
  41. package/dist/useQueries.d.ts.map +0 -1
  42. package/dist/useQueries.js +0 -31
  43. package/dist/useQuery.d.ts +0 -37
  44. package/dist/useQuery.d.ts.map +0 -1
  45. package/dist/useQuery.js +0 -45
  46. package/dist/useQuerySubscription.d.ts +0 -10
  47. package/dist/useQuerySubscription.d.ts.map +0 -1
  48. package/dist/useQuerySubscription.js +0 -18
  49. package/dist/useSyncState.d.ts +0 -4
  50. package/dist/useSyncState.d.ts.map +0 -1
  51. package/dist/useSyncState.js +0 -11
  52. package/src/EvoluContext.ts +0 -4
  53. package/src/EvoluProvider.tsx +0 -13
  54. package/src/createUseEvolu.ts +0 -17
  55. package/src/useEvolu.ts +0 -20
  56. package/src/useEvoluError.ts +0 -9
  57. package/src/useOwner.ts +0 -18
  58. package/src/useQueries.ts +0 -52
  59. package/src/useQuery.ts +0 -57
  60. package/src/useQuerySubscription.ts +0 -38
  61. package/src/useSyncState.ts +0 -13
  62. /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,43 @@
1
+ import { type DisposableRun, 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 {@link DisposableRun} type argument is used to infer the deps type for the
7
+ * returned API, which exposes only {@link Run}.
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<typeof 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<typeof testRun>();
30
+ *
31
+ * <RunContext value={testRun}>
32
+ * <MyComponent />
33
+ * </RunContext>;
34
+ * ```
35
+ */
36
+ export declare const createRunBinding: <R extends DisposableRun<any> = DisposableRun<unknown>>() => {
37
+ readonly RunContext: React.FC<{
38
+ readonly value: Run<R extends DisposableRun<infer D> ? D : never>;
39
+ readonly children?: ReactNode;
40
+ }>;
41
+ readonly useRun: () => Run<R extends DisposableRun<infer D> ? D : never>;
42
+ };
43
+ //# 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,aAAa,EAAE,KAAK,GAAG,EAAE,MAAM,eAAe,CAAC;AACrE,OAAO,EAAsB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAE3D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,gBAAgB,GAC3B,CAAC,SAAS,aAAa,CAAC,GAAG,CAAC,GAAG,aAAa,CAAC,OAAO,CAAC,OAClD;IACH,QAAQ,CAAC,UAAU,EAAE,KAAK,CAAC,EAAE,CAAC;QAC5B,QAAQ,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,SAAS,aAAa,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC;QAClE,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,CAAC;KAC/B,CAAC,CAAC;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,CAAC,CAAC,SAAS,aAAa,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC;CAc1E,CAAC"}
@@ -0,0 +1,47 @@
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 {@link DisposableRun} type argument is used to infer the deps type for the
8
+ * returned API, which exposes only {@link Run}.
9
+ *
10
+ * `useRun` throws when the provider is missing.
11
+ *
12
+ * ### Example
13
+ *
14
+ * ```tsx
15
+ * const run = createRun(createEvoluDeps());
16
+ * const { RunContext, useRun } = createRunBinding<typeof run>();
17
+ *
18
+ * <RunContext value={run}>
19
+ * <App />
20
+ * </RunContext>;
21
+ *
22
+ * // In a component
23
+ * const run = useRun();
24
+ * ```
25
+ *
26
+ * ### Testing
27
+ *
28
+ * ```tsx
29
+ * const testRun = testCreateRun({ api: testApi });
30
+ * const { RunContext } = createRunBinding<typeof testRun>();
31
+ *
32
+ * <RunContext value={testRun}>
33
+ * <MyComponent />
34
+ * </RunContext>;
35
+ * ```
36
+ */
37
+ export const createRunBinding = () => {
38
+ const RunContext = createContext(null);
39
+ return {
40
+ RunContext,
41
+ useRun: () => {
42
+ const run = use(RunContext);
43
+ assert(run, "RunContext is missing.");
44
+ return run;
45
+ },
46
+ };
47
+ };
@@ -0,0 +1,4 @@
1
+ export * from "./local-first/createEvoluBinding.tsx";
2
+ export * from "./local-first/useIsSsr.ts";
3
+ export * from "./Task.tsx";
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,sCAAsC,CAAC;AACrD,cAAc,2BAA2B,CAAC;AAC1C,cAAc,YAAY,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,72 @@
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
+ * The {@link EvoluSchema} type argument configures the binding without passing
62
+ * the schema at runtime.
63
+ *
64
+ * ### Example
65
+ *
66
+ * ```tsx
67
+ * const { EvoluContext, useEvolu, useQuery } =
68
+ * createEvoluBinding<typeof AppSchema>();
69
+ * ```
70
+ */
71
+ export declare const createEvoluBinding: <S extends EvoluSchema = EvoluSchema>() => ReactBinding<S>;
72
+ //# 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;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,kBAAkB,GAC7B,CAAC,SAAS,WAAW,GAAG,WAAW,OAChC,YAAY,CAAC,CAAC,CAiGlB,CAAC"}
@@ -0,0 +1,82 @@
1
+ "use client";
2
+ import { constVoid, assert, emptyArray } 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
+ * The {@link EvoluSchema} type argument configures the binding without passing
12
+ * the schema at runtime.
13
+ *
14
+ * ### Example
15
+ *
16
+ * ```tsx
17
+ * const { EvoluContext, useEvolu, useQuery } =
18
+ * createEvoluBinding<typeof AppSchema>();
19
+ * ```
20
+ */
21
+ export const createEvoluBinding = () => {
22
+ const EvoluContext = createContext(null);
23
+ const useEvolu = () => {
24
+ const evolu = use(EvoluContext);
25
+ assert(evolu, "EvoluContext is missing.");
26
+ return evolu;
27
+ };
28
+ const useQuerySubscription = (query, options = {}) => {
29
+ const evolu = useEvolu();
30
+ const { once } = useRef(options).current;
31
+ if (once) {
32
+ /* eslint-disable react-hooks/rules-of-hooks */
33
+ useEffect(() => evolu.subscribeQuery(query)(constVoid), [evolu, query]);
34
+ return evolu.getQueryRows(query);
35
+ }
36
+ return useSyncExternalStore(useMemo(() => evolu.subscribeQuery(query), [evolu, query]), useMemo(() => () => evolu.getQueryRows(query), [evolu, query]), () => emptyArray);
37
+ };
38
+ const useQuery = (query, options = {}) => {
39
+ const evolu = useEvolu();
40
+ const isSSR = useIsSsr();
41
+ if (isSSR) {
42
+ if (!options.promise)
43
+ void evolu.loadQuery(query);
44
+ }
45
+ else {
46
+ use(options.promise ?? evolu.loadQuery(query));
47
+ }
48
+ return useQuerySubscription(query, options);
49
+ };
50
+ const useQueries = (queries, options = {}) => {
51
+ const evolu = useEvolu();
52
+ const once = useRef(options).current.once;
53
+ const allQueries = once ? queries.concat(once) : queries;
54
+ const wasSSR = useIsSsr();
55
+ if (wasSSR) {
56
+ if (!options.promises)
57
+ void evolu.loadQueries(allQueries);
58
+ }
59
+ else {
60
+ if (options.promises)
61
+ options.promises.map(use);
62
+ else
63
+ evolu.loadQueries(allQueries).map(use);
64
+ }
65
+ return allQueries.map((query, index) =>
66
+ // Safe until the number of queries is stable.
67
+ // eslint-disable-next-line react-hooks/rules-of-hooks
68
+ useQuerySubscription(query, { once: index > queries.length - 1 }));
69
+ };
70
+ const useOwner = (owner, transports) => {
71
+ const evolu = useEvolu();
72
+ return evolu.useOwner(owner, transports);
73
+ };
74
+ return {
75
+ EvoluContext,
76
+ useEvolu,
77
+ useQuery,
78
+ useQueries,
79
+ useQuerySubscription,
80
+ useOwner,
81
+ };
82
+ };
@@ -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,OAEiC,CAAC"}
@@ -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, constFalse, constTrue);
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",
4
4
  "description": "Evolu for React",
5
5
  "keywords": [
6
6
  "evolu",
@@ -9,50 +9,50 @@
9
9
  ],
10
10
  "author": "Daniel Steigerwald <daniel@steigerwald.cz>",
11
11
  "license": "MIT",
12
- "repository": "evoluhq/evolu",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/evoluhq/evolu.git"
15
+ },
13
16
  "bugs": {
14
17
  "url": "https://github.com/evoluhq/evolu/issues"
15
18
  },
16
19
  "homepage": "https://evolu.dev",
17
20
  "type": "module",
18
- "types": "./dist/index.d.ts",
21
+ "types": "./dist/src/index.d.ts",
19
22
  "exports": {
20
23
  ".": {
21
- "types": "./dist/index.d.ts",
22
- "import": "./dist/index.js",
23
- "browser": "./dist/index.js",
24
- "react-native": "./dist/index.js"
24
+ "types": "./dist/src/index.d.ts",
25
+ "import": "./dist/src/index.js",
26
+ "browser": "./dist/src/index.js",
27
+ "react-native": "./dist/src/index.js"
25
28
  }
26
29
  },
27
30
  "files": [
28
- "dist/**",
31
+ "dist/src/**",
29
32
  "src/**",
30
33
  "README.md"
31
34
  ],
32
35
  "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"
36
+ "@types/react": "^19.2.18",
37
+ "@types/react-dom": "^19.2.4",
38
+ "@typescript/native": "npm:typescript@^7.0.2",
39
+ "react": "19.2.3",
40
+ "@evolu/common": "8.0.0",
41
+ "@evolu/typescript-config": "0.0.2"
40
42
  },
41
43
  "peerDependencies": {
42
- "@evolu/common": "^7.4.0",
44
+ "@evolu/common": "^8.0.0-next.0",
43
45
  "react": ">=19"
44
46
  },
45
47
  "publishConfig": {
46
48
  "access": "public"
47
49
  },
48
50
  "engines": {
49
- "node": ">=22.0.0"
51
+ "node": ">=24.0.0"
50
52
  },
51
- "sideEffects": [],
53
+ "sideEffects": false,
52
54
  "scripts": {
53
- "dev": "tsc --watch",
54
- "build": "rimraf dist && tsc",
55
- "clean": "rimraf .turbo node_modules dist",
55
+ "build": "tsc --build tsconfig.build.json",
56
56
  "format": "prettier --write \"src/*.{ts,tsx,md}\""
57
57
  }
58
58
  }
package/src/Task.tsx ADDED
@@ -0,0 +1,60 @@
1
+ "use client";
2
+
3
+ import { assert, type DisposableRun, 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 {@link DisposableRun} type argument is used to infer the deps type for the
10
+ * returned API, which exposes only {@link Run}.
11
+ *
12
+ * `useRun` throws when the provider is missing.
13
+ *
14
+ * ### Example
15
+ *
16
+ * ```tsx
17
+ * const run = createRun(createEvoluDeps());
18
+ * const { RunContext, useRun } = createRunBinding<typeof run>();
19
+ *
20
+ * <RunContext value={run}>
21
+ * <App />
22
+ * </RunContext>;
23
+ *
24
+ * // In a component
25
+ * const run = useRun();
26
+ * ```
27
+ *
28
+ * ### Testing
29
+ *
30
+ * ```tsx
31
+ * const testRun = testCreateRun({ api: testApi });
32
+ * const { RunContext } = createRunBinding<typeof testRun>();
33
+ *
34
+ * <RunContext value={testRun}>
35
+ * <MyComponent />
36
+ * </RunContext>;
37
+ * ```
38
+ */
39
+ export const createRunBinding = <
40
+ R extends DisposableRun<any> = DisposableRun<unknown>,
41
+ >(): {
42
+ readonly RunContext: React.FC<{
43
+ readonly value: Run<R extends DisposableRun<infer D> ? D : never>;
44
+ readonly children?: ReactNode;
45
+ }>;
46
+ readonly useRun: () => Run<R extends DisposableRun<infer D> ? D : never>;
47
+ } => {
48
+ type D = R extends DisposableRun<infer D> ? D : never;
49
+
50
+ const RunContext = createContext<Run<D> | null>(null);
51
+
52
+ return {
53
+ RunContext,
54
+ useRun: () => {
55
+ const run = use(RunContext);
56
+ assert(run, "RunContext is missing.");
57
+ return run;
58
+ },
59
+ };
60
+ };
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.tsx";
2
+ export * from "./local-first/useIsSsr.ts";
3
+ export * from "./Task.tsx";
@@ -0,0 +1,212 @@
1
+ "use client";
2
+
3
+ import { constVoid, assert, emptyArray } 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.ts";
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
+ * The {@link EvoluSchema} type argument configures the binding without passing
104
+ * the schema at runtime.
105
+ *
106
+ * ### Example
107
+ *
108
+ * ```tsx
109
+ * const { EvoluContext, useEvolu, useQuery } =
110
+ * createEvoluBinding<typeof AppSchema>();
111
+ * ```
112
+ */
113
+ export const createEvoluBinding = <
114
+ S extends EvoluSchema = EvoluSchema,
115
+ >(): ReactBinding<S> => {
116
+ const EvoluContext = createContext<Evolu<S> | null>(null);
117
+
118
+ const useEvolu = (): Evolu<S> => {
119
+ const evolu = use(EvoluContext);
120
+ assert(evolu, "EvoluContext is missing.");
121
+ return evolu;
122
+ };
123
+
124
+ const useQuerySubscription = <R extends Row>(
125
+ query: Query<S, R>,
126
+ options: Partial<{
127
+ readonly once: boolean;
128
+ }> = {},
129
+ ): QueryRows<R> => {
130
+ const evolu = useEvolu();
131
+ const { once } = useRef(options).current;
132
+
133
+ if (once) {
134
+ /* eslint-disable react-hooks/rules-of-hooks */
135
+ useEffect(() => evolu.subscribeQuery(query)(constVoid), [evolu, query]);
136
+ return evolu.getQueryRows(query);
137
+ }
138
+
139
+ return useSyncExternalStore(
140
+ useMemo(() => evolu.subscribeQuery(query), [evolu, query]),
141
+ useMemo(() => () => evolu.getQueryRows(query), [evolu, query]),
142
+ () => emptyArray as QueryRows<R>,
143
+ /* eslint-enable react-hooks/rules-of-hooks */
144
+ );
145
+ };
146
+
147
+ const useQuery = <R extends Row>(
148
+ query: Query<S, R>,
149
+ options: Partial<{
150
+ readonly once: boolean;
151
+ readonly promise: Promise<QueryRows<R>>;
152
+ }> = {},
153
+ ): QueryRows<R> => {
154
+ const evolu = useEvolu();
155
+ const isSSR = useIsSsr();
156
+
157
+ if (isSSR) {
158
+ if (!options.promise) void evolu.loadQuery(query);
159
+ } else {
160
+ use(options.promise ?? evolu.loadQuery(query));
161
+ }
162
+
163
+ return useQuerySubscription(query, options);
164
+ };
165
+
166
+ const useQueries = <Q extends Queries<S>, OQ extends Queries<S>>(
167
+ queries: [...Q],
168
+ options: Partial<{
169
+ readonly once: [...OQ];
170
+ readonly promises: [
171
+ ...QueriesToQueryRowsPromises<Q>,
172
+ ...QueriesToQueryRowsPromises<OQ>,
173
+ ];
174
+ }> = {},
175
+ ): [...QueriesToQueryRows<Q>, ...QueriesToQueryRows<OQ>] => {
176
+ const evolu = useEvolu();
177
+ const once = useRef(options).current.once;
178
+ const allQueries = once ? queries.concat(once) : queries;
179
+
180
+ const wasSSR = useIsSsr();
181
+ if (wasSSR) {
182
+ if (!options.promises) void evolu.loadQueries(allQueries);
183
+ } else {
184
+ if (options.promises) options.promises.map(use);
185
+ else evolu.loadQueries(allQueries).map(use);
186
+ }
187
+
188
+ return allQueries.map((query, index) =>
189
+ // Safe until the number of queries is stable.
190
+ // eslint-disable-next-line react-hooks/rules-of-hooks
191
+ useQuerySubscription(query, { once: index > queries.length - 1 }),
192
+ ) as never;
193
+ };
194
+
195
+ const useOwner = (
196
+ owner: Parameters<Evolu<S>["useOwner"]>[0],
197
+ transports?: Parameters<Evolu<S>["useOwner"]>[1],
198
+ ): ReturnType<Evolu<S>["useOwner"]> => {
199
+ const evolu = useEvolu();
200
+
201
+ return evolu.useOwner(owner, transports);
202
+ };
203
+
204
+ return {
205
+ EvoluContext,
206
+ useEvolu,
207
+ useQuery,
208
+ useQueries,
209
+ useQuerySubscription,
210
+ useOwner,
211
+ };
212
+ };