├── .changeset └── config.json ├── .codesandbox └── ci.json ├── .editorconfig ├── .eslintrc.js ├── .github └── workflows │ ├── compress-size-action.yml │ └── version.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── babel.config.js ├── jest.config.js ├── package.json ├── pnpm-lock.yaml ├── src ├── atoms │ ├── atom-family-with-infinite-query.ts │ ├── atom-family-with-query.ts │ ├── atom-with-infinite-query.ts │ ├── atom-with-query.ts │ ├── devtool-atom.ts │ ├── intitial-data-atom.ts │ ├── react-query │ │ ├── infinite-query-key-observer.ts │ │ ├── infinite-query-key-status-atom.ts │ │ ├── query-client-atom.ts │ │ ├── query-key-observer.ts │ │ └── query-key-status-atom.ts │ ├── static-query.ts │ ├── types.ts │ └── utils │ │ ├── as-infinite-data.ts │ │ ├── get-query-key.ts │ │ └── weak-cache.ts ├── cache.ts ├── constants.ts ├── devtools.ts ├── hooks │ ├── use-infinite-query-atom.ts │ └── use-query-atom.ts ├── index.ts ├── nextjs.ts ├── nextjs │ ├── build-initial-value-atoms.ts │ ├── get-initial-props-from-queries.ts │ ├── get-initial-query-props.ts │ ├── get-server-side-query-props.ts │ ├── get-static-query-props.ts │ ├── intial-queries-wrapper.ts │ ├── query-helpers.ts │ ├── types.ts │ ├── use-get-initial-query-props.ts │ ├── use-query-initial-values.ts │ └── with-initial-queries.ts ├── query-client.ts └── utils.ts └── tsconfig.json /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@1.6.0/schema.json", 3 | "changelog": [ 4 | "@changesets/changelog-github", 5 | { 6 | "repo": "fungible-systems/jotai-query-toolkit" 7 | } 8 | ], 9 | "commit": false, 10 | "linked": [], 11 | "access": "restricted", 12 | "baseBranch": "main", 13 | "updateInternalDependencies": "patch", 14 | "ignore": [] 15 | } 16 | -------------------------------------------------------------------------------- /.codesandbox/ci.json: -------------------------------------------------------------------------------- 1 | { 2 | "sandboxes": ["new", "vanilla"], 3 | "publishDirectory": { 4 | "jotai-query-toolkit": "dist" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: './tsconfig.json', 5 | }, 6 | extends: '@stacks/eslint-config', 7 | env: { 8 | browser: true, 9 | node: true, 10 | es6: true, 11 | }, 12 | globals: { 13 | page: true, 14 | browser: true, 15 | context: true, 16 | }, 17 | plugins: ['react-hooks', '@typescript-eslint'], 18 | rules: { 19 | '@typescript-eslint/no-floating-promises': [1], 20 | '@typescript-eslint/no-unnecessary-type-assertion': [0], 21 | '@typescript-eslint/no-unsafe-assignment': [0], 22 | '@typescript-eslint/no-unsafe-return': [0], 23 | '@typescript-eslint/no-unsafe-call': [0], 24 | '@typescript-eslint/no-unsafe-member-access': [0], 25 | '@typescript-eslint/ban-types': [0], 26 | '@typescript-eslint/restrict-template-expressions': [0], 27 | '@typescript-eslint/explicit-module-boundary-types': [0], 28 | '@typescript-eslint/no-non-null-assertion': [0], 29 | '@typescript-eslint/restrict-plus-operands': [0], 30 | '@typescript-eslint/no-var-requires': [0], 31 | 'no-warning-comments': [0], 32 | 'react-hooks/exhaustive-deps': ['warn'], 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /.github/workflows/compress-size-action.yml: -------------------------------------------------------------------------------- 1 | name: Compressed Size 2 | on: [ pull_request ] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Cancel Previous Runs 8 | uses: styfle/cancel-workflow-action@aec1e95348a6983270197ef14011414346269b77 9 | with: 10 | access_token: ${{ github.token }} 11 | - name: checkout code repository 12 | uses: actions/checkout@v2 13 | with: 14 | fetch-depth: 0 15 | - name: setup node.js 16 | uses: actions/setup-node@v2 17 | with: 18 | node-version: 16 19 | registry-url: https://registry.npmjs.org/ 20 | - name: Cache .pnpm-store 21 | uses: actions/cache@v1 22 | with: 23 | path: ~/.pnpm-store 24 | key: ${{ runner.os }}-node-${{ hashFiles('**/pnpm-lock.yaml') }} 25 | - name: Install pnpm 26 | run: curl -f https://get.pnpm.io/v6.14.js | node - add --global pnpm@6 27 | - name: compressed size action 28 | uses: fungible-systems/compressed-size-action@v2.0.3 29 | with: 30 | repo-token: "${{ secrets.GITHUB_TOKEN }}" 31 | -------------------------------------------------------------------------------- /.github/workflows/version.yml: -------------------------------------------------------------------------------- 1 | name: Changesets 2 | on: 3 | push: 4 | branches: 5 | - main 6 | env: 7 | CI: true 8 | PNPM_CACHE_FOLDER: .pnpm-store 9 | 10 | jobs: 11 | version: 12 | timeout-minutes: 15 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Cancel Previous Runs 16 | uses: styfle/cancel-workflow-action@aec1e95348a6983270197ef14011414346269b77 17 | with: 18 | access_token: ${{ github.token }} 19 | - name: checkout code repository 20 | uses: actions/checkout@v2 21 | with: 22 | fetch-depth: 0 23 | - name: setup node.js 24 | uses: actions/setup-node@v2 25 | with: 26 | node-version: 16 27 | registry-url: https://registry.npmjs.org/ 28 | - name: Cache .pnpm-store 29 | uses: actions/cache@v1 30 | with: 31 | path: ~/.pnpm-store 32 | key: ${{ runner.os }}-node-${{ hashFiles('**/pnpm-lock.yaml') }} 33 | - name: Install pnpm 34 | run: curl -f https://get.pnpm.io/v6.14.js | node - add --global pnpm@6 35 | - name: pnpm Build 36 | run: pnpm install 37 | - name: create and publish versions 38 | uses: changesets/action@v1 39 | with: 40 | commit: "chore: update versions" 41 | title: "chore: update versions" 42 | publish: pnpm ci:publish 43 | version: pnpm ci:version 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 47 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 48 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | .idea 7 | coverage 8 | .vscode 9 | .yalc 10 | yalc.lock 11 | .next 12 | docs 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # jotai-query-toolkit 2 | 3 | ## 0.1.23 4 | 5 | ### Patch Changes 6 | 7 | - [#39](https://github.com/fungible-systems/jotai-query-toolkit/pull/39) [`172e916`](https://github.com/fungible-systems/jotai-query-toolkit/commit/172e9163f1f08b8501c82371c84f2796e59339eb) Thanks [@aulneau](https://github.com/aulneau)! - This updates the build tooling to improve bundle sizes and tree shaking. 8 | 9 | ## 0.1.22 10 | 11 | ### Patch Changes 12 | 13 | - [#37](https://github.com/fungible-systems/jotai-query-toolkit/pull/37) [`00b5246`](https://github.com/fungible-systems/jotai-query-toolkit/commit/00b5246ea40ee9a6669739ebb891fbc5819bb4ed) Thanks [@aulneau](https://github.com/aulneau)! - This improves some of the ways we were caching certain atoms, and uses use-memo-one for more stable atom references. 14 | 15 | ## 0.1.21 16 | 17 | ### Patch Changes 18 | 19 | - [#35](https://github.com/fungible-systems/jotai-query-toolkit/pull/35) [`aba4298`](https://github.com/fungible-systems/jotai-query-toolkit/commit/aba4298f9a670752d7c684799a2f52f11b1f326e) Thanks [@aulneau](https://github.com/aulneau)! - This has a small fix for how we generate query-keys for our atom families, this should prevent keys from including params more than one time. 20 | 21 | ## 0.1.20 22 | 23 | ### Patch Changes 24 | 25 | - [`f6ddb00`](https://github.com/fungible-systems/jotai-query-toolkit/commit/f6ddb00fae12c8e46962991eb156d60f627b902e) Thanks [@aulneau](https://github.com/aulneau)! - This prevents an error from being thrown during dev mode. 26 | 27 | ## 0.1.19 28 | 29 | ### Patch Changes 30 | 31 | - [#31](https://github.com/fungible-systems/jotai-query-toolkit/pull/31) [`94ab573`](https://github.com/fungible-systems/jotai-query-toolkit/commit/94ab5736e4efdce652fc8e4f7eb6fad5807b9021) Thanks [@aulneau](https://github.com/aulneau)! - This update cleans up some of the typings around various functions, and removes certain checks while in dev mode. 32 | 33 | ## 0.1.18 34 | 35 | ### Patch Changes 36 | 37 | - [#28](https://github.com/fungible-systems/jotai-query-toolkit/pull/28) [`24111df`](https://github.com/fungible-systems/jotai-query-toolkit/commit/24111df5d5ef26fc152658f7202658fb06e7c7a8) Thanks [@hstove](https://github.com/hstove)! - fix: allow falsy values in initial values 38 | 39 | ## 0.1.17 40 | 41 | ### Patch Changes 42 | 43 | - [#25](https://github.com/fungible-systems/jotai-query-toolkit/pull/25) [`62b126e`](https://github.com/fungible-systems/jotai-query-toolkit/commit/62b126ebaf7dfaebce8265d619f82d4bb9990633) Thanks [@aulneau](https://github.com/aulneau)! - This updates the versions for react-query and jotai. 44 | 45 | ## 0.1.16 46 | 47 | ### Patch Changes 48 | 49 | - [#24](https://github.com/fungible-systems/jotai-query-toolkit/pull/24) [`a344f76`](https://github.com/fungible-systems/jotai-query-toolkit/commit/a344f7625dbd4b8d4c24d5d52f2964d85095f914) Thanks [@aulneau](https://github.com/aulneau)! - Makes it so you do not have to pass props to useQueryInitialValues. 50 | 51 | * [`6ababc9`](https://github.com/fungible-systems/jotai-query-toolkit/commit/6ababc9726caf6b8a10a4db2e3c33ee0b124d08a) Thanks [@aulneau](https://github.com/aulneau)! - Fixes a small issue around peer deps. 52 | 53 | ## 0.1.15 54 | 55 | ### Patch Changes 56 | 57 | - [#21](https://github.com/fungible-systems/jotai-query-toolkit/pull/21) [`2389018`](https://github.com/fungible-systems/jotai-query-toolkit/commit/238901869f8cecd2ba00184d99dcf6f2b5e04db8) Thanks [@aulneau](https://github.com/aulneau)! - Small fix around nextjs integration. 58 | 59 | ## 0.1.14 60 | 61 | ### Patch Changes 62 | 63 | - [#20](https://github.com/fungible-systems/jotai-query-toolkit/pull/20) [`3cdba6c`](https://github.com/fungible-systems/jotai-query-toolkit/commit/3cdba6c65d09c615b1851a2f9c02db3273c60848) Thanks [@aulneau](https://github.com/aulneau)! - General clean up and tidy around where logic lives. 64 | 65 | ## 0.1.13 66 | 67 | ### Patch Changes 68 | 69 | - [#16](https://github.com/fungible-systems/jotai-query-toolkit/pull/16) [`cea9795`](https://github.com/fungible-systems/jotai-query-toolkit/commit/cea9795622e450f8706da1ed7d1452ebf3bcafa3) Thanks [@aulneau](https://github.com/aulneau)! - This update works to improve and optimize the next.js integration, making it easier to use and extend for other libraries (such as micro-stacks). 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 👻🏗️ Jotai Query Toolkit 2 | 3 | This is an opinionated toolkit for working with Jotai and react-query. This library extends upon the react-query 4 | integration found within jotai and includes some batteries included solutions for using these tools with next.js. You 5 | can learn more about [Jotai here](https://jotai.pmnd.rs/), and [react-query here](https://react-query.tanstack.com/). 6 | 7 | ## Why 8 | 9 | I've spent years trying to find the optimal state management and remote data solutions that work well in both 10 | client-side react apps and server side rendered react apps (typically using next.js), and I have come to believe this is 11 | one of the best combinations both for developer experience and user experience and performance. Jotai is my favorite way 12 | to handle state in react applications, and react-query has an amazing API for handling remote data state. Together they 13 | create a new way of handling state and fetching remote data at the same time. 14 | 15 | ## Getting started 16 | 17 | To get started, you'll have to install a few dependencies: 18 | 19 | ```bash 20 | yarn add jotai jotai-query-toolkit react-query 21 | ``` 22 | 23 | ### Query keys 24 | 25 | Due to the tight integration with react-query, every atom we make will need to have some key that connects it to 26 | react-query state. To read more about how react-query uses query 27 | keys, [read this](https://react-query.tanstack.com/guides/query-keys). 28 | 29 | > At its core, React Query manages query caching for you based on query keys. Query keys can be as simple as a string, or as complex as an array of many strings and nested objects. As long as the query key is serializable, and unique to the query's data, you can use it! 30 | 31 | For this example, lets create a simple string enum as a key: 32 | 33 | ```typescript 34 | enum MyQueryKeys { 35 | FooBar = 'keys/FooBar' 36 | } 37 | ``` 38 | 39 | ### atomFamilyWithQuery 40 | 41 | Often times you'll have a category of remote data that you want to pass a parameter or set of parameters to, such as a 42 | remote user profile with a unique id. This atom is very similar to the standard `atomFamily` but takes some additional 43 | parameters: a query key, and a fetcher function. All `atomFamilyWithQuery` atom's will ultimately combine the base 44 | atomFamily key with whatever param is fed to the atom when used. 45 | 46 | ```typescript 47 | import {atomFamilyWithQuery} from "jotai-query-toolkit"; 48 | 49 | const fooBarAtom = atomFamilyWithQuery(MyQueryKeys.FooBar, async (get, param) => { 50 | const anotherAtomValue = get(anotherAtom); 51 | // this could be to fetch a unique users profile 52 | const remoteData = await fetchRemoteDate(anotherAtomValue, param); 53 | return remoteData; 54 | }) 55 | ``` 56 | 57 | To make use of this atom: 58 | 59 | ```tsx 60 | const FooBar = ({param}) => { 61 | const [fooBar, refresh] = useAtom(fooBarAtom(param)); 62 | return <>{fooBar} 63 | } 64 | 65 | const Component = () => { 66 | const param = 'foo' 67 | return loading...}> 68 | 69 | 70 | } 71 | ``` 72 | 73 | ### atomWithQuery 74 | 75 | Note: This is an opinionated wrapper around `atomWithQuery` that is exported by jotai. 76 | 77 | For data types that don't have unique parameters you need to fetch by, you can use the `atomWithQuery`. 78 | 79 | ```typescript 80 | import {atomWithQuery} from "jotai-query-toolkit"; 81 | 82 | const fooBarAtom = atomWithQuery(MyQueryKeys.FooBar, async (get) => { 83 | const anotherAtomValue = get(anotherAtom); 84 | // this could be to fetch a list of all users 85 | return fetchRemoteDate(anotherAtomValue); 86 | }) 87 | ``` 88 | 89 | ## Next.js 90 | 91 | Next.js is a framework that makes using server side rendered react very easy. Fetching data on the server and ensuring 92 | that client state reflects that initial data is less easy. JQT hopes to make this experience much better. 93 | 94 | All next.js related functionality is exported via `jotai-query-toolkit/nextjs`. 95 | 96 | You can see a [demo here](https://jqt-next.vercel.app/), and 97 | the [code that powers it here](https://github.com/fungible-systems/jotai-query-toolkit/blob/main/examples/next-js/src/pages/index.tsx) 98 | . 99 | 100 | To get started, create a query key and an atom: 101 | 102 | ```typescript 103 | // our query keys 104 | enum HomeQueryKeys { 105 | FooBar = 'home/FooBar', 106 | } 107 | 108 | // some values for demo, 109 | // not specific to JQT 110 | let count = 0; 111 | let hasMounted = false; 112 | 113 | // our atomWithQueryRefresh 114 | const fooBarAtom = atomWithQuery( 115 | HomeQueryKeys.FooBar, // our QueryKey 116 | () => { 117 | if (hasMounted) count += 3; 118 | if (!hasMounted) hasMounted = true; 119 | return `bar ${count} (client rendered, updates every 3 seconds)`; 120 | }, 121 | {refetchInterval: 3000} // extra queryClient options can be passed here 122 | ); 123 | ``` 124 | 125 | Next up we can create a component that will use this atom: 126 | 127 | ```tsx 128 | // the component that uses the atomWithQueryRefresh 129 | const FooBar = () => { 130 | const [fooBar, refresh] = useAtom(fooBarAtom); 131 | return ( 132 | <> 133 |

{fooBar}

134 | 135 | 136 | ); 137 | }; 138 | ``` 139 | 140 | Next we will go to the page which will contain this atom and component, and we'll import `QueryProvider` 141 | from `jotai-query-toolkit`, and pass it our page props, and the query keys we are using. 142 | 143 | ```tsx 144 | import { QueryProvider } from 'jotai-query-toolkit/nextjs' 145 | // our next.js page component 146 | const MyHomePage = (props: Record) => { 147 | return ( 148 | 149 |
150 |

next.js jotai-query-toolkit

151 | 152 |
153 |
154 | ); 155 | }; 156 | ``` 157 | 158 | To fetch the data on the server, we'll use `getInitialProps` and from `getInitialPropsFromQueries` 159 | from `jotai-query-toolkit`. 160 | 161 | ```ts 162 | import { getInitialPropsFromQueries } from 'jotai-query-toolkit/nextjs' 163 | 164 | // our queries 165 | const queries = [ 166 | [ 167 | HomeQueryKeys.FooBar, // the query key we're using 168 | async (_context: NextPageContext) => { // all fetchers can make use of the NextPageContext 169 | return `foo ${count} (initial data on the server, will update in 3 seconds)`; 170 | }, // our fetcher for the server 171 | ], 172 | ]; 173 | 174 | MyHomePage.getInitialProps = async (ctx: NextPageContext) => { 175 | return getInitialPropsFromQueries(queries, ctx); // returns Record 176 | }; 177 | ``` 178 | 179 | There you have it! you'll automatically fetch the data on the server, and when the client hydrates, the atom will 180 | take over and automatically refresh every 3 seconds as we've defined above. If the user navigates to this page from a 181 | different page and there is data in the react-query cache, no additional fetching will occur. 182 | 183 | ### HOC for next.js pages 184 | 185 | Above is the method you can use if you have more complex needs (such as queries that rely on one another). If you have 186 | less connected queries, you can opt for the higher order component that takes more complexity away. Let's modify the 187 | example above to use the `withInitialQueries` HOC: 188 | 189 | ```tsx 190 | import {withInitialQueries, GetQueries} from 'jotai-query-toolkit/nextjs' 191 | 192 | // the same queries as above 193 | const getQueries: GetQueries = (ctx: NextPageContext) => [ 194 | [ 195 | HomeQueryKeys.FooBar, // the query key we're using 196 | async () => { 197 | return `foo ${count} (initial data on the server, will update in 3 seconds)`; 198 | }, // our fetcher for the server 199 | ], 200 | ]; 201 | // our next.js page component 202 | const MyHomePage = (props) => { 203 | return ( 204 |
205 |

next.js jotai-query-toolkit

206 | 207 |
208 | ); 209 | }; 210 | 211 | // wrap your page component with `withInitialQueries`, and pass your queries array to it. 212 | export default withInitialQueries(MyHomePage)(getQueries) 213 | ``` 214 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = (api, targets) => { 2 | // https://babeljs.io/docs/en/config-files#config-function-api 3 | const isTestEnv = api.env('test'); 4 | 5 | return { 6 | babelrc: false, 7 | ignore: ['./node_modules'], 8 | presets: [ 9 | [ 10 | '@babel/preset-env', 11 | { 12 | modules: isTestEnv ? 'commonjs' : false, 13 | targets: isTestEnv ? { node: 'current' } : targets, 14 | loose: false, 15 | useBuiltIns: false, 16 | exclude: ['transform-async-to-generator', 'transform-regenerator'], 17 | }, 18 | ], 19 | ], 20 | plugins: [ 21 | '@babel/plugin-transform-react-jsx', 22 | ['@babel/plugin-transform-typescript', { allExtensions: true, isTSX: true }], 23 | '@babel/plugin-proposal-class-properties', 24 | '@babel/plugin-proposal-nullish-coalescing-operator', 25 | ], 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const { pathsToModuleNameMapper } = require('ts-jest/utils'); 2 | const { compilerOptions } = require('./tsconfig'); 3 | 4 | module.exports = { 5 | testEnvironment: 'node', 6 | moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: '/' }), 7 | preset: 'ts-jest/presets/js-with-babel', 8 | globals: { 9 | 'ts-jest': { 10 | babelConfig: true, 11 | }, 12 | }, 13 | transform: { 14 | '^.+\\.(ts|tsx)?$': 'ts-jest', 15 | '^.+\\.(js|jsx)$': 'babel-jest', 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jotai-query-toolkit", 3 | "private": false, 4 | "version": "0.1.23", 5 | "description": "A toolkit for opinionated ways to use Jotai, react-query, and next.js", 6 | "main": "./index.js", 7 | "module": "esm/index.js", 8 | "import": "esm/index.js", 9 | "types": "./index.d.ts", 10 | "typesVersions": { 11 | "<4.0": { 12 | "esm/*": [ 13 | "ts3.4/*" 14 | ], 15 | "*": [ 16 | "ts3.4/*" 17 | ] 18 | } 19 | }, 20 | "exports": { 21 | "./package.json": "./package.json", 22 | ".": { 23 | "main": "./index.js", 24 | "types": "./index.d.ts", 25 | "module": "./esm/index.js", 26 | "import": "./esm/index.js", 27 | "default": "./index.js" 28 | }, 29 | "./nextjs": { 30 | "main": "./nextjs.js", 31 | "types": "./nextjs.d.ts", 32 | "module": "./esm/nextjs.js", 33 | "import": "./esm/nextjs.js", 34 | "default": "./nextjs.js" 35 | } 36 | }, 37 | "files": [ 38 | "**" 39 | ], 40 | "sideEffects": false, 41 | "scripts": { 42 | "prebuild": "shx rm -rf dist", 43 | "build": "concurrently 'pnpm:build:*'", 44 | "build:base": "tsup src/index.ts --format esm,cjs --dts --minify --target node16 --splitting --external react jotai react-query fast-deep-equal use-memo-one --legacy-output", 45 | "build:nextjs": "tsup src/nextjs.ts --format esm,cjs --dts --minify --target node16 --splitting --external react jotai react-query fast-deep-equal use-memo-one next --legacy-output", 46 | "dev:build": "pnpm build && pnpm postbuild && yalc publish dist --push", 47 | "postbuild": "pnpm copy", 48 | "lint:eslint": "eslint --ext .ts,.tsx ./src", 49 | "lint:fix": "eslint --ext .ts,.tsx ./src/ -f unix --fix && prettier --write src/**/*.{ts,tsx} *.js", 50 | "lint:prettier": "prettier --check \"src/**/*.{ts,tsx}\" *.js *.json", 51 | "lint:prettier:fix": "prettier --write \"src/**/*.{ts,tsx}\" *.js *.json", 52 | "pretest": "tsc --noEmit", 53 | "test": "NODE_ENV=test jest --passWithNoTests", 54 | "test:coverage": "NODE_ENV=test jest --coverage", 55 | "typecheck": "tsc --noEmit", 56 | "prerelease": "pnpm build", 57 | "release": "cd dist && npm publish", 58 | "copy": "shx cp package.json dist && json -I -f dist/package.json -e \"this.private=false; this.devDependencies=undefined; this.optionalDependencies=undefined; this.scripts=undefined; this.husky=undefined; this.prettier=undefined; this.jest=undefined; this['lint-staged']=undefined;\"", 59 | "ci:publish": "pnpm build && pnpm postbuild && pnpm publish ./dist --no-git-checks --access public", 60 | "ci:version": "pnpm changeset version && pnpm install --no-frozen-lockfile && git add ." 61 | }, 62 | "husky": { 63 | "hooks": { 64 | "pre-commit": "lint-staged" 65 | } 66 | }, 67 | "engines": { 68 | "node": ">=12" 69 | }, 70 | "prettier": "@stacks/prettier-config", 71 | "lint-staged": { 72 | "*.{js,ts,tsx,md}": [ 73 | "prettier --write" 74 | ] 75 | }, 76 | "repository": { 77 | "type": "git", 78 | "url": "git+https://github.com/fungible-systems/jotai-query-utils.git" 79 | }, 80 | "keywords": [ 81 | "stacks", 82 | "web3", 83 | "small-bundle" 84 | ], 85 | "author": "Thomas Osmonson", 86 | "contributors": [], 87 | "license": "GPL-3.0-or-later", 88 | "bugs": { 89 | "url": "https://github.com/fungible-systems/jotai-query-utils/issues" 90 | }, 91 | "homepage": "https://github.com/fungible-systems/jotai-query-utils", 92 | "devDependencies": { 93 | "@changesets/changelog-github": "0.4.1", 94 | "@changesets/cli": "2.17.0", 95 | "@stacks/eslint-config": "1.0.10", 96 | "@stacks/prettier-config": "0.0.9", 97 | "@testing-library/react": "12.1.2", 98 | "@types/merge-deep": "3.0.0", 99 | "@types/react": "17.0.28", 100 | "@types/react-dom": "17.0.9", 101 | "@typescript-eslint/eslint-plugin": "5.0.0", 102 | "@typescript-eslint/parser": "5.0.0", 103 | "concurrently": "6.3.0", 104 | "eslint": "7.32.0", 105 | "eslint-config-prettier": "8.3.0", 106 | "eslint-import-resolver-alias": "1.1.2", 107 | "eslint-plugin-import": "2.24.2", 108 | "eslint-plugin-jest": "24.4.2", 109 | "eslint-plugin-prettier": "4.0.0", 110 | "eslint-plugin-react": "7.26.0", 111 | "eslint-plugin-react-hooks": "4.2.0", 112 | "husky": "7.0.2", 113 | "jest": "27.2.5", 114 | "json": "11.0.0", 115 | "lint-staged": "11.2.3", 116 | "next": "11.1.2", 117 | "prettier": "2.4.1", 118 | "react": "17.0.2", 119 | "react-dom": "17.0.2", 120 | "shx": "0.3.3", 121 | "ts-jest": "27.0.5", 122 | "tslib": "2.3.1", 123 | "tsup": "^5.11.6", 124 | "typescript": "4.5.3" 125 | }, 126 | "dependencies": { 127 | "fast-deep-equal": "latest", 128 | "jotai": "latest", 129 | "react-query": "latest", 130 | "use-memo-one": "1.1.2" 131 | }, 132 | "peerDependencies": { 133 | "next": "*", 134 | "react": ">=16.8", 135 | "react-dom": ">=16.8" 136 | }, 137 | "peerDependenciesMeta": { 138 | "next": { 139 | "optional": true 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/atoms/atom-family-with-infinite-query.ts: -------------------------------------------------------------------------------- 1 | import deepEqual from 'fast-deep-equal/es6'; 2 | import { atom } from 'jotai'; 3 | import { atomFamily } from 'jotai/utils'; 4 | import { getKeys } from './utils/get-query-key'; 5 | import { atomWithInfiniteQuery } from './atom-with-infinite-query'; 6 | import { queryKeyCache } from '../utils'; 7 | import { setCacheItem } from '../cache'; 8 | 9 | import type { WritableAtom, Getter } from 'jotai'; 10 | import type { AtomWithInfiniteQueryAction } from 'jotai/query'; 11 | import type { InfiniteData } from 'react-query'; 12 | import type { 13 | AtomFamily, 14 | AtomFamilyWithInfiniteQueryFn, 15 | AtomWithInfiniteQueryOptions, 16 | } from './types'; 17 | import type { QueryKeyOrGetQueryKey } from './types'; 18 | 19 | export const atomFamilyWithInfiniteQuery = ( 20 | key: QueryKeyOrGetQueryKey, 21 | queryFn: AtomFamilyWithInfiniteQueryFn, 22 | options: 23 | | AtomWithInfiniteQueryOptions 24 | | ((param: Param, get: Getter) => AtomWithInfiniteQueryOptions) = {} 25 | ): AtomFamily< 26 | Param, 27 | WritableAtom | undefined, AtomWithInfiniteQueryAction> 28 | > => 29 | atomFamily | undefined, AtomWithInfiniteQueryAction>(param => { 30 | // create our query atom 31 | const baseAtom = atom(get => { 32 | if (typeof options === 'function') options = options(param, get); 33 | const { queryKeyAtom, ...queryOptions } = options; 34 | const { queryKey } = getKeys(get, key, param, queryKeyAtom); 35 | const queryAtom = atomWithInfiniteQuery( 36 | queryKey, 37 | (get, context) => queryFn(get, param, context), 38 | queryOptions 39 | ); 40 | return { queryAtom, queryKey }; 41 | }); 42 | 43 | // wrapper atom 44 | const anAtom = atom | undefined, AtomWithInfiniteQueryAction>( 45 | get => { 46 | const { queryAtom, queryKey } = get(baseAtom); 47 | setCacheItem(queryKeyCache, anAtom, queryKey); 48 | return get(queryAtom); 49 | }, 50 | (get, set, action) => set(get(baseAtom).queryAtom, action) 51 | ); 52 | return anAtom; 53 | }, deepEqual); 54 | -------------------------------------------------------------------------------- /src/atoms/atom-family-with-query.ts: -------------------------------------------------------------------------------- 1 | import deepEqual from 'fast-deep-equal/es6'; 2 | import { atom } from 'jotai'; 3 | import { atomFamily } from 'jotai/utils'; 4 | import { queryKeyCache } from '../utils'; 5 | import { atomWithQuery } from './atom-with-query'; 6 | import { createMemoizeAtom, setWeakCacheItem } from '../cache'; 7 | import { getKeys, makeDebugLabel } from './utils/get-query-key'; 8 | import type { Getter } from 'jotai'; 9 | import type { JQTAtomWithQueryActions } from './atom-with-query'; 10 | import type { AtomFamilyWithQueryFn, AtomWithQueryOptions, QueryKeyOrGetQueryKey } from './types'; 11 | 12 | export const atomFamilyWithQuery = ( 13 | key: QueryKeyOrGetQueryKey, 14 | queryFn: AtomFamilyWithQueryFn, 15 | options: 16 | | AtomWithQueryOptions 17 | | ((param: Param, get: Getter) => AtomWithQueryOptions) = {} 18 | ) => { 19 | const memoized = createMemoizeAtom>(); 20 | 21 | return atomFamily>(param => { 22 | const baseAtom = atom(get => { 23 | if (typeof options === 'function') options = options(param, get); 24 | const { queryKeyAtom, ...queryOptions } = options; 25 | // create our query atom 26 | const { queryKey } = getKeys(get, key, param, queryKeyAtom); 27 | const queryAtom = atomWithQuery( 28 | queryKey, 29 | (get, context) => queryFn(get, param, context), 30 | queryOptions 31 | ); 32 | return { queryAtom, queryKey }; 33 | }); 34 | 35 | // wrapper atom 36 | const anAtom = memoized( 37 | () => 38 | atom>( 39 | get => { 40 | const { queryAtom, queryKey } = get(baseAtom); 41 | const deps = [anAtom] as const; 42 | setWeakCacheItem(queryKeyCache, deps, queryKey); 43 | return get(queryAtom); 44 | }, 45 | (get, set, action) => set(get(baseAtom).queryAtom, action) 46 | ), 47 | [baseAtom] 48 | ); 49 | anAtom.debugLabel = makeDebugLabel('atomFamilyWithQuery', 'TODO:fix', param); 50 | return anAtom; 51 | }, deepEqual); 52 | }; 53 | -------------------------------------------------------------------------------- /src/atoms/atom-with-infinite-query.ts: -------------------------------------------------------------------------------- 1 | import deepEqual from 'fast-deep-equal/es6'; 2 | import { atom } from 'jotai'; 3 | import { atomWithInfiniteQuery as jotaiAtomWithInfiniteQuery } from 'jotai/query'; 4 | import { hashQueryKey } from 'react-query'; 5 | import { makeQueryKey, queryKeyCache } from '../utils'; 6 | import { initialDataAtom } from './intitial-data-atom'; 7 | import { IS_SSR, QueryRefreshRates } from '../constants'; 8 | import { setWeakCacheItem } from '../cache'; 9 | import { asInfiniteData } from './utils/as-infinite-data'; 10 | import { getQueryClientAtom } from './react-query/query-client-atom'; 11 | 12 | import type { AtomWithInfiniteQueryAction } from 'jotai/query'; 13 | import type { InfiniteData, QueryKey } from 'react-query'; 14 | import type { AtomWithInfiniteQueryFn, AtomWithInfiniteQueryOptions } from './types'; 15 | import type { Getter, Atom } from 'jotai'; 16 | 17 | type QueryKeyOrGetQueryKey = QueryKey | ((get: Getter) => QueryKey); 18 | type QueryOptionsOrGetQueryOptions = 19 | | ((get: Getter) => AtomWithInfiniteQueryOptions) 20 | | AtomWithInfiniteQueryOptions; 21 | 22 | const getQueryKey = (get: Getter, key: QueryKeyOrGetQueryKey, queryKeyAtom?: Atom) => { 23 | const queryKey = typeof key === 'function' ? key(get) : key; 24 | if (queryKeyAtom) return makeQueryKey(queryKey, get(queryKeyAtom)); 25 | return makeQueryKey(queryKey); 26 | }; 27 | 28 | export const atomWithInfiniteQuery = ( 29 | key: QueryKeyOrGetQueryKey, 30 | queryFn: AtomWithInfiniteQueryFn, 31 | queryOptions: QueryOptionsOrGetQueryOptions = {} 32 | ) => { 33 | const baseAtom = atom(get => { 34 | const options = typeof queryOptions === 'function' ? queryOptions(get) : queryOptions; 35 | 36 | const { 37 | equalityFn = deepEqual, 38 | getShouldRefetch, 39 | queryKeyAtom, 40 | refetchInterval, 41 | refetchOnMount = false, 42 | refetchOnWindowFocus = false, 43 | refetchOnReconnect = false, 44 | ...rest 45 | } = options; 46 | 47 | const queryKey = getQueryKey(get, key, queryKeyAtom); 48 | const hashedQueryKey = hashQueryKey(queryKey); 49 | const theInitialDataAtom = initialDataAtom(hashedQueryKey); 50 | const initialData = asInfiniteData(get(theInitialDataAtom) as unknown as Data); 51 | 52 | const shouldRefresh = getShouldRefetch && initialData ? getShouldRefetch(initialData) : true; 53 | const queryClient = getQueryClientAtom(get); 54 | const defaultOptions = queryClient.defaultQueryOptions(rest); 55 | 56 | const getRefreshInterval = () => { 57 | return shouldRefresh 58 | ? refetchInterval === false 59 | ? false 60 | : refetchInterval || QueryRefreshRates.Default 61 | : false; 62 | }; 63 | 64 | const queryAtom = jotaiAtomWithInfiniteQuery( 65 | get => ({ 66 | queryKey, 67 | queryFn: context => queryFn(get, context), 68 | ...(defaultOptions as any), 69 | initialData, 70 | refetchInterval: getRefreshInterval(), 71 | refetchOnMount: shouldRefresh ? refetchOnMount : false, 72 | refetchOnWindowFocus: shouldRefresh ? refetchOnWindowFocus : false, 73 | refetchOnReconnect: shouldRefresh ? refetchOnReconnect : false, 74 | }), 75 | getQueryClientAtom 76 | ); 77 | queryAtom.debugLabel = `atomWithInfiniteQuery/queryAtom/${hashedQueryKey}`; 78 | 79 | return { 80 | queryKey, 81 | queryAtom, 82 | initialData, 83 | }; 84 | }); 85 | 86 | const anAtom = atom | undefined, AtomWithInfiniteQueryAction>( 87 | get => { 88 | const { initialData, queryAtom, queryKey } = get(baseAtom); 89 | const deps = [anAtom] as const; 90 | setWeakCacheItem(queryKeyCache, deps, queryKey); 91 | return IS_SSR ? initialData : get(queryAtom); 92 | }, 93 | (get, set, action) => set(get(baseAtom).queryAtom, action) 94 | ); 95 | return anAtom; 96 | }; 97 | -------------------------------------------------------------------------------- /src/atoms/atom-with-query.ts: -------------------------------------------------------------------------------- 1 | import deepEqual from 'fast-deep-equal/es6'; 2 | import { atom, Getter } from 'jotai'; 3 | import { atomWithQuery as jotaiAtomWithQuery, AtomWithQueryAction } from 'jotai/query'; 4 | import { hashQueryKey, MutateOptions, QueryKey, SetDataOptions } from 'react-query'; 5 | import { makeQueryKey, queryKeyCache } from '../utils'; 6 | import { initialDataAtom } from './intitial-data-atom'; 7 | import { IS_SSR, QueryRefreshRates } from '../constants'; 8 | import { AtomWithQueryOptions, AtomWithQueryFn } from './types'; 9 | import { setWeakCacheItem } from '../cache'; 10 | import { queryKeyObserver } from './react-query/query-key-observer'; 11 | import { getQueryClientAtom, queryClientAtom } from './react-query/query-client-atom'; 12 | import { Atom } from 'jotai/ts3.4'; 13 | 14 | type QueryKeyOrGetQueryKey = QueryKey | ((get: Getter) => QueryKey); 15 | type QueryOptionsOrGetQueryOptions = 16 | | ((get: Getter) => AtomWithQueryOptions) 17 | | AtomWithQueryOptions; 18 | 19 | export type JQTAtomWithQueryActions = 20 | | AtomWithQueryAction 21 | | { 22 | type: 'setQueryData'; 23 | payload: { 24 | data: Data; 25 | options?: SetDataOptions; 26 | }; 27 | } 28 | | { type: 'mutate'; payload: MutateOptions }; 29 | 30 | const getQueryKey = (get: Getter, key: QueryKeyOrGetQueryKey, queryKeyAtom?: Atom) => { 31 | const queryKey = typeof key === 'function' ? key(get) : key; 32 | if (queryKeyAtom) return makeQueryKey(queryKey, get(queryKeyAtom)); 33 | return makeQueryKey(queryKey); 34 | }; 35 | 36 | export const atomWithQuery = ( 37 | key: QueryKeyOrGetQueryKey, 38 | queryFn: AtomWithQueryFn, 39 | queryOptions: QueryOptionsOrGetQueryOptions = {} 40 | ) => { 41 | const baseAtom = atom(get => { 42 | const options = typeof queryOptions === 'function' ? queryOptions(get) : queryOptions; 43 | 44 | const { 45 | equalityFn = deepEqual, 46 | getShouldRefetch, 47 | queryKeyAtom, 48 | refetchInterval, 49 | refetchOnMount = false, 50 | refetchOnWindowFocus = false, 51 | refetchOnReconnect = false, 52 | ...rest 53 | } = options; 54 | 55 | const queryKey = getQueryKey(get, key, queryKeyAtom); 56 | const hashedQueryKey = hashQueryKey(queryKey); 57 | const theInitialDataAtom = initialDataAtom(hashedQueryKey); 58 | const initialData = get(theInitialDataAtom) as unknown as Data; 59 | 60 | const shouldRefresh = getShouldRefetch && initialData ? getShouldRefetch(initialData) : true; 61 | const queryClient = get(queryClientAtom); 62 | 63 | const getRefreshInterval = () => { 64 | return shouldRefresh 65 | ? refetchInterval === false 66 | ? false 67 | : refetchInterval || QueryRefreshRates.Default 68 | : false; 69 | }; 70 | 71 | const defaultOptions = queryClient.defaultQueryOptions({ 72 | ...rest, 73 | refetchInterval: getRefreshInterval(), 74 | refetchOnMount: shouldRefresh ? refetchOnMount : false, 75 | refetchOnWindowFocus: shouldRefresh ? refetchOnWindowFocus : false, 76 | refetchOnReconnect: shouldRefresh ? refetchOnReconnect : false, 77 | initialData, 78 | }); 79 | 80 | const queryAtom = jotaiAtomWithQuery( 81 | get => ({ 82 | queryKey, 83 | queryFn: context => queryFn(get, context), 84 | ...defaultOptions, 85 | }), 86 | getQueryClientAtom 87 | ); 88 | queryAtom.debugLabel = `atomWithQuery/queryAtom/${hashedQueryKey}`; 89 | 90 | return { 91 | queryKey, 92 | queryAtom, 93 | initialData, 94 | }; 95 | }); 96 | 97 | const anAtom = atom>( 98 | get => { 99 | const { initialData, queryAtom, queryKey } = get(baseAtom); 100 | const deps = [anAtom] as const; 101 | setWeakCacheItem(queryKeyCache, deps, queryKey); 102 | return IS_SSR ? initialData : get(queryAtom); 103 | }, 104 | (get, set, action) => { 105 | const { queryKey } = get(baseAtom); 106 | 107 | switch (action.type) { 108 | case 'refetch': { 109 | const observer = get(queryKeyObserver(queryKey)); 110 | void observer?.refetch(); 111 | break; 112 | } 113 | case 'setQueryData': { 114 | const queryClient = getQueryClientAtom(get); 115 | void queryClient 116 | .getQueryCache() 117 | .find(queryKey) 118 | ?.setData(action.payload.data, action.payload.options); 119 | break; 120 | } 121 | case 'mutate': { 122 | const queryClient = getQueryClientAtom(get); 123 | void queryClient.executeMutation(action.payload); 124 | break; 125 | } 126 | } 127 | } 128 | ); 129 | return anAtom; 130 | }; 131 | -------------------------------------------------------------------------------- /src/atoms/devtool-atom.ts: -------------------------------------------------------------------------------- 1 | import { atom, PrimitiveAtom } from 'jotai'; 2 | import { atomFamily } from 'jotai/utils'; 3 | import deepEqual from 'fast-deep-equal/es6'; 4 | 5 | type Config = { 6 | instanceID?: number; 7 | name?: string; 8 | serialize?: boolean; 9 | actionCreators?: any; 10 | latency?: number; 11 | predicate?: any; 12 | autoPause?: boolean; 13 | }; 14 | 15 | type Message = { 16 | type: string; 17 | payload?: any; 18 | state?: any; 19 | }; 20 | 21 | type ConnectionResult = { 22 | subscribe: (dispatch: any) => () => void; 23 | unsubscribe: () => void; 24 | send: (action: string, state: any) => void; 25 | init: (state: any) => void; 26 | error: (payload: any) => void; 27 | }; 28 | 29 | type Extension = { 30 | connect: (options?: Config) => ConnectionResult; 31 | }; 32 | 33 | const atomMap = new WeakMap(); 34 | 35 | export const IS_DEVTOOL_ENV = 36 | typeof process === 'object' && 37 | process.env.NODE_ENV === 'development' && 38 | typeof window !== 'undefined' && 39 | '__REDUX_DEVTOOLS_EXTENSION__' in window; 40 | 41 | const getExtension = (): Extension => { 42 | try { 43 | return (window as any).__REDUX_DEVTOOLS_EXTENSION__ as Extension; 44 | } catch (e) { 45 | throw new Error('Please install or enable Redux Devtools'); 46 | } 47 | }; 48 | 49 | type DevtoolsAtom = ConnectionResult & { shouldInit?: boolean }; 50 | 51 | export const devtoolAtom = atomFamily, void>( 52 | anAtom => 53 | atom(get => { 54 | if (!IS_DEVTOOL_ENV) return; 55 | const isWriteable = !!anAtom.write; 56 | const isTimeTravelingAtom = atom(false); 57 | const lastValueAtom = atom(undefined); 58 | const atomName = anAtom.debugLabel || anAtom.toString(); 59 | const extension = getExtension(); 60 | const cached = atomMap.get(anAtom); 61 | const devtools: DevtoolsAtom = cached || extension.connect({ name: atomName }); 62 | 63 | if (!cached) { 64 | devtools.shouldInit = true; 65 | atomMap.set(anAtom, devtools); 66 | } else if (cached.shouldInit) { 67 | devtools.shouldInit = false; 68 | atomMap.set(anAtom, devtools); 69 | } 70 | 71 | let unsubscribe: undefined | (() => void); 72 | 73 | const subscribeAtom = atom( 74 | get => { 75 | if (isWriteable) { 76 | const isTimeTraveling = get(isTimeTravelingAtom); 77 | if (!isTimeTraveling) 78 | devtools.send(`${atomName} - ${new Date().toLocaleString()}`, get(anAtom)); 79 | } 80 | }, 81 | (get, set, update) => { 82 | const listener = (message: Message) => { 83 | if (message.type === 'DISPATCH' && message.state) { 84 | if ( 85 | message.payload?.type === 'JUMP_TO_ACTION' || 86 | message.payload?.type === 'JUMP_TO_STATE' 87 | ) { 88 | set(isTimeTravelingAtom, true); 89 | } 90 | if (message.payload?.type !== 'TOGGLE_ACTION') set(anAtom, JSON.parse(message.state)); 91 | } else if (message.type === 'DISPATCH' && message.payload?.type === 'COMMIT') { 92 | devtools.init(get(lastValueAtom)); 93 | } else if (message.type === 'DISPATCH' && message.payload?.type === 'IMPORT_STATE') { 94 | const computedStates = message.payload.nextLiftedState?.computedStates || []; 95 | computedStates.forEach(({ state }: { state: any }, index: number) => { 96 | if (index === 0) { 97 | devtools?.init(state); 98 | } else { 99 | set(anAtom, state); 100 | } 101 | }); 102 | } 103 | }; 104 | 105 | switch (update.type) { 106 | case 'mount': { 107 | unsubscribe = isWriteable ? devtools.subscribe(listener) : undefined; 108 | if (devtools.shouldInit) devtools.init(get(anAtom)); 109 | } 110 | } 111 | } 112 | ); 113 | 114 | subscribeAtom.onMount = setAtom => { 115 | setAtom({ type: 'mount' }); 116 | return () => unsubscribe?.(); 117 | }; 118 | 119 | get(subscribeAtom); 120 | if (!isWriteable) { 121 | devtools.send(`${atomName} - ${new Date().toLocaleString()}`, get(anAtom)); 122 | } 123 | }), 124 | deepEqual 125 | ); 126 | -------------------------------------------------------------------------------- /src/atoms/intitial-data-atom.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'jotai'; 2 | import { atomFamily } from 'jotai/utils'; 3 | import deepEqual from 'fast-deep-equal/es6'; 4 | 5 | export const initialDataAtom = atomFamily(queryKey => { 6 | const anAtom = atom(undefined); 7 | anAtom.debugLabel = `initialDataAtom/${queryKey}`; 8 | return anAtom; 9 | }, deepEqual); 10 | -------------------------------------------------------------------------------- /src/atoms/react-query/infinite-query-key-observer.ts: -------------------------------------------------------------------------------- 1 | import { atomFamily } from 'jotai/utils'; 2 | import { InfiniteQueryObserver, InfiniteQueryObserverOptions, QueryKey } from 'react-query'; 3 | import { atom } from 'jotai'; 4 | import { getQueryClientAtom } from './query-client-atom'; 5 | import deepEqual from 'fast-deep-equal/es6'; 6 | 7 | export const infiniteQueryKeyObserver = atomFamily( 8 | queryKey => 9 | atom(get => { 10 | const queryClient = getQueryClientAtom(get); 11 | const options = queryClient.getQueryCache().find(queryKey)?.options || { 12 | queryKey, 13 | }; 14 | const defaultedOptions = queryClient.defaultQueryObserverOptions({ 15 | ...options, 16 | notifyOnChangeProps: [ 17 | 'isFetchingPreviousPage', 18 | 'isFetchingNextPage', 19 | 'hasNextPage', 20 | 'hasPreviousPage', 21 | ], 22 | }); 23 | const observer = new InfiniteQueryObserver( 24 | queryClient, 25 | defaultedOptions as InfiniteQueryObserverOptions 26 | ); 27 | return observer; 28 | }), 29 | deepEqual 30 | ); 31 | -------------------------------------------------------------------------------- /src/atoms/react-query/infinite-query-key-status-atom.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'jotai'; 2 | import { atomFamily, atomWithDefault } from 'jotai/utils'; 3 | import { InfiniteQueryObserverResult, QueryKey } from 'react-query'; 4 | import deepEqual from 'fast-deep-equal/es6'; 5 | import { infiniteQueryKeyObserver } from './infinite-query-key-observer'; 6 | 7 | export interface InfiniteQueryStatus { 8 | isFetchingNextPage: boolean; 9 | isFetchingPreviousPage: boolean; 10 | hasPreviousPage?: boolean; 11 | hasNextPage?: boolean; 12 | } 13 | 14 | export const infiniteStatusAtomFamily = atomFamily< 15 | QueryKey, 16 | InfiniteQueryStatus, 17 | InfiniteQueryStatus 18 | >( 19 | queryKey => 20 | atomWithDefault(get => { 21 | const observer = get(infiniteQueryKeyObserver(queryKey)); 22 | const { isFetchingPreviousPage, isFetchingNextPage, hasNextPage, hasPreviousPage } = 23 | observer.getCurrentResult(); 24 | return { 25 | isFetchingPreviousPage, 26 | isFetchingNextPage, 27 | hasNextPage, 28 | hasPreviousPage, 29 | }; 30 | }), 31 | deepEqual 32 | ); 33 | 34 | export const infiniteQueryKeyStatusAtom = atomFamily(queryKey => { 35 | return atom(get => { 36 | if (!queryKey) throw Error('infiniteQueryKeyStatusAtom: no query key found'); 37 | 38 | const statusAtom = infiniteStatusAtomFamily(queryKey); 39 | const observer = get(infiniteQueryKeyObserver(queryKey)); 40 | 41 | let setData: (data: any) => void = () => { 42 | throw new Error('infiniteQueryKeyStatusAtom: setting data without mount'); 43 | }; 44 | 45 | const listener = ({ 46 | isFetchingPreviousPage, 47 | isFetchingNextPage, 48 | hasNextPage, 49 | hasPreviousPage, 50 | }: InfiniteQueryObserverResult) => 51 | setData({ 52 | isFetchingPreviousPage, 53 | isFetchingNextPage, 54 | hasNextPage, 55 | hasPreviousPage, 56 | }); 57 | 58 | statusAtom.onMount = update => { 59 | setData = update; 60 | const unsubscribe = observer.subscribe(listener); 61 | return unsubscribe; 62 | }; 63 | 64 | return get(statusAtom); 65 | }); 66 | }, deepEqual); 67 | -------------------------------------------------------------------------------- /src/atoms/react-query/query-client-atom.ts: -------------------------------------------------------------------------------- 1 | import { queryClient } from '../../query-client'; 2 | import { atom, Getter } from 'jotai'; 3 | 4 | export const queryClientAtom = atom(queryClient); 5 | export const getQueryClientAtom = (get: Getter) => get(queryClientAtom); 6 | -------------------------------------------------------------------------------- /src/atoms/react-query/query-key-observer.ts: -------------------------------------------------------------------------------- 1 | import { atomFamily } from 'jotai/utils'; 2 | import { QueryKey, QueryObserver } from 'react-query'; 3 | import { atom } from 'jotai'; 4 | import { getQueryClientAtom } from './query-client-atom'; 5 | import deepEqual from 'fast-deep-equal/es6'; 6 | 7 | export const queryKeyObserver = atomFamily( 8 | queryKey => 9 | atom(get => { 10 | if (!queryKey) return; 11 | const queryClient = getQueryClientAtom(get); 12 | const options = queryClient.getQueryCache().find(queryKey)?.options || { queryKey }; 13 | const defaultedOptions = queryClient.defaultQueryObserverOptions({ 14 | ...options, 15 | notifyOnChangeProps: ['isFetching', 'isIdle', 'isSuccess', 'isStale'], 16 | }); 17 | return new QueryObserver(queryClient, defaultedOptions); 18 | }), 19 | deepEqual 20 | ); 21 | -------------------------------------------------------------------------------- /src/atoms/react-query/query-key-status-atom.ts: -------------------------------------------------------------------------------- 1 | import { atomFamily, atomWithDefault } from 'jotai/utils'; 2 | import { QueryKey, QueryObserverResult } from 'react-query'; 3 | import { queryKeyObserver } from './query-key-observer'; 4 | import deepEqual from 'fast-deep-equal/es6'; 5 | import { atom } from 'jotai'; 6 | 7 | export interface QueryStatus { 8 | isLoading: boolean; 9 | isFetching: boolean; 10 | isIdle: boolean; 11 | isSuccess: boolean; 12 | isStale: boolean; 13 | } 14 | 15 | export const queryStatusAtomFamily = atomFamily( 16 | queryKey => 17 | atomWithDefault(get => { 18 | const observer = get(queryKeyObserver(queryKey)); 19 | if (!observer) 20 | return { 21 | isLoading: false, 22 | isFetching: false, 23 | isIdle: false, 24 | isSuccess: false, 25 | isStale: false, 26 | }; 27 | const { isFetching, isLoading, isIdle, isSuccess, isStale } = observer?.getCurrentResult(); 28 | return { 29 | isLoading, 30 | isFetching, 31 | isIdle, 32 | isSuccess, 33 | isStale, 34 | }; 35 | }), 36 | deepEqual 37 | ); 38 | 39 | export const queryKeyStatusAtom = atomFamily(queryKey => { 40 | return atom(get => { 41 | if (!queryKey) throw Error('queryKeyObserver: no query key found'); 42 | 43 | const statusAtom = queryStatusAtomFamily(queryKey); 44 | const observer = get(queryKeyObserver(queryKey)); 45 | 46 | let setData: (data: any) => void = () => { 47 | throw new Error('queryKeyObserver: setting data without mount'); 48 | }; 49 | 50 | const listener = ({ isFetching, isLoading, isIdle, isSuccess, isStale }: QueryObserverResult) => 51 | setData({ 52 | isLoading, 53 | isFetching, 54 | isIdle, 55 | isSuccess, 56 | isStale, 57 | }); 58 | 59 | statusAtom.onMount = update => { 60 | setData = update; 61 | return observer?.subscribe(listener); 62 | }; 63 | 64 | return get(statusAtom); 65 | }); 66 | }, deepEqual); 67 | -------------------------------------------------------------------------------- /src/atoms/static-query.ts: -------------------------------------------------------------------------------- 1 | import { hashQueryKey, QueryKey } from 'react-query'; 2 | import { atom, Atom, Getter } from 'jotai'; 3 | import { atomFamily } from 'jotai/utils'; 4 | import { initialDataAtom } from 'jotai-query-toolkit'; 5 | import { Queries } from '../nextjs/types'; 6 | 7 | export function makeInitialDataAtom(queryKey: QueryKey) { 8 | return initialDataAtom(hashQueryKey(queryKey)); 9 | } 10 | 11 | function getInitialData(get: Getter, queryKey: QueryKey) { 12 | const result = get(makeInitialDataAtom(queryKey)); 13 | if (result) return result as ReturnType; 14 | } 15 | 16 | export function atomFamilyWithStaticQuery( 17 | getQueryKey: QueryKey | ((param: ParamType) => QueryKey), 18 | queryFn: (param: ParamType, get?: Getter) => ReturnType | Promise 19 | ) { 20 | return atomFamily>>(param => 21 | atom>(get => { 22 | const queryKey = typeof getQueryKey === 'function' ? getQueryKey(param) : getQueryKey; 23 | return getInitialData(get, queryKey) ?? queryFn(param, get); 24 | }) 25 | ); 26 | } 27 | 28 | export function queryFamilyFactory( 29 | getQueryKey: QueryKey | ((param: ParamType) => QueryKey), 30 | queryFn: (param: ParamType) => ReturnType | Promise 31 | ) { 32 | return function queryFamilyBuilder(param: ParamType): Queries[number] { 33 | const queryKey = typeof getQueryKey === 'function' ? getQueryKey(param) : getQueryKey; 34 | return [queryKey, () => queryFn(param)] as const; 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /src/atoms/types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | InfiniteData, 3 | InfiniteQueryObserverOptions, 4 | QueryFunctionContext, 5 | QueryKey, 6 | QueryObserverOptions, 7 | } from 'react-query'; 8 | import { Atom, Getter, PrimitiveAtom } from 'jotai'; 9 | 10 | export interface BaseQueryAtomCustomOptions { 11 | equalityFn?: (a: Data, b: Data) => boolean; 12 | queryKeyAtom?: Atom | PrimitiveAtom; 13 | getShouldRefetch?: (initialData: Data) => boolean; 14 | } 15 | 16 | export type AtomWithQueryOptions = QueryObserverOptions & 17 | BaseQueryAtomCustomOptions; 18 | 19 | export interface AtomWithInfiniteQueryOptions 20 | extends InfiniteQueryObserverOptions { 21 | equalityFn?: (a: InfiniteData, b: InfiniteData) => boolean; 22 | getShouldRefetch?: (initialData: InfiniteData) => boolean; 23 | queryKeyAtom?: Atom | PrimitiveAtom; 24 | } 25 | 26 | export type AtomWithQueryFn = ( 27 | get: Getter, 28 | context: QueryFunctionContext 29 | ) => Data | Promise; 30 | export type AtomWithInfiniteQueryFn = ( 31 | get: Getter, 32 | context: QueryFunctionContext 33 | ) => Data | Promise; 34 | 35 | export type InfiniteQueryDispatch = 36 | | { type: 'mount' } 37 | | { type: 'next' } 38 | | { type: 'prev' } 39 | | { type: 'refresh' }; 40 | 41 | export interface ListParams { 42 | limit: number; 43 | offset: number; 44 | } 45 | 46 | export type ParamWithListParams = [param: T, options: ListParams]; 47 | 48 | export type AtomFamilyWithQueryFn = ( 49 | get: Getter, 50 | param: Param, 51 | context: QueryFunctionContext 52 | ) => Data | Promise; 53 | 54 | export type AtomFamilyWithInfiniteQueryFn = ( 55 | get: Getter, 56 | param: Param, 57 | context: QueryFunctionContext 58 | ) => Data | Promise; 59 | 60 | type ShouldRemove = (createdAt: number, param: Param) => boolean; 61 | 62 | export type AtomFamily = { 63 | (param: Param): AtomType; 64 | remove(param: Param): void; 65 | setShouldRemove(shouldRemove: ShouldRemove | null): void; 66 | }; 67 | 68 | export type GetQueryKey = (get: Getter, param: Param) => QueryKey; 69 | export type QueryKeyOrGetQueryKey = QueryKey | GetQueryKey; 70 | -------------------------------------------------------------------------------- /src/atoms/utils/as-infinite-data.ts: -------------------------------------------------------------------------------- 1 | import { InfiniteData } from 'react-query'; 2 | 3 | export function asInfiniteData(data: Data): InfiniteData | undefined { 4 | if (!data) return; 5 | if ('pages' in data && 'pageParams' in data) return data as unknown as InfiniteData; 6 | return { 7 | pages: [data], 8 | pageParams: [undefined], 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /src/atoms/utils/get-query-key.ts: -------------------------------------------------------------------------------- 1 | import { Atom, Getter } from 'jotai'; 2 | import { hashQueryKey, QueryKey } from 'react-query'; 3 | 4 | import { QueryKeyOrGetQueryKey } from '../types'; 5 | import { makeQueryKey } from '../../utils'; 6 | 7 | export const getQueryKey = ( 8 | get: Getter, 9 | getKey: QueryKeyOrGetQueryKey, 10 | param?: Param, 11 | queryKeyAtom?: Atom 12 | ) => { 13 | const key = typeof getKey === 'function' ? getKey(get, param as Param) : getKey; 14 | // check so we don't include it more than once 15 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument 16 | const hashedParams = param ? hashQueryKey(param as any).slice(1, -1) : undefined; 17 | const hashedKey = hashQueryKey(key); 18 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument 19 | const hashedContainsParams = hashedKey?.includes(hashedParams as any); 20 | 21 | // todo: should probably deprecate this 22 | if (queryKeyAtom) { 23 | const qkAtomValue = get(queryKeyAtom); 24 | return makeQueryKey(key, hashedContainsParams ? qkAtomValue : [param, qkAtomValue]); 25 | } 26 | 27 | // do not include params more than 1 time 28 | if (hashedContainsParams) return makeQueryKey(key); 29 | 30 | // params not included, so we should include them 31 | return makeQueryKey(key, param); 32 | }; 33 | 34 | export function getKeys( 35 | get: Getter, 36 | key: QueryKeyOrGetQueryKey, 37 | param?: Param, 38 | queryKeyAtom?: Atom 39 | ) { 40 | const queryKey = getQueryKey(get, key, param, queryKeyAtom); 41 | const hashedQueryKey = hashQueryKey(queryKey); 42 | return { queryKey, hashedQueryKey }; 43 | } 44 | 45 | export function makeHashedQueryKey(key: QueryKey, param?: Param) { 46 | return hashQueryKey(makeQueryKey(key, param)); 47 | } 48 | 49 | export function makeDebugLabel(atomLabel: string, key: QueryKey, param?: Param) { 50 | return `${atomLabel}/${makeHashedQueryKey(key, param)}`; 51 | } 52 | -------------------------------------------------------------------------------- /src/atoms/utils/weak-cache.ts: -------------------------------------------------------------------------------- 1 | export type WeakCache = WeakMap] | [WeakCache, T]>; 2 | 3 | export const getWeakCacheItem = ( 4 | cache: WeakCache, 5 | deps: readonly object[] 6 | ): T | undefined => { 7 | const [dep, ...rest] = deps; 8 | const entry = cache.get(dep); 9 | if (!entry) { 10 | return; 11 | } 12 | if (!rest.length) { 13 | return entry[1]; 14 | } 15 | return getWeakCacheItem(entry[0], rest); 16 | }; 17 | 18 | export const setWeakCacheItem = ( 19 | cache: WeakCache, 20 | deps: readonly object[], 21 | item: T 22 | ): void => { 23 | const [dep, ...rest] = deps; 24 | let entry = cache.get(dep); 25 | if (!entry) { 26 | entry = [new WeakMap()]; 27 | cache.set(dep, entry); 28 | } 29 | if (!rest.length) { 30 | entry[1] = item; 31 | return; 32 | } 33 | setWeakCacheItem(entry[0], rest, item); 34 | }; 35 | -------------------------------------------------------------------------------- /src/cache.ts: -------------------------------------------------------------------------------- 1 | import type { WritableAtom } from 'jotai'; 2 | 3 | export type WeakCache = WeakMap] | [WeakCache, T]>; 4 | export type Cache = WeakMap; 5 | 6 | export const makeWeakCache = (): WeakCache => new WeakMap(); 7 | 8 | export const getCacheItem = (cache: Cache, dep: object): T | undefined => { 9 | const entry = cache.get(dep); 10 | if (entry) return entry; 11 | }; 12 | export const setCacheItem = (cache: Cache, dep: object, item: T): void => { 13 | if (!cache.get(dep as object)) cache.set(dep as object, item as T); 14 | }; 15 | 16 | export const getWeakCacheItem = ( 17 | cache: WeakCache, 18 | deps: readonly object[] 19 | ): T | undefined => { 20 | while (true) { 21 | const [dep, ...rest] = deps; 22 | const entry = cache.get(dep as object); 23 | if (!entry) { 24 | return; 25 | } 26 | if (!rest.length) { 27 | return entry[1]; 28 | } 29 | cache = entry[0]; 30 | deps = rest; 31 | } 32 | }; 33 | 34 | export const setWeakCacheItem = ( 35 | cache: WeakCache, 36 | deps: readonly object[], 37 | item: T 38 | ): void => { 39 | while (true) { 40 | const [dep, ...rest] = deps; 41 | let entry = cache.get(dep as object); 42 | if (!entry) { 43 | entry = [new WeakMap()]; 44 | cache.set(dep as object, entry); 45 | } 46 | if (!rest.length) { 47 | entry[1] = item; 48 | return; 49 | } 50 | cache = entry[0]; 51 | deps = rest; 52 | } 53 | }; 54 | 55 | export const createMemoizeAtom = () => { 56 | const cache: WeakCache> = new WeakMap(); 57 | const memoizeAtom = , Deps extends readonly object[]>( 58 | createAtom: () => AtomType, 59 | deps: Deps 60 | ) => { 61 | const cachedAtom = getWeakCacheItem(cache, deps); 62 | if (cachedAtom) { 63 | return cachedAtom as AtomType; 64 | } 65 | const createdAtom = createAtom(); 66 | setWeakCacheItem(cache, deps, createdAtom); 67 | return createdAtom; 68 | }; 69 | return memoizeAtom; 70 | }; 71 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const IS_SSR = typeof window === 'undefined'; 2 | export const IS_DEV = typeof process === 'object' && process?.env?.NODE_ENV !== 'production'; 3 | export const QueryRefreshRates: Record<'Default' | 'Fast' | 'RealTime' | 'None', number | false> = { 4 | Default: 10000, 5 | Fast: 5000, 6 | RealTime: 2000, 7 | None: false, 8 | }; 9 | -------------------------------------------------------------------------------- /src/devtools.ts: -------------------------------------------------------------------------------- 1 | import { createElement } from 'react'; 2 | import { useAtomValue } from 'jotai/utils'; 3 | 4 | import { QueryClientProvider } from 'react-query'; 5 | import { ReactQueryDevtools } from 'react-query/devtools'; 6 | import { memo } from 'react'; 7 | import { queryClientAtom } from './atoms/react-query/query-client-atom'; 8 | 9 | export const JqtDevtools = memo(() => { 10 | if (process.env.NODE_ENV === 'production') return null; 11 | const client = useAtomValue(queryClientAtom); 12 | if (!client) return null; 13 | return createElement(QueryClientProvider, { client }, createElement(ReactQueryDevtools)); 14 | }); 15 | -------------------------------------------------------------------------------- /src/hooks/use-infinite-query-atom.ts: -------------------------------------------------------------------------------- 1 | import { useCallbackOne, useMemoOne } from 'use-memo-one'; 2 | import { atom } from 'jotai'; 3 | import { useAtomCallback, useAtomValue } from 'jotai/utils'; 4 | 5 | import { infiniteQueryKeyStatusAtom } from '../atoms/react-query/infinite-query-key-status-atom'; 6 | import { makeMessage, queryKeyCache } from '../utils'; 7 | import { getCacheItem } from '../cache'; 8 | 9 | import type { WritableAtom } from 'jotai'; 10 | import type { InfiniteQueryStatus } from '../atoms/react-query/infinite-query-key-status-atom'; 11 | import type { InfiniteData, QueryKey } from 'react-query'; 12 | import type { AtomWithInfiniteQueryAction } from 'jotai/query'; 13 | import { Atom } from 'jotai/core/atom'; 14 | 15 | const noopAtom = atom(undefined); 16 | 17 | const conditionalQueryKeyAtom = (queryKey: QueryKey | undefined) => { 18 | if (!queryKey) return noopAtom; 19 | return infiniteQueryKeyStatusAtom(queryKey); 20 | }; 21 | 22 | export interface UseInfiniteQueryAtomBaseExtras { 23 | fetchNextPage: () => void; 24 | fetchPreviousPage: () => void; 25 | refetch: () => void; 26 | } 27 | 28 | export interface OptionalStatus extends UseInfiniteQueryAtomBaseExtras { 29 | isFetchingPreviousPage: boolean; 30 | isFetchingNextPage: boolean; 31 | hasNextPage: boolean; 32 | hasPreviousPage: boolean; 33 | } 34 | 35 | export function useInfiniteQueryAtom( 36 | anAtom: WritableAtom | undefined, AtomWithInfiniteQueryAction> 37 | ): [InfiniteData | undefined, OptionalStatus] { 38 | const memoizedAtom = useMemoOne(() => anAtom, [anAtom]); 39 | const value = useAtomValue | undefined>(memoizedAtom); 40 | const queryKey = getCacheItem(queryKeyCache, memoizedAtom); 41 | 42 | const withQueryKeyWarning = useCallbackOne(() => { 43 | if (!queryKey) 44 | console.warn( 45 | makeMessage( 46 | `no query key was found for ${memoizedAtom.debugLabel || memoizedAtom.toString()}` 47 | ) 48 | ); 49 | }, [queryKey, memoizedAtom]); 50 | 51 | const statusAtom = useMemoOne>(() => conditionalQueryKeyAtom(queryKey), [queryKey]); 52 | const status = useAtomValue(statusAtom); 53 | 54 | const fetchNextPage = useAtomCallback( 55 | useCallbackOne( 56 | (get, set) => { 57 | withQueryKeyWarning(); 58 | set(memoizedAtom, { type: 'fetchNextPage' }); 59 | }, 60 | [memoizedAtom] 61 | ) 62 | ); 63 | const fetchPreviousPage = useAtomCallback( 64 | useCallbackOne( 65 | (get, set) => { 66 | withQueryKeyWarning(); 67 | set(memoizedAtom, { type: 'fetchPreviousPage' }); 68 | }, 69 | [memoizedAtom] 70 | ) 71 | ); 72 | const refetch = useAtomCallback( 73 | useCallbackOne( 74 | (get, set) => { 75 | withQueryKeyWarning(); 76 | set(memoizedAtom, { type: 'refetch' }); 77 | }, 78 | [memoizedAtom] 79 | ) 80 | ); 81 | 82 | const optionalStatus = { 83 | isFetchingPreviousPage: (status as InfiniteQueryStatus)?.isFetchingPreviousPage || false, 84 | isFetchingNextPage: (status as InfiniteQueryStatus)?.isFetchingNextPage || false, 85 | hasNextPage: !!(status as InfiniteQueryStatus)?.hasNextPage || false, 86 | hasPreviousPage: !!(status as InfiniteQueryStatus)?.hasPreviousPage || false, 87 | }; 88 | 89 | return [ 90 | value, 91 | { 92 | fetchNextPage, 93 | fetchPreviousPage, 94 | refetch, 95 | ...optionalStatus, 96 | }, 97 | ]; 98 | } 99 | -------------------------------------------------------------------------------- /src/hooks/use-query-atom.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useMemo } from 'react'; 2 | import { atom } from 'jotai'; 3 | import { useAtomCallback, useAtomValue } from 'jotai/utils'; 4 | 5 | import { makeMessage, queryKeyCache } from '../utils'; 6 | import { getWeakCacheItem } from '../cache'; 7 | 8 | import type { WritableAtom } from 'jotai'; 9 | import type { QueryKey, SetDataOptions } from 'react-query'; 10 | import { queryKeyStatusAtom, QueryStatus } from '../atoms/react-query/query-key-status-atom'; 11 | import { JQTAtomWithQueryActions } from '../atoms/atom-with-query'; 12 | import { queryKeyObserver } from '../atoms/react-query/query-key-observer'; 13 | import { getQueryClientAtom } from '../atoms/react-query/query-client-atom'; 14 | import { IS_DEV } from '../constants'; 15 | 16 | const noopAtom = atom(undefined); 17 | 18 | const conditionalQueryKeyAtom = (queryKey: QueryKey | undefined) => { 19 | if (!queryKey) return noopAtom; 20 | return queryKeyStatusAtom(queryKey); 21 | }; 22 | 23 | export interface UseQueryAtomBaseExtras extends QueryStatus { 24 | refetch: () => void; 25 | setQueryData: ({ data, options }: { data: T; options?: SetDataOptions }) => void; 26 | } 27 | 28 | function makeErrorLog(anAtom?: any) { 29 | if (IS_DEV) 30 | console.error( 31 | makeMessage( 32 | `no query key was found for ${ 33 | anAtom.debugLabel || anAtom.toString() || 'UnknownAtom' 34 | }, is it an atomFamilyWithQuery atom?` 35 | ) 36 | ); 37 | } 38 | 39 | export function useQueryAtom( 40 | anAtom: WritableAtom> 41 | ): [T extends Promise ? V : T, UseQueryAtomBaseExtras] { 42 | const atom = useMemo(() => anAtom, [anAtom]); 43 | const value = useAtomValue(atom); 44 | const deps = [atom] as const; 45 | const queryKey = getWeakCacheItem(queryKeyCache, deps); 46 | if (!queryKey) makeErrorLog(anAtom); 47 | 48 | const statusAtom = useMemo(() => conditionalQueryKeyAtom(queryKey), [queryKey]); 49 | const _status = useAtomValue(statusAtom); 50 | const refetch = useAtomCallback( 51 | useCallback( 52 | async get => { 53 | if (!queryKey) { 54 | makeErrorLog(anAtom); 55 | return; 56 | } 57 | const observer = get(queryKeyObserver(queryKey)); 58 | await observer?.refetch(); 59 | }, 60 | [anAtom, queryKey] 61 | ) 62 | ); 63 | 64 | const setQueryData = useAtomCallback< 65 | void, 66 | { 67 | data: T; 68 | options?: SetDataOptions; 69 | } 70 | >( 71 | useCallback( 72 | async (get, set, payload) => { 73 | if (!queryKey) { 74 | makeErrorLog(anAtom); 75 | return; 76 | } 77 | const queryClient = getQueryClientAtom(get); 78 | await queryClient.getQueryCache().find(queryKey)?.setData(payload.data, payload.options); 79 | }, 80 | [anAtom, queryKey] 81 | ) 82 | ); 83 | 84 | const status = _status || {}; 85 | return [ 86 | value, 87 | { 88 | refetch, 89 | setQueryData, 90 | ...(status as QueryStatus), 91 | }, 92 | ]; 93 | } 94 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | //--------------------- 2 | // atoms 3 | //--------------------- 4 | export { atomFamilyWithQuery } from './atoms/atom-family-with-query'; 5 | export { atomFamilyWithInfiniteQuery } from './atoms/atom-family-with-infinite-query'; 6 | export { atomWithQuery } from './atoms/atom-with-query'; 7 | export { atomWithInfiniteQuery } from './atoms/atom-with-infinite-query'; 8 | export * from './atoms/react-query/query-client-atom'; 9 | export { initialDataAtom } from './atoms/intitial-data-atom'; 10 | export { infiniteQueryKeyStatusAtom } from './atoms/react-query/infinite-query-key-status-atom'; 11 | export { queryKeyStatusAtom } from './atoms/react-query/query-key-status-atom'; 12 | export { queryKeyObserver } from './atoms/react-query/query-key-observer'; 13 | export { infiniteQueryKeyObserver } from './atoms/react-query/infinite-query-key-observer'; 14 | export { devtoolAtom } from './atoms/devtool-atom'; 15 | export * from './atoms/static-query'; 16 | 17 | //hooks 18 | export { useInfiniteQueryAtom } from './hooks/use-infinite-query-atom'; 19 | export { useQueryAtom } from './hooks/use-query-atom'; 20 | export type { 21 | UseInfiniteQueryAtomBaseExtras, 22 | OptionalStatus, 23 | } from './hooks/use-infinite-query-atom'; 24 | export type { UseQueryAtomBaseExtras } from './hooks/use-query-atom'; 25 | 26 | // types 27 | export type { 28 | AtomFamilyWithQueryFn, 29 | AtomWithQueryOptions, 30 | AtomWithInfiniteQueryOptions, 31 | InfiniteQueryDispatch, 32 | AtomWithQueryFn, 33 | AtomWithInfiniteQueryFn, 34 | ParamWithListParams, 35 | ListParams, 36 | } from './atoms/types'; 37 | export type { InfiniteQueryStatus } from './atoms/react-query/infinite-query-key-status-atom'; 38 | export type { QueryStatus } from './atoms/react-query/query-key-status-atom'; 39 | //--------------------- 40 | // utils + misc 41 | //--------------------- 42 | export { queryClient } from './query-client'; 43 | export { makeQueryKey, queryKeyCache } from './utils'; 44 | export { QueryRefreshRates, IS_SSR } from './constants'; 45 | -------------------------------------------------------------------------------- /src/nextjs.ts: -------------------------------------------------------------------------------- 1 | //--------------------- 2 | // next.js specific 3 | //--------------------- 4 | export { withInitialQueries } from './nextjs/with-initial-queries'; 5 | export { IS_SSR } from './constants'; 6 | export { getInitialPropsFromQueries } from './nextjs/get-initial-props-from-queries'; 7 | export { getDataFromQueryArray } from './nextjs/query-helpers'; 8 | export { getCachedQueryData } from './nextjs/query-helpers'; 9 | export { getSingleCachedQueryData } from './nextjs/query-helpers'; 10 | export { useQueryInitialValues } from './nextjs/use-query-initial-values'; 11 | export { buildInitialValueAtoms } from './nextjs/build-initial-value-atoms'; 12 | export { JqtDevtools } from './devtools'; 13 | export type { 14 | QueryPropsGetter, 15 | GetQueries, 16 | Queries, 17 | Query, 18 | GetQueryKey, 19 | Fetcher, 20 | InitialValuesAtomBuilder, 21 | GetInitialPropsFromQueriesOptions, 22 | QueryPropsDefault, 23 | } from './nextjs/types'; 24 | 25 | export { getStaticQueryProps } from './nextjs/get-static-query-props'; 26 | export { getServerSideQueryProps } from './nextjs/get-server-side-query-props'; 27 | export { getInitialQueryProps } from './nextjs/get-initial-query-props'; 28 | export { withInitialQueryData } from './nextjs/intial-queries-wrapper'; 29 | -------------------------------------------------------------------------------- /src/nextjs/build-initial-value-atoms.ts: -------------------------------------------------------------------------------- 1 | import { InitialValuesAtomBuilder } from './types'; 2 | 3 | export function buildInitialValueAtoms( 4 | props: Record, 5 | atomBuilders: InitialValuesAtomBuilder[] 6 | ) { 7 | return atomBuilders.map(([propKey, builder]) => { 8 | const propData = props[propKey]; 9 | return builder(propData); 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /src/nextjs/get-initial-props-from-queries.ts: -------------------------------------------------------------------------------- 1 | import { hashQueryKey, QueryKey } from 'react-query'; 2 | import { 3 | Fetcher, 4 | GetInitialPropsFromQueriesOptions, 5 | GetQueryKey, 6 | QueryPropsDefault, 7 | } from './types'; 8 | import { getCachedQueryData } from './query-helpers'; 9 | import { IS_DEV } from '../constants'; 10 | import { makeMessage } from '../utils'; 11 | 12 | /** 13 | * getInitialPropsFromQueries 14 | * 15 | * This is the main function that gives us a great developer and user experience when it comes to 16 | * fetching data on the server and making use of it on the client via atom state. 17 | * 18 | * This method will either find an existing bit of data/state cached via react-query 19 | * or it will fetch the data via the async fetcher provided to it. 20 | * 21 | * This is important because we only want to fetch our data in `getInitialProps` if there is no cached data. 22 | * 23 | *```typescript 24 | * SomeNextPage.getInitialProps = async (context: NextPageContext) => { 25 | * const queries = [[SomeEnum.SomeKey, async () => fetchData()]]; 26 | * const pageProps = await getInitialPropsFromQueries(queries); 27 | * 28 | * return pageProps 29 | * } 30 | * ``` 31 | * @param options - {@link GetInitialPropsFromQueriesOptions} the object of options for this method 32 | * @return Returns an object of the hashed query keys and data result from the fetcher associated with it, to be consumed by {@see useQueryInitialValues} 33 | */ 34 | export async function getInitialPropsFromQueries( 35 | options: GetInitialPropsFromQueriesOptions 36 | ) { 37 | try { 38 | const { getQueries, ctx, getQueryProps, queryClient } = options; 39 | 40 | const queryProps: QueryProps | undefined = getQueryProps 41 | ? await getQueryProps(ctx, queryClient) 42 | : undefined; 43 | 44 | const getQueryKey = (queryKey: GetQueryKey | QueryKey) => { 45 | if (typeof queryKey === 'function') return queryKey(ctx, queryProps, queryClient); 46 | return queryKey; 47 | }; 48 | 49 | const _queries = 50 | typeof getQueries === 'function' 51 | ? await getQueries(ctx, queryProps, queryClient) 52 | : getQueries; 53 | 54 | if (!_queries) return {}; 55 | 56 | const queries = ( 57 | await Promise.all( 58 | _queries 59 | .filter(([queryKey]) => !!queryKey) 60 | .map(async ([queryKey, fetcher]) => [await getQueryKey(queryKey!), fetcher]) 61 | ) 62 | ).filter(([queryKey]) => queryKey) as [QueryKey, Fetcher][]; 63 | // let's extract only the query keys 64 | const queryKeys = queries.map(([queryKey]) => queryKey); 65 | 66 | if (queryKeys.length === 0) { 67 | if (IS_DEV) console.error(makeMessage('getInitialPropsFromQueries -> no query keys')); 68 | return {}; 69 | } 70 | 71 | // see if we have any cached in the query client 72 | const data = getCachedQueryData(queryKeys, queryClient) || {}; 73 | const dataKeys = Object.keys(data); 74 | const allArgsAreCached = dataKeys.length === queries.length; 75 | // everything is cached, let's return it now 76 | if (allArgsAreCached) return data; 77 | // some or none of the args weren't available, as such we need to fetch them 78 | const results = await Promise.all( 79 | queries 80 | // filter the items away that are already cached 81 | .filter(([queryKey]) => { 82 | const valueExists = !!data[hashQueryKey(queryKey)]; 83 | return !valueExists; 84 | }) 85 | // map through and fetch the data for each 86 | .map(async ([queryKey, fetcher]) => { 87 | const value = await fetcher(ctx, queryProps, queryClient); 88 | return [queryKey, value] as [QueryKey, typeof value]; 89 | }) 90 | ); 91 | 92 | results.forEach(([queryKey, result]) => { 93 | // add them to the data object 94 | data[hashQueryKey(queryKey)] = result; 95 | }); 96 | // and return them! 97 | return data; 98 | } catch (e: any) { 99 | if (IS_DEV) console.error(makeMessage(e?.message as string)); 100 | return { 101 | error: true, 102 | message: e.message, 103 | }; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/nextjs/get-initial-query-props.ts: -------------------------------------------------------------------------------- 1 | import { GetQueries, Queries, QueryPropsGetter } from './types'; 2 | import { NextPageContext } from 'next'; 3 | import { getInitialPropsFromQueries } from './get-initial-props-from-queries'; 4 | import { queryClient } from 'jotai-query-toolkit'; 5 | 6 | export type GetInitialProps = ( 7 | context: NextPageContext 8 | ) => PageProps | Promise; 9 | 10 | export function getInitialQueryProps( 11 | getQueries: Queries | GetQueries, 12 | getQueryProps?: QueryPropsGetter 13 | ) { 14 | return (getInitialProps?: GetInitialProps) => { 15 | return async (ctx: NextPageContext): Promise => { 16 | async function _getInitialProps(): Promise<{} | PageProps> { 17 | try { 18 | if (getInitialProps) return getInitialProps(ctx); 19 | return {}; 20 | } catch (e) { 21 | console.error( 22 | `[jotai-query-toolkit] getInitialQueryProps: getInitialProps failed. message:` 23 | ); 24 | console.error(e); 25 | return {}; 26 | } 27 | } 28 | 29 | const promises: Promise[] = [ 30 | getInitialPropsFromQueries({ 31 | getQueries, 32 | getQueryProps, 33 | ctx, 34 | queryClient, 35 | }), 36 | _getInitialProps(), 37 | ]; 38 | 39 | const [initialQueryData, initialProps] = await Promise.all(promises); 40 | 41 | return { 42 | ...initialProps, 43 | initialQueryData, 44 | }; 45 | }; 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /src/nextjs/get-server-side-query-props.ts: -------------------------------------------------------------------------------- 1 | import { getInitialPropsFromQueries } from './get-initial-props-from-queries'; 2 | import { queryClient } from 'jotai-query-toolkit'; 3 | import type { GetServerSideProps, GetServerSidePropsContext, GetServerSidePropsResult } from 'next'; 4 | import type { GetQueries, Queries, QueryPropsGetter } from './types'; 5 | 6 | export function getServerSideQueryProps( 7 | getQueries?: Queries | GetQueries, 8 | getQueryProps?: QueryPropsGetter 9 | ) { 10 | return (getServerSideProps?: GetServerSideProps) => { 11 | return async (ctx: GetServerSidePropsContext): Promise> => { 12 | const _getServerSideProps = async (): Promise< 13 | { props: {} } | GetServerSidePropsResult 14 | > => { 15 | if (getServerSideProps) return getServerSideProps(ctx); 16 | return { props: {} }; 17 | }; 18 | 19 | const promises: Promise[] = [_getServerSideProps()]; 20 | 21 | if (typeof getQueries !== 'undefined') { 22 | promises.push( 23 | getInitialPropsFromQueries({ 24 | getQueries, 25 | getQueryProps, 26 | ctx, 27 | queryClient, 28 | }) 29 | ); 30 | } 31 | 32 | const [serverProps, initialQueryData] = await Promise.all(promises); 33 | 34 | return { 35 | props: { 36 | ...(serverProps?.props || {}), 37 | initialQueryData, 38 | }, 39 | }; 40 | }; 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /src/nextjs/get-static-query-props.ts: -------------------------------------------------------------------------------- 1 | import { GetStaticProps, GetStaticPropsContext, GetStaticPropsResult } from 'next'; 2 | import { GetQueries, Queries, QueryPropsGetter } from './types'; 3 | import { getInitialPropsFromQueries } from './get-initial-props-from-queries'; 4 | import { queryClient } from 'jotai-query-toolkit'; 5 | 6 | export function getStaticQueryProps( 7 | getQueries: Queries | GetQueries, 8 | getQueryProps?: QueryPropsGetter 9 | ) { 10 | return (getStaticProps?: GetStaticProps) => { 11 | return async (ctx: GetStaticPropsContext): Promise> => { 12 | const _getStaticProps = async () => { 13 | if (getStaticProps) return getStaticProps(ctx); 14 | return { props: {} }; 15 | }; 16 | 17 | const promises: Promise[] = [ 18 | getInitialPropsFromQueries({ 19 | getQueries, 20 | getQueryProps, 21 | ctx, 22 | queryClient, 23 | }), 24 | _getStaticProps(), 25 | ]; 26 | 27 | const [initialQueryData, staticProps] = await Promise.all(promises); 28 | 29 | return { 30 | ...staticProps, 31 | props: { 32 | ...staticProps.props, 33 | initialQueryData, 34 | }, 35 | }; 36 | }; 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /src/nextjs/intial-queries-wrapper.ts: -------------------------------------------------------------------------------- 1 | import { createElement } from 'react'; 2 | import { Provider } from 'jotai'; 3 | import { useGetProviderInitialValues } from './use-get-initial-query-props'; 4 | 5 | import type { NextPage } from 'next'; 6 | import type { InitialValuesAtomBuilder } from './types'; 7 | 8 | /** 9 | * withInitialQueryData 10 | * 11 | * This is a higher-order-component (HoC) that wraps a next.js page with queries that 12 | * are then fetched on the server and injected into a Jotai Provider. 13 | * 14 | * @typeParam QueryProps - optional, these types are for the generic global query props that each query has access to 15 | * @typeParam PageProps - the next.js page props 16 | * @param WrappedComponent - The next.js page component to wrap 17 | * @param initialValuesAtomBuilders - Optional values to add to our provider 18 | */ 19 | export function withInitialQueryData>( 20 | WrappedComponent: NextPage, 21 | initialValuesAtomBuilders?: InitialValuesAtomBuilder[] 22 | ) { 23 | const Wrapper: NextPage<{ 24 | initialQueryData?: Record; 25 | }> = props => { 26 | const { initialValues, key, ...childProps } = useGetProviderInitialValues( 27 | props, 28 | initialValuesAtomBuilders 29 | ); 30 | return createElement( 31 | Provider, 32 | { initialValues, key }, 33 | createElement(WrappedComponent, childProps as PageProps) 34 | ); 35 | }; 36 | 37 | return Wrapper as unknown as NextPage; 38 | } 39 | -------------------------------------------------------------------------------- /src/nextjs/query-helpers.ts: -------------------------------------------------------------------------------- 1 | import { hashQueryKey, QueryClient, QueryKey } from 'react-query'; 2 | 3 | /** 4 | * this function gets the QueryCache from our react-query queryClient 5 | * and looks for a query that might already be cached and returns it 6 | * 7 | * @param queryKey - {@link QueryKey} the query key we're interested in 8 | * @param queryClient - {@link QueryClient} the query client to check against 9 | */ 10 | export function getSingleCachedQueryData( 11 | queryKey: QueryKey, 12 | queryClient: QueryClient 13 | ): Data | undefined { 14 | const cache = queryClient.getQueryCache(); 15 | const queries = cache.getAll(); 16 | const hashedQueryKey = hashQueryKey(queryKey); 17 | const match = queries.find(query => query.queryHash === hashedQueryKey); 18 | if (match) return match?.state.data as Data; 19 | return undefined; 20 | } 21 | 22 | /** 23 | * this function gets the QueryCache from our react-query queryClient 24 | * and looks for any of the queries passed that might already be cached and returns thems 25 | * 26 | * @param queryKeys - {@link QueryKey} the query key we're interested in 27 | * @param queryClient - {@link QueryClient} the query client to check against 28 | */ 29 | export function getCachedQueryData(queryKeys: QueryKey[], queryClient: QueryClient) { 30 | const found: Record = {}; 31 | queryKeys.forEach(queryKey => { 32 | const match = getSingleCachedQueryData(queryKey, queryClient); 33 | if (match) found[hashQueryKey(queryKey)] = match; 34 | }); 35 | if (Object.keys(found).length) return found; 36 | } 37 | 38 | /** 39 | * Helper function to extract a query key's data from the response of {@link getInitialPropsFromQueries} 40 | * 41 | * @param queryKey - {@link QueryKey} the query key we're interested in 42 | * @param queryArray - An object where each key is a hashed query key and the value is the data associated with it 43 | */ 44 | export function getDataFromQueryArray( 45 | queryKey: QueryKey, 46 | queryArray: Record 47 | ) { 48 | return queryArray[hashQueryKey(queryKey)] as Data; 49 | } 50 | -------------------------------------------------------------------------------- /src/nextjs/types.ts: -------------------------------------------------------------------------------- 1 | import type { GetServerSidePropsContext, NextPageContext } from 'next'; 2 | import type { QueryClient, QueryKey } from 'react-query'; 3 | import type { Atom } from 'jotai/core/atom'; 4 | import { GetStaticPropsContext } from 'next'; 5 | 6 | export type QueryPropsDefault = unknown | undefined; 7 | 8 | export type Fetcher = ( 9 | ctx: NextPageContext | GetServerSidePropsContext | GetStaticPropsContext, 10 | queryProps?: QueryProps, 11 | queryClient?: QueryClient 12 | ) => Promise | Data; 13 | 14 | export type GetQueryKey = ( 15 | ctx: NextPageContext | GetServerSidePropsContext | GetStaticPropsContext, 16 | queryProps?: QueryProps, 17 | queryClient?: QueryClient 18 | ) => QueryKey | Promise | undefined; 19 | 20 | export type Query = [ 21 | queryKey: GetQueryKey | QueryKey | undefined, 22 | fetcher: Fetcher 23 | ]; 24 | export type Queries = Readonly>[]; 25 | 26 | export type GetQueries = ( 27 | ctx: NextPageContext | GetServerSidePropsContext | GetStaticPropsContext, 28 | queryProps?: QueryProps, 29 | queryClient?: QueryClient 30 | ) => Queries | Promise> | null; 31 | 32 | export type QueryPropsGetter = ( 33 | context: NextPageContext | GetServerSidePropsContext | GetStaticPropsContext, 34 | queryClient: QueryClient 35 | ) => QueryProps | Promise; 36 | 37 | export interface GetInitialPropsFromQueriesOptions { 38 | getQueries: GetQueries | Queries; 39 | ctx: NextPageContext | GetServerSidePropsContext | GetStaticPropsContext; 40 | getQueryProps?: QueryPropsGetter; 41 | queryClient: QueryClient; 42 | } 43 | 44 | export type InitialValuesAtomBuilder = [ 45 | propKey: Key, 46 | atomBuilder: (propData: unknown) => readonly [Atom, unknown] 47 | ]; 48 | -------------------------------------------------------------------------------- /src/nextjs/use-get-initial-query-props.ts: -------------------------------------------------------------------------------- 1 | import { Atom } from 'jotai/core/atom'; 2 | import { useQueryInitialValues } from './use-query-initial-values'; 3 | import { buildInitialValueAtoms } from './build-initial-value-atoms'; 4 | import { hashQueryKey } from 'react-query'; 5 | import { InitialValuesAtomBuilder } from './types'; 6 | import { useMemo } from 'react'; 7 | 8 | function useBuildInitialValues( 9 | props: Record, 10 | initialValuesAtomBuilders?: InitialValuesAtomBuilder[] 11 | ): [newProps: Record, values: Iterable, unknown]>] { 12 | if (!initialValuesAtomBuilders) return [props, []]; 13 | const initialValues = buildInitialValueAtoms( 14 | props as Record, 15 | initialValuesAtomBuilders 16 | ); 17 | initialValuesAtomBuilders.forEach(([propKey]) => { 18 | delete (props as Record)[propKey]; 19 | }); 20 | return [props, initialValues]; 21 | } 22 | 23 | export const useGetProviderInitialValues = ( 24 | { 25 | initialQueryData, 26 | ...props 27 | }: { 28 | [key: string]: unknown; 29 | initialQueryData?: Record; 30 | }, 31 | initialValuesAtomBuilders?: InitialValuesAtomBuilder[] 32 | ) => { 33 | const initialQueryValues: Iterable, unknown]> = 34 | useQueryInitialValues(initialQueryData); 35 | 36 | // this key is very important, without passing key={key} to the Provider, 37 | // it won't know to re-render if someone navigates within the same page component in next.js 38 | const key = useMemo( 39 | () => hashQueryKey(initialQueryData ? Object.keys(initialQueryData) : []), 40 | [initialQueryData] 41 | ); 42 | 43 | // sometimes apps require additional atoms to be set within this provider, 44 | // this will build the atoms and add them to our initialValues array 45 | const [updatedProps, initialAtomValues] = useBuildInitialValues(props, initialValuesAtomBuilders); 46 | 47 | const initialValues = useMemo( 48 | () => [...initialAtomValues, ...initialQueryValues], 49 | [initialQueryValues, initialAtomValues] 50 | ); 51 | 52 | return { 53 | initialValues, 54 | key, 55 | ...updatedProps, 56 | }; 57 | }; 58 | -------------------------------------------------------------------------------- /src/nextjs/use-query-initial-values.ts: -------------------------------------------------------------------------------- 1 | import { initialDataAtom } from 'jotai-query-toolkit'; 2 | import { Atom } from 'jotai/core/atom'; 3 | import { hashQueryKey } from 'react-query'; 4 | 5 | /** 6 | * useQueryInitialValues 7 | * 8 | * This hook is made to be used on next.js pages only to provide the initial data for our query atoms. 9 | * Note: This should only be used by advanced users, withInitialQueries makes use of this internally. 10 | * 11 | * ```typescript 12 | * const queryKeys = [SomeEnum.SomeKey]; 13 | * const props = { '["SomeEnum.SomeKey"]": { foo: "bar' } }; // this will be autogenerated via {@link getInitialPropsFromQueries}` 14 | * const initialValues = useQueryInitialValues(queryKeys, props); 15 | * ``` 16 | * 17 | * @param props - the data generated from {@link getInitialPropsFromQueries}, should not be created manually. 18 | */ 19 | export function useQueryInitialValues(props?: Record) { 20 | const queryKeys = props ? Object.keys(props) : []; 21 | const atoms = queryKeys.map(queryKey => { 22 | if (queryKey && props && !(queryKey in props)) 23 | throw Error(`[Jotai Query Toolkit] no initial data found for ${hashQueryKey(queryKey)}`); 24 | const value = props ? props[queryKey] : null; 25 | return [initialDataAtom(queryKey), value] as const; 26 | }); 27 | return [...atoms] as Iterable, unknown]>; 28 | } 29 | -------------------------------------------------------------------------------- /src/nextjs/with-initial-queries.ts: -------------------------------------------------------------------------------- 1 | import { withInitialQueryData } from './intial-queries-wrapper'; 2 | 3 | import type { NextPage } from 'next'; 4 | import type { InitialValuesAtomBuilder, GetQueries, Queries, QueryPropsGetter } from './types'; 5 | import { GetInitialProps, getInitialQueryProps } from './get-initial-query-props'; 6 | 7 | /** 8 | * withInitialQueries 9 | * 10 | * This is a higher-order-component (HoC) that wraps a next.js page with queries that 11 | * are then fetched on the server and injected into a Jotai Provider. 12 | * 13 | * @typeParam QueryProps - optional, these types are for the generic global query props that each query has access to 14 | * @typeParam PageProps - the next.js page props 15 | * @param WrappedComponent - The next.js page component to wrap 16 | * @param initialValuesAtomBuilders - Optional values to add to our provider 17 | * @param getInitialProps - Optional getInitialProps to be passed down to the wrapper 18 | */ 19 | export function withInitialQueries>( 20 | WrappedComponent: NextPage, 21 | initialValuesAtomBuilders?: InitialValuesAtomBuilder[], 22 | getInitialProps?: GetInitialProps 23 | ) { 24 | /** 25 | * withInitialQueriesWrapper 26 | * 27 | * the function that creates our wrapper component and fetches our queries in addition to the next.js page getInitialProps 28 | * 29 | * @param getQueries - the set of queries or getter function for getting the queries 30 | * @param getQueryProps - optional getter for additional context props that will be fed to getQueries 31 | */ 32 | function withInitialQueriesWrapper( 33 | getQueries?: Queries | GetQueries, 34 | getQueryProps?: QueryPropsGetter 35 | ): NextPage { 36 | const hasWrappedComponentGetInitialProps = 37 | typeof WrappedComponent.getInitialProps !== 'undefined'; 38 | const hasCustomGetInitialProps = typeof getInitialProps !== 'undefined'; 39 | 40 | // we only want to allow one instance of a getInitial props 41 | if (hasCustomGetInitialProps && hasWrappedComponentGetInitialProps) 42 | throw new TypeError( 43 | '[jotai-query-toolkit] withInitialQueries: The wrapped next.js page has getInitialProps defined, and getInitialProps was passed to withInitialQueries. Please only use one of these.' 44 | ); 45 | 46 | const Wrapper = withInitialQueryData(WrappedComponent, initialValuesAtomBuilders); 47 | 48 | if (typeof getQueries !== 'undefined') { 49 | Wrapper.getInitialProps = getInitialQueryProps( 50 | getQueries, 51 | getQueryProps 52 | // eslint-disable-next-line @typescript-eslint/unbound-method 53 | )(getInitialProps || WrappedComponent.getInitialProps); 54 | } else if (hasCustomGetInitialProps) { 55 | Wrapper.getInitialProps = getInitialProps; 56 | } else if (hasWrappedComponentGetInitialProps) { 57 | // eslint-disable-next-line @typescript-eslint/unbound-method 58 | Wrapper.getInitialProps = WrappedComponent.getInitialProps; 59 | } 60 | 61 | return Wrapper as unknown as NextPage; 62 | } 63 | 64 | return withInitialQueriesWrapper; 65 | } 66 | -------------------------------------------------------------------------------- /src/query-client.ts: -------------------------------------------------------------------------------- 1 | import { QueryClient } from 'react-query'; 2 | 3 | export const queryClient = new QueryClient({ 4 | defaultOptions: { 5 | queries: { 6 | cacheTime: 1000 * 60 * 60 * 12, // 12 hours 7 | notifyOnChangeProps: ['data', 'error'], 8 | }, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { makeWeakCache } from './cache'; 2 | import type { QueryKey } from 'react-query'; 3 | 4 | const spreadableSymbol = Symbol ? Symbol.isConcatSpreadable : undefined; 5 | 6 | function getTag(value: any) { 7 | if (value == null) { 8 | return value === undefined ? '[object Undefined]' : '[object Null]'; 9 | } 10 | return toString.call(value); 11 | } 12 | 13 | function isObjectLike(value: any) { 14 | return typeof value === 'object' && value !== null; 15 | } 16 | 17 | function isArguments(value: any) { 18 | return isObjectLike(value) && getTag(value) == '[object Arguments]'; 19 | } 20 | 21 | function isFlattenable(value: any) { 22 | return ( 23 | Array.isArray(value) || 24 | isArguments(value) || 25 | !!(spreadableSymbol && value && value[spreadableSymbol]) 26 | ); 27 | } 28 | 29 | export function makeQueryKey

(key: QueryKey, param?: P): [QueryKey, P] | QueryKey { 30 | const flattenedKey = isFlattenable(key) ? (key as any).flat() : key; 31 | const flattenedParam = param ? (isFlattenable(param) ? (param as any).flat(100) : param) : null; 32 | return flattenedParam ? [flattenedKey, flattenedParam].flat(100) : flattenedKey; 33 | } 34 | 35 | export const queryKeyCache = makeWeakCache(); 36 | 37 | export function makeMessage(message: string) { 38 | return `[jotai-query-toolkit] ${message}`; 39 | } 40 | 41 | export function makeError(message: string) { 42 | return new Error(makeMessage(message)); 43 | } 44 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "strict": true, 5 | "jsx": "preserve", 6 | "allowSyntheticDefaultImports": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "moduleResolution": "node", 10 | "baseUrl": ".", 11 | "paths": { 12 | "jotai-query-toolkit": ["./src"] 13 | } 14 | }, 15 | "include": ["src/**/*", "tests/**/*"], 16 | "exclude": ["node_modules", "dist"] 17 | } 18 | --------------------------------------------------------------------------------