convex-jotai 0.1.0-alpha.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/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright 2026 Grzegorz Dunin-Ślęczek (https://github.com/grzesiekgs)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,210 @@
1
+ # convex-jotai
2
+
3
+ A lightweight bridge between [Convex](https://www.convex.dev/) and [Jotai](https://jotai.org/) that lets you manage Convex queries, mutations, and actions through Jotai atoms.
4
+ In concept it's similar to [`jotai-tanstack-query`](https://github.com/jotaijs/jotai-tanstack-query), meaning that it allows to access and modify `convex` state within `jotai` atoms.
5
+ Effectively it's drop in replacement for `convex/react`.
6
+
7
+ # Installation
8
+
9
+ ```bash
10
+ # or any bundler of your choice
11
+ bun add convex jotai convex-jotai
12
+ ```
13
+
14
+ ## Peer Dependencies
15
+
16
+ | Package | Version |
17
+ |---------|---------|
18
+ | `convex` | `>=1.31` |
19
+ | `jotai` | `>=2.6` |
20
+
21
+ These versions can be downgraded. I've selected latest version as of 06.01.2025. Create a GitHub issue if you would like to change this.
22
+
23
+ # Quick Start
24
+
25
+ ## Set up the Convex client in your Jotai store
26
+
27
+ ```typescript
28
+ import { ConvexClient } from 'convex/browser';
29
+ import { createStore, Provider } from 'jotai';
30
+ import { convexClientAtom } from 'convex-jotai';
31
+
32
+ // Create the Convex client.
33
+ // Note that ConvexReactClient is wrapper around ConvexClient.
34
+ // It could be possible to use both convex/react and convex-jotai, but currently that would require ConvexReactClient to expose ConvexClient.
35
+ const convex = new ConvexClient(import.meta.env.VITE_CONVEX_URL);
36
+ // convexClientAtom has to be set before using any of convex atoms. It doesn't necessarily has to be set in module scope.
37
+ // Therefore it's possible to rely on default store provided by jotai Provider.
38
+ const store = createStore();
39
+ store.set(convexClientAtom, convex);
40
+
41
+ // Wrap your app with the Jotai Provider
42
+ const App: FC = () => {
43
+ return (
44
+ <Provider store={store}>
45
+ <YourApp />
46
+ </Provider>
47
+ );
48
+ }
49
+ ```
50
+
51
+ ## Define your atoms
52
+
53
+ ```typescript
54
+ import { convexQueryAtom, convexMutationAtom, convexActionAtom } from 'convex-jotai';
55
+ import { api } from '../convex/_generated/api';
56
+
57
+ const themeQueryAtom = convexQueryAtom(api.settings.get, () => ({ key: 'theme' }));
58
+ const settingsMutationAtom = convexMutationAtom(api.settings.set);
59
+ const eventActionAtom = convexActionAtom(api.actions.event);
60
+ ```
61
+
62
+ ## Use atoms in your other atoms or components
63
+
64
+ ```typescript
65
+ import { atom, useAtomValue, useSetAtom } from 'jotai';
66
+
67
+ const backgroundColorAtom = atom((get) => {
68
+ const theme = get(themeQueryAtom);
69
+ // api.settings.get is still loading.
70
+ if (!theme) {
71
+ return '#FF0000';
72
+ }
73
+
74
+ if (theme === 'light') {
75
+ return '#FFFFFF';
76
+ }
77
+
78
+ return '#000000';
79
+ })
80
+
81
+ const Settings: FC = () => {
82
+ const backgroundColor = useAtomValue(backgroundColorAtom);
83
+ const settingsMutation = useSetAtom(settingsMutationAtom);
84
+ const runEventAction = useSetAtom(eventActionAtom);
85
+
86
+ useEffect(() => {
87
+ runEventAction({ event: 'settings-viewed' });
88
+ }, [runEventAction])
89
+
90
+ return (
91
+ <div style={{ backgroundColor }}>
92
+ <button onClick={() => settingsMutation({ key: 'theme', value: 'dark' })}>
93
+ Set Dark Theme
94
+ </button>
95
+ <button onClick={() => settingsMutation({ key: 'theme', value: 'light' })}>
96
+ Set Light Theme
97
+ </button>
98
+ </div>
99
+ );
100
+ }
101
+ ```
102
+ # Deep dive
103
+ More details about the implementation.
104
+
105
+ ## More features
106
+ Find out what else `convex-jotai` can do.
107
+
108
+ ### Dynamic query arguments
109
+ ```tsx
110
+ const dynamicSettingsQueryAtom = convexQueryAtom(api.settings.get, (get) => {
111
+ const userSelectedKey = get(userSelectedKeyAtom);
112
+
113
+ return { key: userSelectedKey }
114
+ });
115
+ ```
116
+
117
+ ### Support for suspense
118
+ ```tsx
119
+ const themeQueryPromiseAtom = convexQueryPromiseAtom(api.settings.get, () => ({ key: 'theme' }));
120
+ ```
121
+
122
+ ### Descriptive query result
123
+ ```tsx
124
+ const themeQueryResultAtom = convexQueryResultAtom(api.settings.get, () => ({ key: 'theme' }));
125
+
126
+ const themeQueryResult = useAtomValue(themeQueryResultAtom);
127
+
128
+ if (themeQueryResult.status === 'loading') {
129
+ ...
130
+ }
131
+
132
+ if (themeQueryResult.status === 'success') {
133
+ themeQueryResult.data
134
+ ...
135
+ }
136
+
137
+ if (themeQueryResult.status === 'error') {
138
+ themeQueryResult.error
139
+ ...
140
+ }
141
+ ```
142
+
143
+ ## `convex/react` vs `convex-jotai`
144
+
145
+ ### Provider Setup
146
+ ```tsx
147
+ // convex/react
148
+ import { ConvexProvider, ConvexReactClient } from 'convex/react';
149
+
150
+ const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL);
151
+
152
+ const App: FC = () => {
153
+ return (
154
+ <ConvexProvider client={convex}>
155
+ <YourApp />
156
+ </ConvexProvider>
157
+ );
158
+ }
159
+
160
+ // convex/jotai
161
+ import { ConvexClient } from 'convex/browser';
162
+ import { createStore, Provider } from 'jotai';
163
+ import { convexClientAtom } from 'convex-jotai';
164
+
165
+ const convex = new ConvexClient(import.meta.env.VITE_CONVEX_URL);
166
+ const store = createStore();
167
+ store.set(convexClientAtom, convex);
168
+
169
+ const App: FC = () => {
170
+ return (
171
+ <Provider store={store}>
172
+ <YourApp />
173
+ </Provider>
174
+ );
175
+ }
176
+ ```
177
+
178
+
179
+ ### Queries, mutations and actions
180
+
181
+ ```tsx
182
+ // convex/react
183
+ import { useQuery, useMutation, useAction } from 'convex/react';
184
+ import { api } from '../convex/_generated/api';
185
+
186
+ const theme = useQuery(api.settings.get, { key: 'theme' });
187
+ const settingsMutation = useMutation(api.settings.set);
188
+ const runEventAction = useAction(api.actions.event);
189
+
190
+ // convex-jotai
191
+ import { convexQueryAtom } from 'convex-jotai';
192
+ import { useAtomValue, useSetAtom } from 'jotai';
193
+ import { api } from '../convex/_generated/api';
194
+
195
+ const themeQueryAtom = convexQueryAtom(api.settings.get, () => ({ key: 'theme' }));
196
+ const settingsMutationAtom = convexMutationAtom(api.settings.set);
197
+ const eventActionAtom = convexActionAtom(api.actions.event);
198
+
199
+ const theme = useAtomValue(themeQueryAtom);
200
+ const settingsMutation = useSetAtom(settingsMutationAtom);
201
+ const runEventAction = useSetAtom(eventActionAtom);
202
+ ```
203
+
204
+ # Example
205
+
206
+ See the [playground app](../../apps/playground/README.md) for a complete working example comparing `convex/react` and `convex-jotai` side-by-side.
207
+
208
+ # License
209
+
210
+ [MIT](./LICENSE)
@@ -0,0 +1,45 @@
1
+ import * as jotai0 from "jotai";
2
+ import { Atom, Getter, WritableAtom } from "jotai";
3
+ import { FunctionArgs, FunctionReference, FunctionReturnType } from "convex/server";
4
+ import { ConvexClient, OptimisticUpdate } from "convex/browser";
5
+
6
+ //#region src/types.d.ts
7
+ type ConvexQueryOptionsGetter<Query extends FunctionReference<'query'>> = (get: Getter) => FunctionArgs<Query>;
8
+ type ConvexQueryErrorResult = {
9
+ status: 'error';
10
+ data?: never;
11
+ error: Error;
12
+ };
13
+ type ConvexQueryLoadingResult = {
14
+ status: 'loading';
15
+ data?: never;
16
+ error?: never;
17
+ };
18
+ type ConvexQuerySuccessResult<T> = {
19
+ status: 'success';
20
+ data: T;
21
+ error?: never;
22
+ };
23
+ type ConvexQueryResult<T> = ConvexQueryErrorResult | ConvexQueryLoadingResult | ConvexQuerySuccessResult<T>;
24
+ //#endregion
25
+ //#region src/atoms/convexQueryResultAtom.d.ts
26
+ declare function convexQueryResultAtom<Query extends FunctionReference<'query'>>(query: Query, queryOptionsGetter: ConvexQueryOptionsGetter<Query>): Atom<ConvexQueryResult<FunctionReturnType<Query>>>;
27
+ //#endregion
28
+ //#region src/atoms/convexClientAtom.d.ts
29
+ declare const convexClientAtom: jotai0.PrimitiveAtom<ConvexClient | null> & {
30
+ init: ConvexClient | null;
31
+ };
32
+ //#endregion
33
+ //#region src/atoms/convexQueryPromiseAtom.d.ts
34
+ declare function convexQueryPromiseAtom<Query extends FunctionReference<'query'>>(query: Query, queryOptionsGetter: ConvexQueryOptionsGetter<Query>): Atom<Promise<FunctionReturnType<Query>>>;
35
+ //#endregion
36
+ //#region src/atoms/convexQueryAtom.d.ts
37
+ declare function convexQueryAtom<Query extends FunctionReference<'query'>>(query: Query, queryOptionsGetter: ConvexQueryOptionsGetter<Query>): Atom<FunctionReturnType<Query>>;
38
+ //#endregion
39
+ //#region src/atoms/convexMutationAtom.d.ts
40
+ declare function convexMutationAtom<Mutation extends FunctionReference<'mutation'>>(mutation: Mutation, optimisticUpdate?: OptimisticUpdate<FunctionArgs<Mutation>>): WritableAtom<null, [FunctionArgs<Mutation>], FunctionReturnType<Mutation>>;
41
+ //#endregion
42
+ //#region src/atoms/convexActionAtom.d.ts
43
+ declare function convexActionAtom<Action extends FunctionReference<'action'>>(action: Action): WritableAtom<null, [FunctionArgs<Action>], FunctionReturnType<Action>>;
44
+ //#endregion
45
+ export { ConvexQueryErrorResult, ConvexQueryLoadingResult, ConvexQueryOptionsGetter, ConvexQueryResult, ConvexQuerySuccessResult, convexActionAtom, convexClientAtom, convexMutationAtom, convexQueryAtom, convexQueryPromiseAtom, convexQueryResultAtom };
package/dist/index.mjs ADDED
@@ -0,0 +1,108 @@
1
+ import { atom } from "jotai";
2
+ import { getFunctionName } from "convex/server";
3
+ import { selectAtom } from "jotai/utils";
4
+
5
+ //#region src/utils/getLocalConvexQueryResult.ts
6
+ function getLocalConvexQueryResult(convexClient, funcRef, queryOptions) {
7
+ try {
8
+ const result = convexClient.client.localQueryResult(getFunctionName(funcRef), queryOptions);
9
+ if (result === void 0) return { status: "loading" };
10
+ return {
11
+ status: "success",
12
+ data: result,
13
+ error: void 0
14
+ };
15
+ } catch (possiblyError) {
16
+ return {
17
+ status: "error",
18
+ error: possiblyError instanceof Error ? possiblyError : new Error(String(possiblyError))
19
+ };
20
+ }
21
+ }
22
+
23
+ //#endregion
24
+ //#region src/utils/assertConvexClient.ts
25
+ const assertConvexClient = (client) => {
26
+ if (typeof window === "undefined") throw new Error("convex-jotai: SSR is currently not supported.");
27
+ if (client === null) throw new Error("convex-jotai: convexClientAtom is not set. Follow the setup guide in the README.");
28
+ return client;
29
+ };
30
+
31
+ //#endregion
32
+ //#region src/atoms/convexClientAtom.ts
33
+ const convexClientAtom = atom(null);
34
+
35
+ //#endregion
36
+ //#region src/atoms/convexQueryResultAtom.ts
37
+ function convexQueryResultAtom(query, queryOptionsGetter) {
38
+ const queryOptionsAtom = atom((get) => queryOptionsGetter(get));
39
+ const querySubscriptionAtom = atom((get) => {
40
+ const convexClient = assertConvexClient(get(convexClientAtom));
41
+ const queryOptions = get(queryOptionsAtom);
42
+ const queryResultAtom = atom(getLocalConvexQueryResult(convexClient, query, queryOptions));
43
+ queryResultAtom.onMount = (setValue) => {
44
+ return convexClient.onUpdate(query, queryOptions, (update) => setValue({
45
+ status: "success",
46
+ data: update
47
+ }), (error) => setValue({
48
+ status: "error",
49
+ error
50
+ }));
51
+ };
52
+ return queryResultAtom;
53
+ });
54
+ return atom((get) => {
55
+ return get(get(querySubscriptionAtom));
56
+ });
57
+ }
58
+
59
+ //#endregion
60
+ //#region src/atoms/convexQueryPromiseAtom.ts
61
+ function convexQueryPromiseAtom(query, queryOptionsGetter) {
62
+ const queryPromiseStateAtom = selectAtom(convexQueryResultAtom(query, queryOptionsGetter), (queryResult, prevState) => {
63
+ const state = prevState ?? {
64
+ promiseWithResolvers: Promise.withResolvers(),
65
+ resolved: false
66
+ };
67
+ if (queryResult.status === "loading") {
68
+ if (!state.resolved) return state;
69
+ return {
70
+ promiseWithResolvers: Promise.withResolvers(),
71
+ resolved: false
72
+ };
73
+ }
74
+ return {
75
+ promiseWithResolvers: state.promiseWithResolvers,
76
+ resolved: true
77
+ };
78
+ });
79
+ return atom((get) => get(queryPromiseStateAtom).promiseWithResolvers.promise);
80
+ }
81
+
82
+ //#endregion
83
+ //#region src/atoms/convexQueryAtom.ts
84
+ function convexQueryAtom(query, queryOptionsGetter) {
85
+ const queryResultAtom = convexQueryResultAtom(query, queryOptionsGetter);
86
+ return atom((get) => {
87
+ return get(queryResultAtom).data;
88
+ });
89
+ }
90
+
91
+ //#endregion
92
+ //#region src/atoms/convexMutationAtom.ts
93
+ function convexMutationAtom(mutation, optimisticUpdate) {
94
+ return atom(null, (get, _set, args) => {
95
+ return assertConvexClient(get(convexClientAtom)).mutation(mutation, args, { optimisticUpdate });
96
+ });
97
+ }
98
+
99
+ //#endregion
100
+ //#region src/atoms/convexActionAtom.ts
101
+ function convexActionAtom(action) {
102
+ return atom(null, (get, _set, args) => {
103
+ return assertConvexClient(get(convexClientAtom)).action(action, args);
104
+ });
105
+ }
106
+
107
+ //#endregion
108
+ export { convexActionAtom, convexClientAtom, convexMutationAtom, convexQueryAtom, convexQueryPromiseAtom, convexQueryResultAtom };
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "convex-jotai",
3
+ "version": "0.1.0-alpha.0",
4
+ "license": "MIT",
5
+ "private": false,
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "description": "Jotai atoms for Convex queries, mutations, and actions",
9
+ "keywords": [
10
+ "jotai",
11
+ "convex",
12
+ "react",
13
+ "state-management",
14
+ "realtime"
15
+ ],
16
+ "author": "Grzegorz Dunin-Ślęczek <grzesiekgs@gmail.com>",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/grzesiekgs/jotai-convex.git",
20
+ "directory": "packages/convex-jotai"
21
+ },
22
+ "homepage": "https://github.com/grzesiekgs/jotai-convex#readme",
23
+ "bugs": {
24
+ "url": "https://github.com/grzesiekgs/jotai-convex/issues"
25
+ },
26
+ "files": [
27
+ "dist",
28
+ "LICENSE",
29
+ "README.md"
30
+ ],
31
+ "main": "dist/index.mjs",
32
+ "module": "dist/index.mjs",
33
+ "types": "dist/index.d.mts",
34
+ "exports": {
35
+ ".": {
36
+ "types": "./dist/index.d.mts",
37
+ "import": "./dist/index.mjs"
38
+ }
39
+ },
40
+ "scripts": {
41
+ "build": "tsdown",
42
+ "dev": "tsdown --watch",
43
+ "tsc": "tsc --noEmit"
44
+ },
45
+ "peerDependencies": {
46
+ "convex": ">=1.31",
47
+ "jotai": ">=2.6"
48
+ },
49
+ "publishConfig": {
50
+ "access": "public"
51
+ },
52
+ "devDependencies": {
53
+ "tsdown": "0.18.4",
54
+ "typescript": "5.9.3"
55
+ }
56
+ }