@evolu/react 10.3.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.
- package/README.md +2 -2
- package/dist/src/Task.d.ts +42 -0
- package/dist/src/Task.d.ts.map +1 -0
- package/dist/src/Task.js +48 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +3 -0
- package/dist/src/local-first/createEvoluBinding.d.ts +64 -0
- package/dist/src/local-first/createEvoluBinding.d.ts.map +1 -0
- package/dist/src/local-first/createEvoluBinding.js +76 -0
- package/dist/src/local-first/useIsSsr.d.ts.map +1 -0
- package/dist/{useIsSsr.js → src/local-first/useIsSsr.js} +4 -5
- package/package.json +22 -25
- package/src/Task.tsx +58 -0
- package/src/index.ts +3 -11
- package/src/local-first/createEvoluBinding.tsx +205 -0
- package/src/{useIsSsr.ts → local-first/useIsSsr.ts} +3 -4
- package/LICENSE +0 -21
- package/dist/EvoluContext.d.ts +0 -3
- package/dist/EvoluContext.d.ts.map +0 -1
- package/dist/EvoluContext.js +0 -2
- package/dist/EvoluProvider.d.ts +0 -7
- package/dist/EvoluProvider.d.ts.map +0 -1
- package/dist/EvoluProvider.js +0 -4
- package/dist/createUseEvolu.d.ts +0 -13
- package/dist/createUseEvolu.d.ts.map +0 -1
- package/dist/createUseEvolu.js +0 -14
- package/dist/index.d.ts +0 -11
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -11
- package/dist/useEvolu.d.ts +0 -9
- package/dist/useEvolu.d.ts.map +0 -1
- package/dist/useEvolu.js +0 -15
- package/dist/useEvoluError.d.ts +0 -4
- package/dist/useEvoluError.d.ts.map +0 -1
- package/dist/useEvoluError.js +0 -8
- package/dist/useIsSsr.d.ts.map +0 -1
- package/dist/useOwner.d.ts +0 -9
- package/dist/useOwner.d.ts.map +0 -1
- package/dist/useOwner.js +0 -16
- package/dist/useQueries.d.ts +0 -13
- package/dist/useQueries.d.ts.map +0 -1
- package/dist/useQueries.js +0 -31
- package/dist/useQuery.d.ts +0 -37
- package/dist/useQuery.d.ts.map +0 -1
- package/dist/useQuery.js +0 -45
- package/dist/useQuerySubscription.d.ts +0 -10
- package/dist/useQuerySubscription.d.ts.map +0 -1
- package/dist/useQuerySubscription.js +0 -18
- package/dist/useSyncState.d.ts +0 -4
- package/dist/useSyncState.d.ts.map +0 -1
- package/dist/useSyncState.js +0 -11
- package/src/EvoluContext.ts +0 -4
- package/src/EvoluProvider.tsx +0 -13
- package/src/createUseEvolu.ts +0 -17
- package/src/useEvolu.ts +0 -20
- package/src/useEvoluError.ts +0 -9
- package/src/useOwner.ts +0 -18
- package/src/useQueries.ts +0 -52
- package/src/useQuery.ts +0 -57
- package/src/useQuerySubscription.ts +0 -38
- package/src/useSyncState.ts +0 -13
- /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
|
-
[
|
|
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
|
-
[](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"}
|
package/dist/src/Task.js
ADDED
|
@@ -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 @@
|
|
|
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,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 {
|
|
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
|
-
|
|
12
|
-
|
|
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": "
|
|
3
|
+
"version": "11.0.0-next.0",
|
|
4
4
|
"description": "Evolu for React",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"evolu",
|
|
@@ -15,45 +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
|
-
"@
|
|
34
|
-
"@
|
|
35
|
-
"react": "19.
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"@evolu/common": "7.3.0",
|
|
40
|
-
"@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"
|
|
41
44
|
},
|
|
42
45
|
"peerDependencies": {
|
|
43
|
-
"@evolu/common": "^
|
|
46
|
+
"@evolu/common": "^8.0.0-next.0",
|
|
44
47
|
"react": ">=19"
|
|
45
48
|
},
|
|
46
49
|
"publishConfig": {
|
|
47
50
|
"access": "public"
|
|
48
51
|
},
|
|
49
52
|
"engines": {
|
|
50
|
-
"node": ">=
|
|
53
|
+
"node": ">=24.0.0"
|
|
51
54
|
},
|
|
52
|
-
"sideEffects":
|
|
53
|
-
|
|
54
|
-
"dev": "tsc --watch",
|
|
55
|
-
"build": "shx rm -rf dist && tsc",
|
|
56
|
-
"clean": "shx rm -rf .turbo node_modules dist",
|
|
57
|
-
"format": "prettier --write \"src/*.{ts,tsx,md}\""
|
|
58
|
-
}
|
|
59
|
-
}
|
|
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 "./
|
|
2
|
-
export * from "./
|
|
3
|
-
export * from "./
|
|
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 {
|
|
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
|
-
|
|
15
|
-
};
|
|
14
|
+
useSyncExternalStore(emptySubscribe, lazyFalse, lazyTrue);
|