@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.
- package/README.md +2 -2
- package/dist/src/Task.d.ts +43 -0
- package/dist/src/Task.d.ts.map +1 -0
- package/dist/src/Task.js +47 -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 +72 -0
- package/dist/src/local-first/createEvoluBinding.d.ts.map +1 -0
- package/dist/src/local-first/createEvoluBinding.js +82 -0
- package/dist/src/local-first/useIsSsr.d.ts.map +1 -0
- package/dist/{useIsSsr.js → src/local-first/useIsSsr.js} +3 -4
- package/package.json +21 -21
- package/src/Task.tsx +60 -0
- package/src/index.ts +3 -11
- package/src/local-first/createEvoluBinding.tsx +212 -0
- package/src/{useIsSsr.ts → local-first/useIsSsr.ts} +2 -3
- 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,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"}
|
package/dist/src/Task.js
ADDED
|
@@ -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 @@
|
|
|
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,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
|
-
|
|
12
|
-
|
|
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": "
|
|
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":
|
|
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": "
|
|
34
|
-
"@types/react-dom": "
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"@evolu/
|
|
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": "^
|
|
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": ">=
|
|
51
|
+
"node": ">=24.0.0"
|
|
50
52
|
},
|
|
51
|
-
"sideEffects":
|
|
53
|
+
"sideEffects": false,
|
|
52
54
|
"scripts": {
|
|
53
|
-
"
|
|
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 "./
|
|
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.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
|
+
};
|