@confect/react 9.0.0-next.5 → 9.0.0-next.7

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/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # @confect/react
2
2
 
3
+ ## 9.0.0-next.7
4
+
5
+ ### Patch Changes
6
+
7
+ - 5d19484: Stabilize the identity of `@confect/react` hook results across renders.
8
+
9
+ `useQuery` previously decoded and wrapped the Convex result on every render, handing consumers a brand new `QueryResult` even when the underlying Convex data was unchanged. Effects and memoization that depend on the result's identity (e.g. `useEffect(..., [user])` derived via `QueryResult.match`) would re-run on every render, which could escalate into `Maximum update depth exceeded`. The decoded `QueryResult` is now memoized by the (referentially stable) Convex result, so unchanged data keeps a stable identity.
10
+
11
+ `useMutation` and `useAction` now return a stable callback via `useCallback`, matching the identity contract of Convex's own hooks, instead of allocating a fresh function each render.
12
+
13
+ `Ref.getFunctionReference` now caches the Convex function reference by function name, so repeated calls for the same ref return the same reference.
14
+
15
+ - Updated dependencies [5d19484]
16
+ - @confect/core@9.0.0-next.7
17
+
18
+ ## 9.0.0-next.6
19
+
20
+ ### Patch Changes
21
+
22
+ - Updated dependencies [46045a9]
23
+ - Updated dependencies [762f7eb]
24
+ - @confect/core@9.0.0-next.6
25
+
3
26
  ## 9.0.0-next.5
4
27
 
5
28
  ### Patch Changes
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";;;;;KAYY,YAAA,cAA0B,GAAA,CAAI,GAAA,KAAQ,GAAA,CAAI,KAAA,CAAM,IAAA,qBAGxD,OAAA,CAAQ,GAAA,CAAI,OAAA,CAAQ,IAAA,KACpB,OAAA,CAAQ,MAAA,CAAO,MAAA,CAAO,GAAA,CAAI,OAAA,CAAQ,IAAA,GAAO,GAAA,CAAI,KAAA,CAAM,IAAA;AAAA,KAElD,YAAA,eAA2B,GAAA,CAAI,cAAA,UAC5B,GAAA,CAAI,IAAA,CAAK,KAAA,mBACV,IAAA,GAAO,GAAA,CAAI,IAAA,CAAK,KAAA,eAChB,IAAA,EAAM,GAAA,CAAI,IAAA,CAAK,KAAA;AAAA,cAET,QAAA,iBAA0B,GAAA,CAAI,cAAA,EACzC,GAAA,EAAK,KAAA,KACF,IAAA,EAAM,YAAA,CAAa,KAAA,MACrB,WAAA,CAAwB,GAAA,CAAI,OAAA,CAAQ,KAAA,GAAQ,GAAA,CAAI,KAAA,CAAM,KAAA;;;;;;;;;;;;;;cA6C5C,WAAA,oBAAgC,GAAA,CAAI,iBAAA,EAC/C,GAAA,EAAK,QAAA,UACC,IAAA,EAAM,GAAA,CAAI,YAAA,CAAa,QAAA,MAAc,YAAA,CAAa,QAAA;;;;;;;;;;;;;;cA2B7C,SAAA,kBAA4B,GAAA,CAAI,eAAA,EAC3C,GAAA,EAAK,MAAA,UACC,IAAA,EAAM,GAAA,CAAI,YAAA,CAAa,MAAA,MAAY,YAAA,CAAa,MAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";;;;;KAaY,YAAA,cAA0B,GAAA,CAAI,GAAA,KAAQ,GAAA,CAAI,KAAA,CAAM,IAAA,qBAGxD,OAAA,CAAQ,GAAA,CAAI,OAAA,CAAQ,IAAA,KACpB,OAAA,CAAQ,MAAA,CAAO,MAAA,CAAO,GAAA,CAAI,OAAA,CAAQ,IAAA,GAAO,GAAA,CAAI,KAAA,CAAM,IAAA;AAAA,KAElD,YAAA,eAA2B,GAAA,CAAI,cAAA,UAC5B,GAAA,CAAI,IAAA,CAAK,KAAA,mBACV,IAAA,GAAO,GAAA,CAAI,IAAA,CAAK,KAAA,eAChB,IAAA,EAAM,GAAA,CAAI,IAAA,CAAK,KAAA;AAAA,cAET,QAAA,iBAA0B,GAAA,CAAI,cAAA,EACzC,GAAA,EAAK,KAAA,KACF,IAAA,EAAM,YAAA,CAAa,KAAA,MACrB,WAAA,CAAwB,GAAA,CAAI,OAAA,CAAQ,KAAA,GAAQ,GAAA,CAAI,KAAA,CAAM,KAAA;;;;;;;;;;;;;;cA2D5C,WAAA,oBAAgC,GAAA,CAAI,iBAAA,EAC/C,GAAA,EAAK,QAAA,UACC,IAAA,EAAM,GAAA,CAAI,YAAA,CAAa,QAAA,MAAc,YAAA,CAAa,QAAA;;;;;;;;;;;;;;cA8B7C,SAAA,kBAA4B,GAAA,CAAI,eAAA,EAC3C,GAAA,EAAK,MAAA,UACC,IAAA,EAAM,GAAA,CAAI,YAAA,CAAa,MAAA,MAAY,YAAA,CAAa,MAAA"}
package/dist/index.js CHANGED
@@ -2,23 +2,29 @@ import { QueryResult_exports, fail, load, succeed } from "./QueryResult.js";
2
2
  import { Ref } from "@confect/core";
3
3
  import { useAction as useAction$1, useMutation as useMutation$1, useQuery as useQuery$1 } from "convex/react";
4
4
  import { Cause, Effect, Either, Exit, Option } from "effect";
5
+ import { useCallback, useMemo } from "react";
5
6
 
6
7
  //#region src/index.ts
7
8
  const useQuery = (ref, ...rest) => {
8
9
  const functionReference = Ref.getFunctionReference(ref);
9
10
  const args = rest[0];
10
- const encodedArgs = args === "skip" ? "skip" : Ref.encodeArgsSync(ref, args ?? {});
11
- try {
12
- const encodedReturnsOrUndefined = useQuery$1(functionReference, encodedArgs);
13
- if (encodedReturnsOrUndefined === void 0) return load(args === "skip");
14
- return succeed(Ref.decodeReturnsSync(ref, encodedReturnsOrUndefined));
15
- } catch (error) {
16
- if (Ref.isConvexError(error)) {
17
- const decoded = Ref.decodeErrorSync(ref, error.data);
18
- if (Option.isSome(decoded)) return fail(decoded.value);
11
+ const skipped = args === "skip";
12
+ const encodedArgs = skipped ? "skip" : Ref.encodeArgsSync(ref, args ?? {});
13
+ const encodedReturnsOrError = Either.try(() => useQuery$1(functionReference, encodedArgs));
14
+ return useMemo(() => Either.match(encodedReturnsOrError, {
15
+ onRight: (encodedReturnsOrUndefined) => encodedReturnsOrUndefined === void 0 ? load(skipped) : succeed(Ref.decodeReturnsSync(ref, encodedReturnsOrUndefined)),
16
+ onLeft: (error) => {
17
+ if (Ref.isConvexError(error)) {
18
+ const decoded = Ref.decodeErrorSync(ref, error.data);
19
+ if (Option.isSome(decoded)) return fail(decoded.value);
20
+ }
21
+ throw error;
19
22
  }
20
- throw error;
21
- }
23
+ }), [
24
+ ref,
25
+ skipped,
26
+ Either.merge(encodedReturnsOrError)
27
+ ]);
22
28
  };
23
29
  /**
24
30
  * Returns a function that invokes the provided `Ref`'s mutation.
@@ -35,7 +41,7 @@ const useQuery = (ref, ...rest) => {
35
41
  */
36
42
  const useMutation = (ref) => {
37
43
  const actualMutation = useMutation$1(Ref.getFunctionReference(ref));
38
- return ((...args) => invokeAsEither(ref, (_, encodedArgs) => actualMutation(encodedArgs), args).then((either) => Ref.hasErrorSchema(ref) ? either : Either.getOrThrow(either)));
44
+ return useCallback(((...args) => invokeAsEither(ref, (_, encodedArgs) => actualMutation(encodedArgs), args).then((either) => Ref.hasErrorSchema(ref) ? either : Either.getOrThrow(either))), [ref, actualMutation]);
39
45
  };
40
46
  /**
41
47
  * Returns a function that invokes the provided `Ref`'s action.
@@ -52,7 +58,7 @@ const useMutation = (ref) => {
52
58
  */
53
59
  const useAction = (ref) => {
54
60
  const actualAction = useAction$1(Ref.getFunctionReference(ref));
55
- return ((...args) => invokeAsEither(ref, (_, encodedArgs) => actualAction(encodedArgs), args).then((either) => Ref.hasErrorSchema(ref) ? either : Either.getOrThrow(either)));
61
+ return useCallback(((...args) => invokeAsEither(ref, (_, encodedArgs) => actualAction(encodedArgs), args).then((either) => Ref.hasErrorSchema(ref) ? either : Either.getOrThrow(either))), [ref, actualAction]);
56
62
  };
57
63
  const invokeAsEither = async (ref, invoke, args) => {
58
64
  const exit = await Effect.runPromiseExit(Ref.runWithCodec(ref, args[0] ?? {}, invoke).pipe(Effect.catchTag("ParseError", Effect.die), Effect.either));
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["useConvexQuery","QueryResult.load","QueryResult.succeed","QueryResult.fail","useConvexMutation","useConvexAction"],"sources":["../src/index.ts"],"sourcesContent":["import { Ref } from \"@confect/core\";\nimport {\n useAction as useConvexAction,\n useMutation as useConvexMutation,\n useQuery as useConvexQuery,\n} from \"convex/react\";\nimport { Cause, Effect, Either, Exit, Option } from \"effect\";\n\nimport * as QueryResult from \"./QueryResult\";\n\nexport { QueryResult };\n\nexport type InvokeReturn<Ref_ extends Ref.Any> = [Ref.Error<Ref_>] extends [\n never,\n]\n ? Promise<Ref.Returns<Ref_>>\n : Promise<Either.Either<Ref.Returns<Ref_>, Ref.Error<Ref_>>>;\n\ntype UseQueryArgs<Query extends Ref.AnyPublicQuery> =\n keyof Ref.Args<Query> extends never\n ? [args?: Ref.Args<Query> | \"skip\"]\n : [args: Ref.Args<Query> | \"skip\"];\n\nexport const useQuery = <Query extends Ref.AnyPublicQuery>(\n ref: Query,\n ...rest: UseQueryArgs<Query>\n): QueryResult.QueryResult<Ref.Returns<Query>, Ref.Error<Query>> => {\n const functionReference = Ref.getFunctionReference(ref);\n const args = rest[0];\n const encodedArgs =\n args === \"skip\"\n ? \"skip\"\n : Ref.encodeArgsSync(ref, (args ?? {}) as Ref.Args<Query>);\n\n try {\n const encodedReturnsOrUndefined = useConvexQuery(\n functionReference,\n encodedArgs,\n );\n\n if (encodedReturnsOrUndefined === undefined) {\n return QueryResult.load(args === \"skip\");\n }\n\n return QueryResult.succeed(\n Ref.decodeReturnsSync(ref, encodedReturnsOrUndefined),\n );\n } catch (error) {\n if (Ref.isConvexError(error)) {\n const decoded = Ref.decodeErrorSync(ref, error.data);\n if (Option.isSome(decoded)) {\n return QueryResult.fail(decoded.value);\n }\n }\n throw error;\n }\n};\n\n/**\n * Returns a function that invokes the provided `Ref`'s mutation.\n *\n * If the `Ref` declares an `error` schema, the returned promise resolves to an\n * `Either` with the decoded `returns` value on the right and the decoded error\n * on the left.\n *\n * If the `Ref` does not declare an `error` schema, the promise resolves\n * directly to the decoded `returns` value, matching the behavior of\n * `useMutation` from `convex/react`.\n *\n * Any other failure rejects the promise.\n */\nexport const useMutation = <Mutation extends Ref.AnyPublicMutation>(\n ref: Mutation,\n): ((...args: Ref.OptionalArgs<Mutation>) => InvokeReturn<Mutation>) => {\n const functionReference = Ref.getFunctionReference(ref);\n const actualMutation = useConvexMutation(functionReference);\n\n return ((...args: Ref.OptionalArgs<Mutation>) =>\n invokeAsEither(\n ref,\n (_, encodedArgs) => actualMutation(encodedArgs),\n args,\n ).then((either) =>\n Ref.hasErrorSchema(ref) ? either : Either.getOrThrow(either),\n )) as (...args: Ref.OptionalArgs<Mutation>) => InvokeReturn<Mutation>;\n};\n\n/**\n * Returns a function that invokes the provided `Ref`'s action.\n *\n * If the `Ref` declares an `error` schema, the returned promise resolves to an\n * `Either` with the decoded `returns` value on the right and the decoded error\n * on the left.\n *\n * If the `Ref` does not declare an `error` schema, the promise resolves\n * directly to the decoded `returns` value, matching the behavior of\n * `useMutation` from `convex/react`.\n *\n * Any other failure rejects the promise.\n */\nexport const useAction = <Action extends Ref.AnyPublicAction>(\n ref: Action,\n): ((...args: Ref.OptionalArgs<Action>) => InvokeReturn<Action>) => {\n const functionReference = Ref.getFunctionReference(ref);\n const actualAction = useConvexAction(functionReference);\n\n return ((...args: Ref.OptionalArgs<Action>) =>\n invokeAsEither(\n ref,\n (_, encodedArgs) => actualAction(encodedArgs),\n args,\n ).then((either) =>\n Ref.hasErrorSchema(ref) ? either : Either.getOrThrow(either),\n )) as (...args: Ref.OptionalArgs<Action>) => InvokeReturn<Action>;\n};\n\nconst invokeAsEither = async <Ref_ extends Ref.Any>(\n ref: Ref_,\n invoke: (\n fnRef: Ref.FunctionReference<Ref_>,\n encodedArgs: unknown,\n ) => PromiseLike<unknown>,\n args: Ref.OptionalArgs<Ref_>,\n): Promise<Either.Either<Ref.Returns<Ref_>, Ref.Error<Ref_>>> => {\n const exit = await Effect.runPromiseExit(\n Ref.runWithCodec(ref, (args[0] ?? {}) as Ref.Args<Ref_>, invoke).pipe(\n Effect.catchTag(\"ParseError\", Effect.die),\n Effect.either,\n ),\n );\n if (Exit.isSuccess(exit)) return exit.value;\n throw Cause.squash(exit.cause);\n};\n"],"mappings":";;;;;;AAuBA,MAAa,YACX,KACA,GAAG,SAC+D;CAClE,MAAM,oBAAoB,IAAI,qBAAqB,IAAI;CACvD,MAAM,OAAO,KAAK;CAClB,MAAM,cACJ,SAAS,SACL,SACA,IAAI,eAAe,KAAM,QAAQ,EAAE,CAAqB;AAE9D,KAAI;EACF,MAAM,4BAA4BA,WAChC,mBACA,YACD;AAED,MAAI,8BAA8B,OAChC,QAAOC,KAAiB,SAAS,OAAO;AAG1C,SAAOC,QACL,IAAI,kBAAkB,KAAK,0BAA0B,CACtD;UACM,OAAO;AACd,MAAI,IAAI,cAAc,MAAM,EAAE;GAC5B,MAAM,UAAU,IAAI,gBAAgB,KAAK,MAAM,KAAK;AACpD,OAAI,OAAO,OAAO,QAAQ,CACxB,QAAOC,KAAiB,QAAQ,MAAM;;AAG1C,QAAM;;;;;;;;;;;;;;;;AAiBV,MAAa,eACX,QACsE;CAEtE,MAAM,iBAAiBC,cADG,IAAI,qBAAqB,IAAI,CACI;AAE3D,UAAS,GAAG,SACV,eACE,MACC,GAAG,gBAAgB,eAAe,YAAY,EAC/C,KACD,CAAC,MAAM,WACN,IAAI,eAAe,IAAI,GAAG,SAAS,OAAO,WAAW,OAAO,CAC7D;;;;;;;;;;;;;;;AAgBL,MAAa,aACX,QACkE;CAElE,MAAM,eAAeC,YADK,IAAI,qBAAqB,IAAI,CACA;AAEvD,UAAS,GAAG,SACV,eACE,MACC,GAAG,gBAAgB,aAAa,YAAY,EAC7C,KACD,CAAC,MAAM,WACN,IAAI,eAAe,IAAI,GAAG,SAAS,OAAO,WAAW,OAAO,CAC7D;;AAGL,MAAM,iBAAiB,OACrB,KACA,QAIA,SAC+D;CAC/D,MAAM,OAAO,MAAM,OAAO,eACxB,IAAI,aAAa,KAAM,KAAK,MAAM,EAAE,EAAqB,OAAO,CAAC,KAC/D,OAAO,SAAS,cAAc,OAAO,IAAI,EACzC,OAAO,OACR,CACF;AACD,KAAI,KAAK,UAAU,KAAK,CAAE,QAAO,KAAK;AACtC,OAAM,MAAM,OAAO,KAAK,MAAM"}
1
+ {"version":3,"file":"index.js","names":["useConvexQuery","QueryResult.load","QueryResult.succeed","QueryResult.fail","useConvexMutation","useConvexAction"],"sources":["../src/index.ts"],"sourcesContent":["import { Ref } from \"@confect/core\";\nimport {\n useAction as useConvexAction,\n useMutation as useConvexMutation,\n useQuery as useConvexQuery,\n} from \"convex/react\";\nimport { Cause, Effect, Either, Exit, Option } from \"effect\";\nimport { useCallback, useMemo } from \"react\";\n\nimport * as QueryResult from \"./QueryResult\";\n\nexport { QueryResult };\n\nexport type InvokeReturn<Ref_ extends Ref.Any> = [Ref.Error<Ref_>] extends [\n never,\n]\n ? Promise<Ref.Returns<Ref_>>\n : Promise<Either.Either<Ref.Returns<Ref_>, Ref.Error<Ref_>>>;\n\ntype UseQueryArgs<Query extends Ref.AnyPublicQuery> =\n keyof Ref.Args<Query> extends never\n ? [args?: Ref.Args<Query> | \"skip\"]\n : [args: Ref.Args<Query> | \"skip\"];\n\nexport const useQuery = <Query extends Ref.AnyPublicQuery>(\n ref: Query,\n ...rest: UseQueryArgs<Query>\n): QueryResult.QueryResult<Ref.Returns<Query>, Ref.Error<Query>> => {\n const functionReference = Ref.getFunctionReference(ref);\n const args = rest[0];\n const skipped = args === \"skip\";\n const encodedArgs = skipped\n ? \"skip\"\n : Ref.encodeArgsSync(ref, (args ?? {}) as Ref.Args<Query>);\n\n // `useConvexQuery` returns a referentially stable value while the underlying\n // Convex result is unchanged, and throws a stable error when the query\n // fails. We capture either outcome as an `Either` and decode/wrap it inside\n // `useMemo` so that the returned `QueryResult` keeps a stable identity across\n // renders when nothing has actually changed. Decoding on every render would\n // hand consumers a fresh object each time, breaking effects and memoization\n // that depend on the result's identity.\n const encodedReturnsOrError: Either.Either<unknown, unknown> = Either.try(\n () => useConvexQuery(functionReference, encodedArgs),\n );\n\n return useMemo(\n () =>\n Either.match(encodedReturnsOrError, {\n onRight: (encodedReturnsOrUndefined) =>\n encodedReturnsOrUndefined === undefined\n ? QueryResult.load(skipped)\n : QueryResult.succeed(\n Ref.decodeReturnsSync(ref, encodedReturnsOrUndefined),\n ),\n onLeft: (error) => {\n if (Ref.isConvexError(error)) {\n const decoded = Ref.decodeErrorSync(ref, error.data);\n if (Option.isSome(decoded)) {\n return QueryResult.fail(decoded.value);\n }\n }\n throw error;\n },\n }),\n // `Either.try` allocates a fresh wrapper each render, so we key the memo on\n // the stable value it carries (the Convex result or thrown error) rather\n // than the wrapper itself; the decoded result is a function of that value,\n // `ref`, and `skipped`.\n [ref, skipped, Either.merge(encodedReturnsOrError)],\n );\n};\n\n/**\n * Returns a function that invokes the provided `Ref`'s mutation.\n *\n * If the `Ref` declares an `error` schema, the returned promise resolves to an\n * `Either` with the decoded `returns` value on the right and the decoded error\n * on the left.\n *\n * If the `Ref` does not declare an `error` schema, the promise resolves\n * directly to the decoded `returns` value, matching the behavior of\n * `useMutation` from `convex/react`.\n *\n * Any other failure rejects the promise.\n */\nexport const useMutation = <Mutation extends Ref.AnyPublicMutation>(\n ref: Mutation,\n): ((...args: Ref.OptionalArgs<Mutation>) => InvokeReturn<Mutation>) => {\n const functionReference = Ref.getFunctionReference(ref);\n const actualMutation = useConvexMutation(functionReference);\n\n return useCallback(\n ((...args: Ref.OptionalArgs<Mutation>) =>\n invokeAsEither(\n ref,\n (_, encodedArgs) => actualMutation(encodedArgs),\n args,\n ).then((either) =>\n Ref.hasErrorSchema(ref) ? either : Either.getOrThrow(either),\n )) as (...args: Ref.OptionalArgs<Mutation>) => InvokeReturn<Mutation>,\n [ref, actualMutation],\n );\n};\n\n/**\n * Returns a function that invokes the provided `Ref`'s action.\n *\n * If the `Ref` declares an `error` schema, the returned promise resolves to an\n * `Either` with the decoded `returns` value on the right and the decoded error\n * on the left.\n *\n * If the `Ref` does not declare an `error` schema, the promise resolves\n * directly to the decoded `returns` value, matching the behavior of\n * `useMutation` from `convex/react`.\n *\n * Any other failure rejects the promise.\n */\nexport const useAction = <Action extends Ref.AnyPublicAction>(\n ref: Action,\n): ((...args: Ref.OptionalArgs<Action>) => InvokeReturn<Action>) => {\n const functionReference = Ref.getFunctionReference(ref);\n const actualAction = useConvexAction(functionReference);\n\n return useCallback(\n ((...args: Ref.OptionalArgs<Action>) =>\n invokeAsEither(\n ref,\n (_, encodedArgs) => actualAction(encodedArgs),\n args,\n ).then((either) =>\n Ref.hasErrorSchema(ref) ? either : Either.getOrThrow(either),\n )) as (...args: Ref.OptionalArgs<Action>) => InvokeReturn<Action>,\n [ref, actualAction],\n );\n};\n\nconst invokeAsEither = async <Ref_ extends Ref.Any>(\n ref: Ref_,\n invoke: (\n fnRef: Ref.FunctionReference<Ref_>,\n encodedArgs: unknown,\n ) => PromiseLike<unknown>,\n args: Ref.OptionalArgs<Ref_>,\n): Promise<Either.Either<Ref.Returns<Ref_>, Ref.Error<Ref_>>> => {\n const exit = await Effect.runPromiseExit(\n Ref.runWithCodec(ref, (args[0] ?? {}) as Ref.Args<Ref_>, invoke).pipe(\n Effect.catchTag(\"ParseError\", Effect.die),\n Effect.either,\n ),\n );\n if (Exit.isSuccess(exit)) return exit.value;\n throw Cause.squash(exit.cause);\n};\n"],"mappings":";;;;;;;AAwBA,MAAa,YACX,KACA,GAAG,SAC+D;CAClE,MAAM,oBAAoB,IAAI,qBAAqB,IAAI;CACvD,MAAM,OAAO,KAAK;CAClB,MAAM,UAAU,SAAS;CACzB,MAAM,cAAc,UAChB,SACA,IAAI,eAAe,KAAM,QAAQ,EAAE,CAAqB;CAS5D,MAAM,wBAAyD,OAAO,UAC9DA,WAAe,mBAAmB,YAAY,CACrD;AAED,QAAO,cAEH,OAAO,MAAM,uBAAuB;EAClC,UAAU,8BACR,8BAA8B,SAC1BC,KAAiB,QAAQ,GACzBC,QACE,IAAI,kBAAkB,KAAK,0BAA0B,CACtD;EACP,SAAS,UAAU;AACjB,OAAI,IAAI,cAAc,MAAM,EAAE;IAC5B,MAAM,UAAU,IAAI,gBAAgB,KAAK,MAAM,KAAK;AACpD,QAAI,OAAO,OAAO,QAAQ,CACxB,QAAOC,KAAiB,QAAQ,MAAM;;AAG1C,SAAM;;EAET,CAAC,EAKJ;EAAC;EAAK;EAAS,OAAO,MAAM,sBAAsB;EAAC,CACpD;;;;;;;;;;;;;;;AAgBH,MAAa,eACX,QACsE;CAEtE,MAAM,iBAAiBC,cADG,IAAI,qBAAqB,IAAI,CACI;AAE3D,QAAO,cACH,GAAG,SACH,eACE,MACC,GAAG,gBAAgB,eAAe,YAAY,EAC/C,KACD,CAAC,MAAM,WACN,IAAI,eAAe,IAAI,GAAG,SAAS,OAAO,WAAW,OAAO,CAC7D,GACH,CAAC,KAAK,eAAe,CACtB;;;;;;;;;;;;;;;AAgBH,MAAa,aACX,QACkE;CAElE,MAAM,eAAeC,YADK,IAAI,qBAAqB,IAAI,CACA;AAEvD,QAAO,cACH,GAAG,SACH,eACE,MACC,GAAG,gBAAgB,aAAa,YAAY,EAC7C,KACD,CAAC,MAAM,WACN,IAAI,eAAe,IAAI,GAAG,SAAS,OAAO,WAAW,OAAO,CAC7D,GACH,CAAC,KAAK,aAAa,CACpB;;AAGH,MAAM,iBAAiB,OACrB,KACA,QAIA,SAC+D;CAC/D,MAAM,OAAO,MAAM,OAAO,eACxB,IAAI,aAAa,KAAM,KAAK,MAAM,EAAE,EAAqB,OAAO,CAAC,KAC/D,OAAO,SAAS,cAAc,OAAO,IAAI,EACzC,OAAO,OACR,CACF;AACD,KAAI,KAAK,UAAU,KAAK,CAAE,QAAO,KAAK;AACtC,OAAM,MAAM,OAAO,KAAK,MAAM"}
package/package.json CHANGED
@@ -1,25 +1,34 @@
1
1
  {
2
2
  "name": "@confect/react",
3
- "version": "9.0.0-next.5",
4
3
  "description": "Client-side bindings for React apps",
5
- "repository": {
6
- "type": "git",
7
- "url": "https://github.com/rjdellecese/confect.git"
8
- },
4
+ "version": "9.0.0-next.7",
5
+ "author": "RJ Dellecese",
9
6
  "bugs": {
10
7
  "url": "https://github.com/rjdellecese/confect/issues"
11
8
  },
12
- "homepage": "https://confect.dev",
13
- "sideEffects": false,
14
- "type": "module",
15
- "files": [
16
- "CHANGELOG.md",
17
- "LICENSE",
18
- "README.md",
19
- "dist",
20
- "package.json",
21
- "src"
22
- ],
9
+ "devDependencies": {
10
+ "@effect/vitest": "0.29.0",
11
+ "@eslint/js": "10.0.1",
12
+ "@testing-library/react": "16.3.2",
13
+ "@types/node": "25.3.3",
14
+ "@types/react": "19.2.14",
15
+ "@types/react-dom": "19.2.3",
16
+ "convex": "1.39.1",
17
+ "effect": "3.21.2",
18
+ "eslint": "10.0.2",
19
+ "happy-dom": "15.11.7",
20
+ "prettier": "3.8.1",
21
+ "react": "19.2.4",
22
+ "react-dom": "19.2.4",
23
+ "tsdown": "0.20.3",
24
+ "typescript": "5.9.3",
25
+ "typescript-eslint": "8.56.1",
26
+ "vite": "7.3.1",
27
+ "vitest": "3.2.4"
28
+ },
29
+ "engines": {
30
+ "node": ">=22"
31
+ },
23
32
  "exports": {
24
33
  ".": {
25
34
  "types": "./dist/index.d.ts",
@@ -31,47 +40,43 @@
31
40
  },
32
41
  "./package.json": "./package.json"
33
42
  },
43
+ "files": [
44
+ "CHANGELOG.md",
45
+ "LICENSE",
46
+ "README.md",
47
+ "dist",
48
+ "package.json",
49
+ "src"
50
+ ],
51
+ "homepage": "https://confect.dev",
34
52
  "keywords": [
35
- "effect",
36
53
  "convex",
54
+ "effect",
37
55
  "react"
38
56
  ],
39
- "author": "RJ Dellecese",
40
57
  "license": "ISC",
41
- "devDependencies": {
42
- "@effect/vitest": "0.29.0",
43
- "@eslint/js": "10.0.1",
44
- "@types/node": "25.3.3",
45
- "convex": "1.39.1",
46
- "effect": "3.21.2",
47
- "eslint": "10.0.2",
48
- "prettier": "3.8.1",
49
- "tsdown": "0.20.3",
50
- "typescript": "5.9.3",
51
- "typescript-eslint": "8.56.1",
52
- "vite": "7.3.1",
53
- "vitest": "3.2.4"
54
- },
58
+ "main": "./dist/index.js",
59
+ "module": "./dist/index.js",
55
60
  "peerDependencies": {
56
61
  "convex": "^1.30.0",
57
62
  "effect": "^3.21.2",
58
63
  "react": "^18.0.0 || ^19.0.0",
59
- "@confect/core": "^9.0.0-next.5"
64
+ "@confect/core": "^9.0.0-next.7"
60
65
  },
61
- "engines": {
62
- "node": ">=22",
63
- "pnpm": ">=10"
66
+ "repository": {
67
+ "type": "git",
68
+ "url": "https://github.com/rjdellecese/confect.git"
64
69
  },
65
- "main": "./dist/index.js",
66
- "module": "./dist/index.js",
70
+ "sideEffects": false,
71
+ "type": "module",
67
72
  "types": "./dist/index.d.ts",
68
73
  "scripts": {
69
74
  "build": "tsdown --config-loader unrun",
75
+ "clean": "rm -rf dist coverage node_modules",
70
76
  "dev": "tsdown --watch",
71
- "typecheck": "tsc --noEmit --project tsconfig.json",
72
77
  "fix": "prettier --write . && eslint --fix . --max-warnings=0",
73
78
  "format": "prettier --check .",
74
79
  "lint": "eslint . --max-warnings=0",
75
- "clean": "rm -rf dist coverage node_modules"
80
+ "typecheck": "tsc --noEmit --project tsconfig.json"
76
81
  }
77
82
  }
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  useQuery as useConvexQuery,
6
6
  } from "convex/react";
7
7
  import { Cause, Effect, Either, Exit, Option } from "effect";
8
+ import { useCallback, useMemo } from "react";
8
9
 
9
10
  import * as QueryResult from "./QueryResult";
10
11
 
@@ -27,33 +28,47 @@ export const useQuery = <Query extends Ref.AnyPublicQuery>(
27
28
  ): QueryResult.QueryResult<Ref.Returns<Query>, Ref.Error<Query>> => {
28
29
  const functionReference = Ref.getFunctionReference(ref);
29
30
  const args = rest[0];
30
- const encodedArgs =
31
- args === "skip"
32
- ? "skip"
33
- : Ref.encodeArgsSync(ref, (args ?? {}) as Ref.Args<Query>);
31
+ const skipped = args === "skip";
32
+ const encodedArgs = skipped
33
+ ? "skip"
34
+ : Ref.encodeArgsSync(ref, (args ?? {}) as Ref.Args<Query>);
34
35
 
35
- try {
36
- const encodedReturnsOrUndefined = useConvexQuery(
37
- functionReference,
38
- encodedArgs,
39
- );
40
-
41
- if (encodedReturnsOrUndefined === undefined) {
42
- return QueryResult.load(args === "skip");
43
- }
36
+ // `useConvexQuery` returns a referentially stable value while the underlying
37
+ // Convex result is unchanged, and throws a stable error when the query
38
+ // fails. We capture either outcome as an `Either` and decode/wrap it inside
39
+ // `useMemo` so that the returned `QueryResult` keeps a stable identity across
40
+ // renders when nothing has actually changed. Decoding on every render would
41
+ // hand consumers a fresh object each time, breaking effects and memoization
42
+ // that depend on the result's identity.
43
+ const encodedReturnsOrError: Either.Either<unknown, unknown> = Either.try(
44
+ () => useConvexQuery(functionReference, encodedArgs),
45
+ );
44
46
 
45
- return QueryResult.succeed(
46
- Ref.decodeReturnsSync(ref, encodedReturnsOrUndefined),
47
- );
48
- } catch (error) {
49
- if (Ref.isConvexError(error)) {
50
- const decoded = Ref.decodeErrorSync(ref, error.data);
51
- if (Option.isSome(decoded)) {
52
- return QueryResult.fail(decoded.value);
53
- }
54
- }
55
- throw error;
56
- }
47
+ return useMemo(
48
+ () =>
49
+ Either.match(encodedReturnsOrError, {
50
+ onRight: (encodedReturnsOrUndefined) =>
51
+ encodedReturnsOrUndefined === undefined
52
+ ? QueryResult.load(skipped)
53
+ : QueryResult.succeed(
54
+ Ref.decodeReturnsSync(ref, encodedReturnsOrUndefined),
55
+ ),
56
+ onLeft: (error) => {
57
+ if (Ref.isConvexError(error)) {
58
+ const decoded = Ref.decodeErrorSync(ref, error.data);
59
+ if (Option.isSome(decoded)) {
60
+ return QueryResult.fail(decoded.value);
61
+ }
62
+ }
63
+ throw error;
64
+ },
65
+ }),
66
+ // `Either.try` allocates a fresh wrapper each render, so we key the memo on
67
+ // the stable value it carries (the Convex result or thrown error) rather
68
+ // than the wrapper itself; the decoded result is a function of that value,
69
+ // `ref`, and `skipped`.
70
+ [ref, skipped, Either.merge(encodedReturnsOrError)],
71
+ );
57
72
  };
58
73
 
59
74
  /**
@@ -75,14 +90,17 @@ export const useMutation = <Mutation extends Ref.AnyPublicMutation>(
75
90
  const functionReference = Ref.getFunctionReference(ref);
76
91
  const actualMutation = useConvexMutation(functionReference);
77
92
 
78
- return ((...args: Ref.OptionalArgs<Mutation>) =>
79
- invokeAsEither(
80
- ref,
81
- (_, encodedArgs) => actualMutation(encodedArgs),
82
- args,
83
- ).then((either) =>
84
- Ref.hasErrorSchema(ref) ? either : Either.getOrThrow(either),
85
- )) as (...args: Ref.OptionalArgs<Mutation>) => InvokeReturn<Mutation>;
93
+ return useCallback(
94
+ ((...args: Ref.OptionalArgs<Mutation>) =>
95
+ invokeAsEither(
96
+ ref,
97
+ (_, encodedArgs) => actualMutation(encodedArgs),
98
+ args,
99
+ ).then((either) =>
100
+ Ref.hasErrorSchema(ref) ? either : Either.getOrThrow(either),
101
+ )) as (...args: Ref.OptionalArgs<Mutation>) => InvokeReturn<Mutation>,
102
+ [ref, actualMutation],
103
+ );
86
104
  };
87
105
 
88
106
  /**
@@ -104,14 +122,17 @@ export const useAction = <Action extends Ref.AnyPublicAction>(
104
122
  const functionReference = Ref.getFunctionReference(ref);
105
123
  const actualAction = useConvexAction(functionReference);
106
124
 
107
- return ((...args: Ref.OptionalArgs<Action>) =>
108
- invokeAsEither(
109
- ref,
110
- (_, encodedArgs) => actualAction(encodedArgs),
111
- args,
112
- ).then((either) =>
113
- Ref.hasErrorSchema(ref) ? either : Either.getOrThrow(either),
114
- )) as (...args: Ref.OptionalArgs<Action>) => InvokeReturn<Action>;
125
+ return useCallback(
126
+ ((...args: Ref.OptionalArgs<Action>) =>
127
+ invokeAsEither(
128
+ ref,
129
+ (_, encodedArgs) => actualAction(encodedArgs),
130
+ args,
131
+ ).then((either) =>
132
+ Ref.hasErrorSchema(ref) ? either : Either.getOrThrow(either),
133
+ )) as (...args: Ref.OptionalArgs<Action>) => InvokeReturn<Action>,
134
+ [ref, actualAction],
135
+ );
115
136
  };
116
137
 
117
138
  const invokeAsEither = async <Ref_ extends Ref.Any>(