Repository: pmndrs/jotai Branch: main Commit: c3db721a53d8 Files: 326 Total size: 960.9 KB Directory structure: gitextract_mot3st09/ ├── .codesandbox/ │ └── ci.json ├── .github/ │ ├── DISCUSSION_TEMPLATE/ │ │ └── bug-report.yml │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── config.yml │ ├── pull_request_template.md │ └── workflows/ │ ├── compressed-size.yml │ ├── ecosystem-ci.yml │ ├── livecodes-post-comment.yml │ ├── livecodes-preview.yml │ ├── preview-release.yml │ ├── publish.yml │ ├── stale-discussions.yml │ ├── test-multiple-builds.yml │ ├── test-multiple-versions.yml │ ├── test-old-typescript.yml │ └── test.yml ├── .gitignore ├── .livecodes/ │ └── react.json ├── .prettierignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── babel.config.mjs ├── benchmarks/ │ ├── .gitignore │ ├── simple-read.ts │ ├── simple-write.ts │ └── subscribe-write.ts ├── docs/ │ ├── basics/ │ │ ├── comparison.mdx │ │ ├── concepts.mdx │ │ ├── functional-programming-and-jotai.mdx │ │ └── showcase.mdx │ ├── core/ │ │ ├── atom.mdx │ │ ├── provider.mdx │ │ ├── store.mdx │ │ └── use-atom.mdx │ ├── extensions/ │ │ ├── cache.mdx │ │ ├── effect.mdx │ │ ├── immer.mdx │ │ ├── location.mdx │ │ ├── optics.mdx │ │ ├── query.mdx │ │ ├── redux.mdx │ │ ├── relay.mdx │ │ ├── scope.mdx │ │ ├── trpc.mdx │ │ ├── urql.mdx │ │ ├── valtio.mdx │ │ ├── xstate.mdx │ │ └── zustand.mdx │ ├── guides/ │ │ ├── async.mdx │ │ ├── atoms-in-atom.mdx │ │ ├── composing-atoms.mdx │ │ ├── core-internals.mdx │ │ ├── debugging.mdx │ │ ├── initialize-atom-on-render.mdx │ │ ├── migrating-to-v2-api.mdx │ │ ├── nextjs.mdx │ │ ├── performance.mdx │ │ ├── persistence.mdx │ │ ├── react-native.mdx │ │ ├── remix.mdx │ │ ├── resettable.mdx │ │ ├── testing.mdx │ │ ├── typescript.mdx │ │ ├── using-store-outside-react.mdx │ │ ├── vite.mdx │ │ └── waku.mdx │ ├── index.mdx │ ├── recipes/ │ │ ├── atom-with-broadcast.mdx │ │ ├── atom-with-compare.mdx │ │ ├── atom-with-debounce.mdx │ │ ├── atom-with-listeners.mdx │ │ ├── atom-with-refresh-and-default.mdx │ │ ├── atom-with-refresh.mdx │ │ ├── atom-with-toggle-and-storage.mdx │ │ ├── atom-with-toggle.mdx │ │ ├── custom-useatom-hooks.mdx │ │ ├── large-objects.mdx │ │ ├── use-atom-effect.mdx │ │ └── use-reducer-atom.mdx │ ├── third-party/ │ │ ├── bunja.mdx │ │ ├── derive.mdx │ │ └── history.mdx │ ├── tools/ │ │ ├── babel.mdx │ │ ├── devtools.mdx │ │ └── swc.mdx │ └── utilities/ │ ├── async.mdx │ ├── callback.mdx │ ├── family.mdx │ ├── lazy.mdx │ ├── reducer.mdx │ ├── resettable.mdx │ ├── select.mdx │ ├── split.mdx │ ├── ssr.mdx │ └── storage.mdx ├── eslint.config.mjs ├── examples/ │ ├── hacker_news/ │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── public/ │ │ │ └── index.html │ │ ├── src/ │ │ │ ├── App.tsx │ │ │ ├── index.tsx │ │ │ ├── styles.css │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ ├── hello/ │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── public/ │ │ │ └── index.html │ │ ├── src/ │ │ │ ├── App.tsx │ │ │ ├── index.tsx │ │ │ ├── prism.css │ │ │ ├── style.css │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ ├── mega-form/ │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── public/ │ │ │ └── index.html │ │ ├── src/ │ │ │ ├── App.tsx │ │ │ ├── index.tsx │ │ │ ├── initialValue.ts │ │ │ ├── style.css │ │ │ ├── useAtomSlice.ts │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ ├── starter/ │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.css │ │ │ ├── index.tsx │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ ├── text_length/ │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── public/ │ │ │ └── index.html │ │ ├── src/ │ │ │ ├── App.tsx │ │ │ ├── index.tsx │ │ │ └── react-app-env.d.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ ├── todos/ │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── public/ │ │ │ └── index.html │ │ ├── src/ │ │ │ ├── App.tsx │ │ │ ├── index.tsx │ │ │ ├── styles.css │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ └── todos_with_atomFamily/ │ ├── README.md │ ├── index.html │ ├── package.json │ ├── public/ │ │ └── index.html │ ├── src/ │ │ ├── App.tsx │ │ ├── index.tsx │ │ ├── styles.css │ │ └── vite-env.d.ts │ ├── tsconfig.json │ └── vite.config.ts ├── package.json ├── pnpm-workspace.yaml ├── rollup.config.mjs ├── src/ │ ├── babel/ │ │ ├── plugin-debug-label.ts │ │ ├── plugin-react-refresh.ts │ │ ├── preset.ts │ │ └── utils.ts │ ├── index.ts │ ├── react/ │ │ ├── Provider.ts │ │ ├── useAtom.ts │ │ ├── useAtomValue.ts │ │ ├── useSetAtom.ts │ │ ├── utils/ │ │ │ ├── useAtomCallback.ts │ │ │ ├── useHydrateAtoms.ts │ │ │ ├── useReducerAtom.ts │ │ │ └── useResetAtom.ts │ │ └── utils.ts │ ├── react.ts │ ├── types.d.ts │ ├── utils.ts │ ├── vanilla/ │ │ ├── atom.ts │ │ ├── internals.ts │ │ ├── store.ts │ │ ├── typeUtils.ts │ │ ├── utils/ │ │ │ ├── atomFamily.ts │ │ │ ├── atomWithDefault.ts │ │ │ ├── atomWithLazy.ts │ │ │ ├── atomWithObservable.ts │ │ │ ├── atomWithReducer.ts │ │ │ ├── atomWithRefresh.ts │ │ │ ├── atomWithReset.ts │ │ │ ├── atomWithStorage.ts │ │ │ ├── constants.ts │ │ │ ├── freezeAtom.ts │ │ │ ├── loadable.ts │ │ │ ├── selectAtom.ts │ │ │ ├── splitAtom.ts │ │ │ └── unwrap.ts │ │ └── utils.ts │ └── vanilla.ts ├── tests/ │ ├── babel/ │ │ ├── plugin-debug-label.test.ts │ │ ├── plugin-react-refresh.test.ts │ │ └── preset.test.ts │ ├── react/ │ │ ├── abortable.test.tsx │ │ ├── async.test.tsx │ │ ├── async2.test.tsx │ │ ├── basic.test.tsx │ │ ├── dependency.test.tsx │ │ ├── error.test.tsx │ │ ├── items.test.tsx │ │ ├── onmount.test.tsx │ │ ├── optimization.test.tsx │ │ ├── provider.test.tsx │ │ ├── transition.test.tsx │ │ ├── types.test.tsx │ │ ├── useAtomValue.test.tsx │ │ ├── useSetAtom.test.tsx │ │ ├── utils/ │ │ │ ├── types.test.tsx │ │ │ ├── useAtomCallback.test.tsx │ │ │ ├── useHydrateAtoms.test.tsx │ │ │ ├── useReducerAtom.test.tsx │ │ │ └── useResetAtom.test.tsx │ │ └── vanilla-utils/ │ │ ├── atomFamily.test.tsx │ │ ├── atomWithDefault.test.tsx │ │ ├── atomWithObservable.test.tsx │ │ ├── atomWithReducer.test.tsx │ │ ├── atomWithRefresh.test.tsx │ │ ├── atomWithStorage.test.tsx │ │ ├── freezeAtom.test.tsx │ │ ├── loadable.test.tsx │ │ ├── selectAtom.test.tsx │ │ └── splitAtom.test.tsx │ ├── setup.ts │ ├── test-utils.ts │ └── vanilla/ │ ├── basic.test.tsx │ ├── dependency.test.tsx │ ├── derive.test.tsx │ ├── effect.test.ts │ ├── internals.test.tsx │ ├── memoryleaks.test.ts │ ├── store.test.tsx │ ├── storedev.test.tsx │ ├── types.test.tsx │ └── utils/ │ ├── atomFamily.test.ts │ ├── atomWithDefault.test.ts │ ├── atomWithLazy.test.ts │ ├── atomWithRefresh.test.ts │ ├── atomWithReset.test.ts │ ├── loadable.test.ts │ ├── types.test.tsx │ └── unwrap.test.ts ├── tsconfig.json ├── vitest.config.mts └── website/ ├── .babelrc ├── .gitignore ├── README.md ├── api/ │ └── contact.js ├── gatsby-browser.js ├── gatsby-config.js ├── gatsby-node.js ├── gatsby-shared.js ├── gatsby-ssr.js ├── jsconfig.json ├── package.json ├── postcss.config.js ├── reach-router.js ├── src/ │ ├── api/ │ │ └── contact.js │ ├── atoms/ │ │ └── index.js │ ├── components/ │ │ ├── button.js │ │ ├── client-only.js │ │ ├── code-sandbox.js │ │ ├── code.js │ │ ├── core-demo.js │ │ ├── credits.js │ │ ├── docs.js │ │ ├── extensions-demo.js │ │ ├── external-link.js │ │ ├── footer.js │ │ ├── headline.js │ │ ├── icon.js │ │ ├── inline-code.js │ │ ├── intro.js │ │ ├── jotai.js │ │ ├── layout.js │ │ ├── logo-cloud.js │ │ ├── logo.js │ │ ├── main.js │ │ ├── mdx.js │ │ ├── menu.js │ │ ├── meta.js │ │ ├── modal.js │ │ ├── search-button.js │ │ ├── search-modal.js │ │ ├── shelf.js │ │ ├── sidebar.js │ │ ├── stackblitz.js │ │ ├── support-modal.js │ │ ├── support.js │ │ ├── tabs.js │ │ ├── toc.js │ │ ├── toggle.js │ │ ├── utilities-demo.js │ │ └── wrapper.js │ ├── hooks/ │ │ └── index.js │ ├── pages/ │ │ ├── 404.js │ │ ├── docs/ │ │ │ └── {Mdx.slug}.js │ │ └── index.js │ ├── styles/ │ │ ├── base.css │ │ ├── components.css │ │ ├── fonts.css │ │ ├── index.css │ │ ├── layout.css │ │ ├── pmndrs.css │ │ └── utilities.css │ └── utils/ │ └── index.js ├── static/ │ └── robots.txt ├── tailwind.config.js └── vercel.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .codesandbox/ci.json ================================================ { "packages": ["dist"], "sandboxes": [ "new", "react-typescript-react-ts", "simple-react-browserify-x9yni", "simple-snowpack-react-o1gmx", "next-js-uo1h0", "next-js-with-custom-babel-config-komw9", "react-with-custom-babel-config-z1ebx" ], "node": "18" } ================================================ FILE: .github/DISCUSSION_TEMPLATE/bug-report.yml ================================================ labels: ['bug'] body: - type: markdown attributes: value: If you don't have a reproduction link, please choose a different category. - type: textarea attributes: label: Bug Description description: Describe the bug you encountered validations: required: true - type: input attributes: label: Reproduction Link description: A link to a [TypeScript Playground](https://www.typescriptlang.org/play), a [StackBlitz Project](https://stackblitz.com/) or something else with a minimal reproduction. validations: required: true ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: [dai-shi] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: jotai # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username custom: ['https://daishi.gumroad.com/l/learn-jotai'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Assigned issue about: This is to create a new issue that already has an assignee. Please open a new discussion otherwise. title: '' labels: '' assignees: '' --- ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Bug Reports url: https://github.com/pmndrs/jotai/discussions/new?category=bug-report about: Please post bug reports here. - name: Questions url: https://github.com/pmndrs/jotai/discussions/new?category=q-a about: Please post questions here. - name: Other Discussions url: https://github.com/pmndrs/jotai/discussions/new/choose about: Please post ideas and general discussions here. ================================================ FILE: .github/pull_request_template.md ================================================ ## Related Bug Reports or Discussions Fixes # ## Summary ## Check List - [ ] `pnpm run fix` for formatting and linting code and docs ================================================ FILE: .github/workflows/compressed-size.yml ================================================ name: Compressed Size on: [pull_request] jobs: compressed_size: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version: 'lts/*' cache: 'pnpm' - uses: preactjs/compressed-size-action@49c7ff02f46adc39a83c24e91f6110ba8138a19d # v3 with: pattern: './dist/**/*.{js,mjs}' ================================================ FILE: .github/workflows/ecosystem-ci.yml ================================================ name: Ecosystem CI on: issue_comment: types: [created] jobs: trigger: runs-on: ubuntu-latest if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body, '/ecosystem-ci run') }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: repository: 'jotaijs/jotai-ecosystem-ci' - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version: 22 cache: 'pnpm' - run: pnpm install - name: Get Short SHA id: short_sha run: | api="https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.issue.number }}" sha=$(curl -s -H "Authorization: token $GITHUB_TOKEN" $api | jq -r '.head.sha' | cut -c1-8) echo "x=$sha" >> $GITHUB_OUTPUT env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Run Ecosystem CI id: run_command run: | echo "x<> $GITHUB_OUTPUT pnpm run ecosystem-ci | tee >(grep -A999 -- '---- Jotai Ecosystem CI Results ----' >> $GITHUB_OUTPUT) echo "EOF" >> $GITHUB_OUTPUT env: JOTAI_PKG: https://pkg.csb.dev/pmndrs/jotai/commit/${{ steps.short_sha.outputs.x }}/jotai VERBOSE: 1 - uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0 with: issue-number: ${{ github.event.issue.number }} body: | ## Ecosystem CI Output ``` ${{ steps.run_command.outputs.x }} ``` ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} ================================================ FILE: .github/workflows/livecodes-post-comment.yml ================================================ name: LiveCodes Post Comment on: workflow_run: workflows: [LiveCodes Preview] types: - completed jobs: upload: runs-on: ubuntu-latest permissions: pull-requests: write if: > github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' steps: - uses: live-codes/pr-comment-from-artifact@15cb31a249e2336571f5d8cc244f6132b7d90ead # v1.0.1 with: GITHUB_TOKEN: ${{ github.token }} ================================================ FILE: .github/workflows/livecodes-preview.yml ================================================ name: LiveCodes Preview on: [pull_request] jobs: build_and_prepare: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version: 'lts/*' cache: 'pnpm' - uses: live-codes/preview-in-livecodes@636df52bd6a29316a7ad7501e7016ffdc6f83468 # v1.2.2 with: install-command: pnpm install build-command: pnpm run build base-url: 'https://{{LC::REF}}.preview-in-livecodes-demo.pages.dev' ================================================ FILE: .github/workflows/preview-release.yml ================================================ name: Preview Release on: [push, pull_request] jobs: preview_release: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version: 'lts/*' cache: 'pnpm' - run: pnpm install - run: pnpm run build - run: pnpm dlx pkg-pr-new publish './dist' --compact --template './examples/*' ================================================ FILE: .github/workflows/publish.yml ================================================ name: Publish on: release: types: [published] permissions: id-token: write contents: read jobs: publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version: 'lts/*' registry-url: 'https://registry.npmjs.org' cache: 'pnpm' - run: pnpm install - run: pnpm run build - run: npm publish working-directory: dist ================================================ FILE: .github/workflows/stale-discussions.yml ================================================ name: Close Stale Discussions on: schedule: - cron: '0 0 * * *' # Runs every day at midnight UTC workflow_dispatch: jobs: close-stale-discussions: runs-on: ubuntu-latest permissions: discussions: write steps: - uses: steffen-karlsson/stalesweeper@cbed739a89b490f703d8466fb59b37ddc0915889 # v1.1.1 with: message: 'This discussion has been closed due to inactivity.' days-before-close: 180 close-unanswered: true verbose: true ================================================ FILE: .github/workflows/test-multiple-builds.yml ================================================ name: Test Multiple Builds on: push: branches: [main] pull_request: types: [opened, synchronize] jobs: test_multiple_builds: runs-on: ubuntu-latest strategy: fail-fast: false matrix: build: [cjs, esm, umd] env: [development, production] steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version: 'lts/*' cache: 'pnpm' - run: pnpm install - run: pnpm run build - name: Use React 17 for production test if: ${{ matrix.env == 'production' }} run: | pnpm add -D react@17.0.2 react-dom@17.0.2 @testing-library/react@12.1.4 - name: Patch for DEV-ONLY if: ${{ matrix.env == 'development' }} run: | sed -i~ "s/\(it\|describe\)[.a-zA-Z]*('\[DEV-ONLY\]/\1('/" tests/*/*.ts* tests/*/*/*.ts* sed -i~ "s/\(it\|describe\)[.a-zA-Z]*('\[PRD-ONLY\]/\1.skip('/" tests/*/*.ts* tests/*/*/*.ts* - name: Patch for PRD-ONLY if: ${{ matrix.env == 'production' }} run: | sed -i~ "s/\(it\|describe\)[.a-zA-Z]*('\PRD-ONLY\]/\1('/" tests/*/*.ts* tests/*/*/*.ts* sed -i~ "s/\(it\|describe\)[.a-zA-Z]*('\[DEV-ONLY\]/\1.skip('/" tests/*/*.ts* tests/*/*/*.ts* - name: Patch for CJS if: ${{ matrix.build == 'cjs' }} run: | sed -i~ "s/resolve('\.\/src\(.*\)\.ts')/resolve('\.\/dist\1.js')/" vitest.config.mts sed -i~ "s/import { useResetAtom } from 'jotai\/react\/utils'/const { useResetAtom } = require('..\/..\/..\/dist\/react\/utils.js')/" tests/react/utils/useResetAtom.test.tsx sed -i~ "s/import { RESET, atomWithReducer, atomWithReset } from 'jotai\/vanilla\/utils'/const { RESET, atomWithReducer, atomWithReset } = require('..\/..\/..\/dist\/vanilla\/utils.js')/" tests/react/utils/useResetAtom.test.tsx perl -i~ -0777 -pe "s/import {[^}]+} from 'jotai\/vanilla\/internals'/const { INTERNAL_buildStoreRev2: INTERNAL_buildStore, INTERNAL_initializeStoreHooksRev2: INTERNAL_initializeStoreHooks, INTERNAL_getBuildingBlocksRev2: INTERNAL_getBuildingBlocks } = require('..\/..\/dist\/vanilla\/internals.js')/g" tests/vanilla/store.test.tsx tests/vanilla/internals.test.tsx tests/vanilla/derive.test.tsx tests/vanilla/effect.test.ts - name: Patch for ESM if: ${{ matrix.build == 'esm' }} run: | sed -i~ "s/resolve('\.\/src\(.*\)\.ts')/resolve('\.\/dist\/esm\1.mjs')/" vitest.config.mts sed -i~ "1s/^/import.meta.env.MODE='${NODE_ENV}';/" tests/*/*.tsx tests/*/*/*.tsx env: NODE_ENV: ${{ matrix.env }} - name: Patch for UMD if: ${{ matrix.build == 'umd' }} run: | sed -i~ "s/resolve('\.\/src\(.*\)\.ts')/resolve('\.\/dist\/umd\1.${NODE_ENV}.js')/" vitest.config.mts rm tests/react/utils/useResetAtom.test.tsx # FIXME we skip this for now. Actually I'm not sure if we really run tests with UMD build rm tests/vanilla/store.test.tsx tests/vanilla/internals.test.tsx tests/vanilla/derive.test.tsx tests/vanilla/effect.test.ts # FIXME we skip this for now. Actually I'm not sure if we really run tests with UMD build env: NODE_ENV: ${{ matrix.env }} - name: Patch for SystemJS if: ${{ matrix.build == 'system' }} run: | sed -i~ "s/resolve('\.\/src\(.*\)\.ts')/resolve('\.\/dist\/system\1.${NODE_ENV}.js')/" vitest.config.mts env: NODE_ENV: ${{ matrix.env }} - name: Test ${{ matrix.build }} ${{ matrix.env }} run: | pnpm run test:spec env: NODE_ENV: ${{ matrix.env }} ================================================ FILE: .github/workflows/test-multiple-versions.yml ================================================ name: Test Multiple Versions on: push: branches: [main] pull_request: types: [opened, synchronize] jobs: test_multiple_versions: runs-on: ubuntu-latest strategy: fail-fast: false matrix: react: - 16.14.0 - 17.0.0 - 18.0.0 - 18.1.0 - 18.2.0 - 18.3.1 - 19.0.0 - 19.1.0 - 19.2.0 - 19.3.0-canary-46103596-20260305 - 0.0.0-experimental-46103596-20260305 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version: 'lts/*' cache: 'pnpm' - run: pnpm install - name: Install legacy testing-library if: ${{ startsWith(matrix.react, '16.') || startsWith(matrix.react, '17.') }} run: | pnpm add -D @testing-library/react@12.1.4 - name: Patch for React 17 if: ${{ startsWith(matrix.react, '17.') }} run: | pnpm add -D vitest@3.2.4 - name: Patch for React 16 if: ${{ startsWith(matrix.react, '16.') }} run: | sed -i~ '1s/^/import React from "react";/' tests/*/*.tsx tests/*/*/*.tsx sed -i~ 's/"jsx": "react-jsx"/"jsx": "react"/' tsconfig.json sed -i~ 's/import\.meta\.env[?]\.MODE/"DEVELOPMENT".toLowerCase()/' src/*.ts src/*/*.ts src/*/*/*.ts - name: Test Build # we need to build for babel tests run: pnpm run build - name: Test ${{ matrix.react }} run: | pnpm add -D react@${{ matrix.react }} react-dom@${{ matrix.react }} pnpm run test:spec ================================================ FILE: .github/workflows/test-old-typescript.yml ================================================ name: Test Old TypeScript on: push: branches: [main] pull_request: types: [opened, synchronize] jobs: test_old_typescript: runs-on: ubuntu-latest strategy: fail-fast: false matrix: typescript: - 5.9.3 - 5.8.3 - 5.7.3 - 5.6.3 - 5.5.4 - 5.4.5 - 5.3.3 - 5.2.2 - 5.1.6 - 5.0.4 - 4.9.5 - 4.8.4 - 4.7.4 - 4.6.4 - 4.5.5 - 4.4.4 - 4.3.5 - 4.2.3 - 4.1.5 - 4.0.5 - 3.9.7 - 3.8.3 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version: 'lts/*' cache: 'pnpm' - run: pnpm install - run: pnpm run build - name: Patch for all TS run: | sed -i~ 's/"isolatedDeclarations": true,//' tsconfig.json - name: Patch for v4/v3 TS if: ${{ startsWith(matrix.typescript, '4.') || startsWith(matrix.typescript, '3.') }} run: | sed -i~ 's/"verbatimModuleSyntax": true,//' tsconfig.json - name: Patch for Newer TS if: ${{ matrix.typescript == '4.9.5' || matrix.typescript == '4.8.4' }} run: | sed -i~ 's/"moduleResolution": "bundler",/"moduleResolution": "node",/' tsconfig.json sed -i~ 's/"allowImportingTsExtensions": true,//' tsconfig.json sed -i~ 's/"jotai": \["\.\/src\/index\.ts"\],/"jotai": [".\/dist\/index.d.ts"],/' tsconfig.json sed -i~ 's/"jotai\/\*": \["\.\/src\/\*\.ts"\]/"jotai\/*": [".\/dist\/*.d.ts"]/' tsconfig.json sed -i~ 's/"include": .*/"include": ["src\/types.d.ts", "dist\/**\/*", "tests\/**\/*"],/' tsconfig.json - name: Patch for specific TS version run: | ts_ver_esc=${{ matrix.typescript }} ts_ver_esc=${ts_ver_esc//./\\.} sed -i~ "s/\/\/ @ts-expect-error .*\[SKIP-TS-${ts_ver_esc}\].*//" tests/*/*.tsx tests/*/*/*.tsx sed -i~ "s/\/\/ .*\[ONLY-TS-${ts_ver_esc}\].* @ts-ignore/\/\/ @ts-ignore/" tests/*/*.tsx tests/*/*/*.tsx - name: Patch for Old TS if: ${{ matrix.typescript == '4.7.4' || matrix.typescript == '4.6.4' || matrix.typescript == '4.5.5' || matrix.typescript == '4.4.4' || matrix.typescript == '4.3.5' || matrix.typescript == '4.2.3' || matrix.typescript == '4.1.5' || matrix.typescript == '4.0.5' || startsWith(matrix.typescript, '3.') }} run: | sed -i~ 's/"target":/"skipLibCheck":true,"target":/' tsconfig.json sed -i~ 's/"exactOptionalPropertyTypes": true,//' tsconfig.json sed -i~ 's/"moduleResolution": "bundler",/"moduleResolution": "node",/' tsconfig.json sed -i~ 's/"allowImportingTsExtensions": true,//' tsconfig.json sed -i~ 's/"jotai": \["\.\/src\/index\.ts"\],/"jotai": [".\/dist\/ts3.8\/index.d.ts"],/' tsconfig.json sed -i~ 's/"jotai\/\*": \["\.\/src\/\*\.ts"\]/"jotai\/*": [".\/dist\/ts3.8\/*.d.ts"]/' tsconfig.json sed -i~ 's/"include": .*/"include": ["src\/types.d.ts", "dist\/**\/*", "tests\/**\/*"],/' tsconfig.json pnpm add -D @testing-library/user-event@14.4.3 @types/node@22.2.0 @types/babel__traverse@7.18.2 - name: Patch for Older TS if: ${{ matrix.typescript == '4.2.3' || matrix.typescript == '4.1.5' || matrix.typescript == '4.0.5' || startsWith(matrix.typescript, '3.') }} run: | sed -i~ 's/import\.meta\.env/(import.meta.env as any)/' tests/*/*.tsx tests/*/*/*.tsx sed -i~ '1s/^/\/\/\/ \nimport React from "react";/' tests/*/*.tsx tests/*/*/*.tsx sed -i~ 's/"jsx": "react-jsx",/"jsx": "react",/' tsconfig.json sed -i~ 's/"noUncheckedIndexedAccess": true,//' tsconfig.json sed -i~ 's/^import type /import /' tests/*/*.tsx tests/*/*/*.tsx pnpm json -I -f package.json -e "this.resolutions={}; this.resolutions['@types/prettier']='2.4.2'; this.resolutions['@types/node']='18.11.18'; this.resolutions['@types/react']='18.2.56';" pnpm add -D @types/prettier@2.4.2 @types/node@18.11.18 @types/yargs@17.0.13 @types/react@18.2.56 rm -r tests/react/vanilla-utils/atomWithObservable.* - name: Install old TypeScript run: pnpm add -D typescript@${{ matrix.typescript }} - name: Patch testing setup for Old TS if: ${{ matrix.typescript == '4.6.4' || matrix.typescript == '4.5.5' || matrix.typescript == '4.4.4' || matrix.typescript == '4.3.5' || matrix.typescript == '4.2.3' || matrix.typescript == '4.1.5' || matrix.typescript == '4.0.5' || startsWith(matrix.typescript, '3.') }} run: | pnpm add -D vitest@0.33.0 @vitest/coverage-v8@0.33.0 @vitest/ui@0.33.0 pnpm add -D @testing-library/jest-dom@5 @types/testing-library__jest-dom@5 pnpm add -D @types/jest@27.4.1 sed -i~ 's/"@testing-library\/jest-dom"/"@types\/testing-library__jest-dom"/' tsconfig.json - name: Patch testing setup for older TS if: ${{ matrix.typescript == '4.0.5' || startsWith(matrix.typescript, '3.') }} run: | pnpm add -D @testing-library/user-event@12.1.7 @testing-library/react@11.0.4 @types/react-dom@18.3.1 rm node_modules/vitest/dist/*.d.ts echo "declare module 'vitest'" >> ./src/types.d.ts - name: Test ${{ matrix.typescript }} run: | rm -r node_modules/@types/babel__core/node_modules sed -i~ 's/">=4.2": {/">=4.1": {/' node_modules/rxjs/package.json pnpm run test:types ================================================ FILE: .github/workflows/test.yml ================================================ name: Test on: push: branches: [main] pull_request: types: [opened, synchronize] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version: 'lts/*' cache: 'pnpm' - run: pnpm install - run: pnpm run test:format - run: pnpm run test:types - run: pnpm run test:lint - run: pnpm run test:spec - run: pnpm run build # we don't have any other workflows to test build ================================================ FILE: .gitignore ================================================ # dependencies node_modules .pnp .pnp.js # testing coverage # development .devcontainer .vscode # production dist build # dotenv environment variables file .env .env.local .env.development.local .env.test.local .env.production.local # logs npm-debug.log* yarn-debug.log* yarn-error.log* # misc .DS_Store .idea # examples examples/**/*/package-lock.json examples/**/*/yarn.lock examples/**/*/pnpm-lock.yaml examples/**/*/bun.lockb ================================================ FILE: .livecodes/react.json ================================================ { "title": "React demo", "activeEditor": "script", "markup": { "language": "html", "content": "
" }, "style": { "language": "css", "content": ".App {\n font-family: sans-serif;\n text-align: center;\n}\n" }, "script": { "language": "jsx", "content": "import { StrictMode, Suspense } from 'react';\nimport { createRoot } from 'react-dom/client';\nimport { atom, useAtom } from 'jotai';\n\nconst countAtom = atom(0);\n\nconst Counter = () => {\n const [count, setCount] = useAtom(countAtom);\n const inc = () => setCount((c) => c + 1);\n return (\n <>\n {count} \n \n );\n};\n\nconst App = () => (\n \n
\n

Hello Jotai

\n

Enjoy coding!

\n \n
\n
\n);\n\nconst rootElement = document.getElementById('root');\nconst root = createRoot(rootElement);\n\nroot.render(\n \n \n \n);\n" }, "customSettings": { "jotai commit sha": "{{LC::SHORT_SHA}}", "imports": { "jotai": "{{LC::TO_DATA_URL(./dist/esm/index.mjs)}}", "jotai/vanilla": "{{LC::TO_DATA_URL(./dist/esm/vanilla.mjs)}}", "jotai/utils": "{{LC::TO_DATA_URL(./dist/esm/utils.mjs)}}", "jotai/react": "{{LC::TO_DATA_URL(./dist/esm/react.mjs)}}", "jotai/vanilla/utils": "{{LC::TO_DATA_URL(./dist/esm/vanilla/utils.mjs)}}", "jotai/react/utils": "{{LC::TO_DATA_URL(./dist/esm/react/utils.mjs)}}" } } } ================================================ FILE: .prettierignore ================================================ dist pnpm-lock.yaml ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing ## General Guideline ### Reporting Issues If you have found what you think is a bug, please [start a discussion](https://github.com/pmndrs/jotai/discussions/new?category=bug-report). For any usage questions, please [start a discussion](https://github.com/pmndrs/jotai/discussions/new?category=q-a). ### Suggesting New Features If you are here to suggest a feature, first [start a discussion](https://github.com/pmndrs/jotai/discussions/new?category=ideas) if it does not already exist. From there, we will discuss use-cases for the feature and then finally discuss how it could be implemented. ### Committing We are applying [conventional commit spec](https://www.conventionalcommits.org/en/v1.0.0/) here. In short, that means a commit has to be one of the following types: Your commit type must be one of the following: - **feat**: A new feature. - **fix**: A bug fix. - **refactor**: A code change that neither fixes a bug nor adds a feature. - **chore**: Changes to the build process, configuration, dependencies, CI/CD pipelines, or other auxiliary tools and libraries. - **docs**: Documentation-only changes. - **test**: Adding missing or correcting existing tests. If you are unfamiliar with the usage of conventional commits, the short version is to simply specify the type as a first word, and follow it with a colon and a space, then start your message from a lowercase letter, like this: ``` feat: add a 'foo' type support ``` You can also specify the scope of the commit in the parentheses after a type: ``` fix(react): change the 'bar' parameter type ``` ### Development If you would like to contribute by fixing an open issue or developing a new feature you can use this suggested workflow: #### General 1. Fork this repository. 2. Create a new feature branch based off the `main` branch. 3. Follow the [Core](#Core) and/or the [Documentation](#Documentation) guide below and come back to this once done. 4. Run `pnpm run fix:format` to format the code. 5. Git stage your required changes and commit. (review the commit guidelines below) 6. Submit the PR for review. ##### Core 1. Run `pnpm install` to install dependencies. 2. Create failing tests for your fix or new feature in the [`tests`](./tests/) folder. 3. Implement your changes. 4. Run `pnpm run build` to build the library. _(Pro-tip: `pnpm run build-watch` runs the build in watch mode)_ 5. Run the tests by running `pnpm run test` and ensure that they pass. 6. You can use `pnpm link` to sym-link this package and test it locally on your own project. Alternatively, you may use CodeSandbox CI's canary releases to test the changes in your own project. (requires a PR to be created first) 7. Follow step 4 and onwards from the [General](#General) guide above to bring it to the finish line. ### Pull Requests Please try to keep your pull request focused in scope and avoid including unrelated commits. After you have submitted your pull request, we'll try to get back to you as soon as possible. We may suggest some changes or request improvements, therefore, please check ✅ ["Allow edits from maintainers"](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork) on your PR. ## Jotai-specific Guideline ##### Documentation 1. Navigate to the [`website`](./website/) folder. (e.g., `cd website`) 2. Run `pnpm install` to install dependencies in the `website` folder. 3. Run `pnpm run dev` to start the dev server. 4. Navigate to [`http://localhost:9000`](http://localhost:9000) to view the documents. 5. Navigate to the [`docs`](./docs/) folder and make necessary changes to the documents. 6. Add your changes to the documents and see them live reloaded in the browser. 7. Follow step 4 and onwards from the [General](#General) guide above to bring it to the finish line. Thank you for contributing! :heart: ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 Poimandres Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================
![Jotai (light mode)](./img/jotai-header-light.png#gh-light-mode-only) ![Jotai (dark mode)](./img/jotai-header-dark.png#gh-dark-mode-only)
visit [jotai.org](https://jotai.org) or `npm i jotai` [![Build Status](https://img.shields.io/github/actions/workflow/status/pmndrs/jotai/test.yml?branch=main&style=flat&colorA=000000&colorB=000000)](https://github.com/pmndrs/jotai/actions/workflows/test.yml?query=branch%3Amain) [![Build Size](https://img.shields.io/bundlephobia/minzip/jotai?label=bundle%20size&style=flat&colorA=000000&colorB=000000)](https://bundlephobia.com/result?p=jotai) [![Version](https://img.shields.io/npm/v/jotai?style=flat&colorA=000000&colorB=000000)](https://www.npmjs.com/package/jotai) [![Downloads](https://img.shields.io/npm/dt/jotai.svg?style=flat&colorA=000000&colorB=000000)](https://www.npmjs.com/package/jotai) [![Discord Shield](https://img.shields.io/discord/740090768164651008?style=flat&colorA=000000&colorB=000000&label=discord&logo=discord&logoColor=ffffff)](https://discord.gg/poimandres) [![Open Collective](https://img.shields.io/opencollective/all/jotai?style=flat&colorA=000000&colorB=000000)](https://opencollective.com/jotai) Jotai scales from a simple useState replacement to an enterprise TypeScript application. - Minimal core API (2kb) - Many utilities and extensions - No string keys (compared to Recoil) Examples: [Demo 1](https://codesandbox.io/s/jotai-demo-47wvh) | [Demo 2](https://codesandbox.io/s/jotai-demo-forked-x2g5d) ### First, create a primitive atom An atom represents a piece of state. All you need is to specify an initial value, which can be primitive values like strings and numbers, objects, and arrays. You can create as many primitive atoms as you want. ```jsx import { atom } from 'jotai' const countAtom = atom(0) const countryAtom = atom('Japan') const citiesAtom = atom(['Tokyo', 'Kyoto', 'Osaka']) const mangaAtom = atom({ 'Dragon Ball': 1984, 'One Piece': 1997, Naruto: 1999 }) ``` ### Use the atom in your components It can be used like `React.useState`: ```jsx import { useAtom } from 'jotai' function Counter() { const [count, setCount] = useAtom(countAtom) return (

{count} ... ``` ### Create derived atoms with computed values A new read-only atom can be created from existing atoms by passing a read function as the first argument. `get` allows you to fetch the contextual value of any atom. ```jsx const doubledCountAtom = atom((get) => get(countAtom) * 2) function DoubleCounter() { const [doubledCount] = useAtom(doubledCountAtom) return

{doubledCount}

} ``` ### Creating an atom from multiple atoms You can combine multiple atoms to create a derived atom. ```jsx const count1 = atom(1) const count2 = atom(2) const count3 = atom(3) const sum = atom((get) => get(count1) + get(count2) + get(count3)) ``` Or if you like fp patterns ... ```jsx const atoms = [count1, count2, count3, ...otherAtoms] const sum = atom((get) => atoms.map(get).reduce((acc, count) => acc + count)) ``` ### Derived async atoms [needs suspense](https://react.dev/reference/react/Suspense) You can make the read function an async function too. ```jsx const urlAtom = atom('https://json.host.com') const fetchUrlAtom = atom(async (get) => { const response = await fetch(get(urlAtom)) return await response.json() }) function Status() { // Re-renders the component after urlAtom is changed and the async function above concludes const [json] = useAtom(fetchUrlAtom) ... ``` ### You can create a writable derived atom Specify a write function at the second argument. `get` will return the current value of an atom. `set` will update the value of an atom. ```jsx const decrementCountAtom = atom( (get) => get(countAtom), (get, set, _arg) => set(countAtom, get(countAtom) - 1) ) function Counter() { const [count, decrement] = useAtom(decrementCountAtom) return (

{count} ... ``` ### Write only derived atoms Just do not define a read function. ```jsx const multiplyCountAtom = atom(null, (get, set, by) => set(countAtom, get(countAtom) * by), ) function Controls() { const [, multiply] = useAtom(multiplyCountAtom) return } ``` ### Async actions Just make the write function an async function and call `set` when you're ready. ```jsx const fetchCountAtom = atom( (get) => get(countAtom), async (_get, set, url) => { const response = await fetch(url) set(countAtom, (await response.json()).count) } ) function Controls() { const [count, compute] = useAtom(fetchCountAtom) return ( ... ``` ### Note about functional programming Jotai's fluid interface is no accident — atoms are monads, just like promises! Monads are an [established]() pattern for modular, pure, robust and understandable code which is [optimized for change](https://overreacted.io/optimized-for-change/). Read more about [Jotai and monads.](https://jotai.org/docs/basics/functional-programming-and-jotai) ## Links - [website](https://jotai.org) - [documentation](https://jotai.org/docs) - [course](https://egghead.io/courses/manage-application-state-with-jotai-atoms-2c3a29f0) ================================================ FILE: babel.config.mjs ================================================ export default (api, targets) => { // https://babeljs.io/docs/en/config-files#config-function-api const isTestEnv = api.env('test') return { babelrc: false, ignore: ['./node_modules'], presets: [ [ '@babel/preset-env', { loose: true, modules: isTestEnv ? 'commonjs' : false, targets: isTestEnv ? { node: 'current' } : targets, }, ], ], plugins: [ [ '@babel/plugin-transform-react-jsx', { runtime: 'automatic', }, ], ['@babel/plugin-transform-typescript', { isTSX: true }], ], } } ================================================ FILE: benchmarks/.gitignore ================================================ /*.json /*.html ================================================ FILE: benchmarks/simple-read.ts ================================================ #!/usr/bin/env npx tsx /// import path from 'node:path' import { fileURLToPath } from 'node:url' import { add, complete, cycle, save, suite } from 'benny' import { atom } from '../src/vanilla/atom.ts' import type { PrimitiveAtom } from '../src/vanilla/atom.ts' import { createStore } from '../src/vanilla/store.ts' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const createStateWithAtoms = (n: number) => { let targetAtom: PrimitiveAtom | undefined const store = createStore() for (let i = 0; i < n; ++i) { const a = atom(i) if (!targetAtom) { targetAtom = a } store.set(a, i) } if (!targetAtom) { throw new Error() } return [store, targetAtom] as const } const main = async () => { await suite( 'simple-read', ...[2, 3, 4, 5, 6].map((n) => add(`atoms=${10 ** n}`, () => { const [store, targetAtom] = createStateWithAtoms(10 ** n) return () => store.get(targetAtom) }), ), cycle(), complete(), save({ folder: __dirname, file: 'simple-read', format: 'json', }), save({ folder: __dirname, file: 'simple-read', format: 'chart.html', }), ) } main() ================================================ FILE: benchmarks/simple-write.ts ================================================ #!/usr/bin/env npx tsx /// import path from 'node:path' import { fileURLToPath } from 'node:url' import { add, complete, cycle, save, suite } from 'benny' import { atom } from '../src/vanilla/atom.ts' import type { PrimitiveAtom } from '../src/vanilla/atom.ts' import { createStore } from '../src/vanilla/store.ts' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const createStateWithAtoms = (n: number) => { let targetAtom: PrimitiveAtom | undefined const store = createStore() for (let i = 0; i < n; ++i) { const a = atom(i) if (!targetAtom) { targetAtom = a } store.set(a, i) } if (!targetAtom) { throw new Error() } return [store, targetAtom] as const } const main = async () => { await suite( 'simple-write', ...[2, 3, 4, 5, 6].map((n) => add(`atoms=${10 ** n}`, () => { const [store, targetAtom] = createStateWithAtoms(10 ** n) return () => store.set(targetAtom, (c) => c + 1) }), ), cycle(), complete(), save({ folder: __dirname, file: 'simple-write', format: 'json', }), save({ folder: __dirname, file: 'simple-write', format: 'chart.html', }), ) } main() ================================================ FILE: benchmarks/subscribe-write.ts ================================================ #!/usr/bin/env npx tsx /// import path from 'node:path' import { fileURLToPath } from 'node:url' import { add, complete, cycle, save, suite } from 'benny' import { atom } from '../src/vanilla/atom.ts' import type { PrimitiveAtom } from '../src/vanilla/atom.ts' import { createStore } from '../src/vanilla/store.ts' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const cleanupFns = new Set<() => void>() const cleanup = () => { cleanupFns.forEach((fn) => fn()) cleanupFns.clear() } const createStateWithAtoms = (n: number) => { let targetAtom: PrimitiveAtom | undefined const store = createStore() for (let i = 0; i < n; ++i) { const a = atom(i) if (!targetAtom) { targetAtom = a } store.get(a) const unsub = store.sub(a, () => { store.get(a) }) cleanupFns.add(unsub) } if (!targetAtom) { throw new Error() } return [store, targetAtom] as const } const main = async () => { await suite( 'subscribe-write', ...[2, 3, 4, 5, 6].map((n) => add(`atoms=${10 ** n}`, () => { cleanup() const [store, targetAtom] = createStateWithAtoms(10 ** n) return () => store.set(targetAtom, (c) => c + 1) }), ), cycle(), complete(), save({ folder: __dirname, file: 'subscribe-write', format: 'json', }), save({ folder: __dirname, file: 'subscribe-write', format: 'chart.html', }), ) } main() ================================================ FILE: docs/basics/comparison.mdx ================================================ --- title: Comparison nav: 7.02 --- Jotai was born to solve extra re-render issues in React. An extra re-render is when the render process produces the same UI result, where users won't see any differences. To tackle this issue with React context (useContext + useState), one would require many contexts and face some issues. - Provider hell: It's likely that your root component has many context providers, which is technically okay, and sometimes desirable to provide context in different subtree. - Dynamic addition/deletion: Adding a new context at runtime is not very nice, because you need to add a new provider and its children will be re-mounted. Traditionally, a top-down solution to this is to use a selector function. The [use-context-selector](https://github.com/dai-shi/use-context-selector) library is one example. The issue with this approach is the selector function needs to return referentially equal values to prevent re-renders, and this often requires a memoization technique. Jotai takes a bottom-up approach with the atomic model, inspired by [Recoil](https://recoiljs.org/). One can build state combining atoms, and optimize renders based on atom dependency. This avoids the need for memoization. Jotai has two principles. - Primitive: Its basic API is simple, like `useState`. - Flexible: Atoms can derive another atom and form a graph. Atoms can also be updated by another arbitrary atom. It allows abstracting complex state models. ### How is Jotai different from useContext of React? Jotai's core API is minimalistic and makes it easy to build utilities based upon it. #### Analogy You can view Jotai as a drop-in replacement to `useContext`. Except Jotai is aiming for simplicity, minimalistic API and can do much more than `useContext` & `useState`. #### Usage difference We can see how we used to share data to children, compared to how we do it with Jotai. But let's use a real-world example where we have multiple `Context` in our app. ```jsx // 1. useState local state const Component = () => { const [state, setState] = useState(0) } // 2. Lift local state up and share it via context const StateContext = createContext() const Parent = ({ children }) => { return ( {children} ) } const Component = () => { const [state, setState] = useContext(StateContext) } // 3. Have multiple states and contexts const State1Context = createContext() const State2Context = createContext() const Parent = ({ children }) => ( {children} ) const Component1 = () => { const [state, setState] = useContext(State1Context) } const Component2 = () => { const [state, setState] = useContext(State2Context) } ``` Now let's see how Jotai simplify it for us. You can just use atoms instead of multiple `Context`. ```jsx import { Provider, atom, useAtom } from 'jotai' const atom1 = atom(0) const atom2 = atom(0) // Optional: you can use Provider's just like useContext, // ...but if you only need one, // ...you can just omit it and Jotai will use a default one (called Provider-less mode). const Parent = ({ children }) => { return {children} } const Component1 = () => { const [state, setState] = useAtom(atom1) } const Component2 = () => { const [state, setState] = useAtom(atom2) } ``` ### How is Jotai different from Zustand? #### Name Jotai means "state" in Japanese. Zustand means "state" in German. #### Analogy Jotai is like Recoil. Zustand is like Redux. #### Where state resides To hold states, Both have stores that can exist either at module level or at context level. Jotai is designed to be context first, and module second. Zustand is designed to be module first, and context second. #### How to structure state Jotai state consists of atoms (i.e. bottom-up). Zustand state is one object (i.e. top-down). #### Technical difference The major difference is the state model. Zustand is a single store (although you could create multiple separate stores), while Jotai consists of primitive atoms and allows composing them together. In this sense, it's the matter of programming mental model. #### When to use which - If you need a replacement for useState+useContext, Jotai fits well. - If you want a simple module state, Zustand fits well. - If code splitting is important, Jotai should perform well. - If you prefer Redux devtools, Zustand is good to go. - If you want to make use of Suspense, Jotai is the one. ### How is Jotai different from Recoil? (Disclaimer: the author is not very familiar with Recoil, this may be biased and inaccurate.) #### Developer - Jotai is developed with collective work by a few developers in Poimandres (formerly react-spring) org. - Recoil is developed by a team at Facebook. #### Basis - Jotai is focusing on primitive APIs for easy learning, and it's unopinionated. (The same philosophy with Zustand) - Recoil is all-in-one, and it has various cache strategies. #### Technical difference - Jotai depends on atom object referential identities. - Recoil depends on atom string keys. #### When to use which - If you want to learn something new, either should work. - If you like Zustand, Jotai would also be pleasant. - If you need React Context alternatives, Jotai comes with enough features. - If you need to read and write atoms outside React, Jotai provides store API. - If you would try to create a new library, Jotai might give good primitives. - Otherwise, both are pretty similar about the general goals and basic techniques, so please try both and share your feedback with us. ================================================ FILE: docs/basics/concepts.mdx ================================================ --- title: Concepts nav: 7.01 --- Jotai is a library that will make you return to the basics of React development & keep everything simple. ### From scratch Before trying to compare Jotai with what we may have known previously, let's just dive straight into something very simple. The React world is very much like our world, it's a big set of small entities, we call them components, and we know that they have their own state. Structuring your components to interact altogether will create your app. Now, the Jotai world also has its small entities, atoms, and they also have their state. Composing atoms will create your app state! Jotai considers anything to be an atom, so you may say: `Huh, I need objects and arrays, filter them and then sort them out`. And here's the beauty of it, Jotai gracefully lets you create dumb atoms derived from even more dumb atoms. If, for example, I have a page with 2 tabs: online friends and offline friends. I will have 2 atoms simply derived from a common, dumber source. ```js const dumbAtom = atom([{ name: 'Friend 1', online: false }]) const onlineAtom = atom((get) => get(dumbAtom).filter((item) => item.online)) const offlineAtom = atom((get) => get(dumbAtom).filter((item) => !item.online)) ``` And you could keep going on complexity forever. Another incredible feature of Jotai is the built-in ability to suspend when using asynchronous atoms. This is a relatively new feature that needs more experimentation, but is definitely the future of how we will build React apps. [Check out the docs](https://react.dev/blog/2022/03/29/react-v18#new-suspense-features) for more info. ================================================ FILE: docs/basics/functional-programming-and-jotai.mdx ================================================ --- title: Functional programming and Jotai nav: 7.04 --- ### Unexpected similarities If you look at getter functions long enough, you may see a striking resemblance to a certain JavaScript language feature. ```tsx const nameAtom = atom('Visitor') const countAtom = atom(1) const greetingAtom = atom((get) => { const name = get(nameAtom) const count = get(countAtom) return (
Hello, {name}! You have visited this page {count} times.
) }) ``` Compare that code with `async`–`await`: ```tsx const namePromise = Promise.resolve('Visitor') const countPromise = Promise.resolve(1) const greetingPromise = (async function () { const name = await namePromise const count = await countPromise return (
Hello, {name}! You have visited this page {count} times.
) })() ``` This similarity is no coincidence. Both atoms and promises are **Monads**†, a concept from functional programming. The syntax used in both `greetingAtom` and `greetingPromise` is known as **do-notation**, a syntax sugar for the plainer monad interface. ### About monads The monad interface is responsible for the fluidity of the atom and promise interfaces. The monad interface allowed us to define `greetingAtom` in terms of `nameAtom` and `countAtom`, and allowed us to define `greetingPromise` in terms of `namePromise` and `countPromise`. If you're curious, a structure (like `Atom` or `Promise`) is a monad if you can implement the following functions for it. A fun exercise is trying to implement `of`, `map` and `join` for Arrays. ```ts type SomeMonad = /* for example... */ Array declare function of(plainValue: T): SomeMonad declare function map( anInstance: SomeMonad, transformContents: (contents: T) => V, ): SomeMonad declare function join(nestedInstances: SomeMonad>): SomeMonad ``` The shared heritage of Promises and Atoms means many patterns and best-practices can be reused between them. Let's take a look at one. ### Sequencing When talking about callback hell, we often mention the boilerplate, the indentation and the easy-to-miss mistakes. However, plumbing a single async operation into another single async operation was not the end of the callback struggle. What if we made four network calls and needed to wait for them all? A snippet like this was common: ```ts const nPending = 4 const results: string[] function callback(err, data) { if (err) throw err results.push(data) if (results.length === nPending) { // do something with results... } } ``` But what if the results have different types? and the order was important? Well, we'd have a lot more frustrating work to do! This logic would be duplicated at each usage, and would be easy to mess up. Since ES6, we simply call `Promise.all`: ```ts declare function promiseAll(promises: Array>): Promise> ``` `Promise.all` "rearranges" `Array` and `Promise`. It turns out this concept, _sequencing_, can be implemented for all monad–_Traversable_ pairs. Many kinds of collections are Traversables, including Arrays. For example, this is a case of sequencing specialized for atoms and arrays: ```ts function sequenceAtomArray(atoms: Array>): Atom> { return atom((get) => atoms.map(get)) } ``` ### Culmination Monads have been an interest to mathematicians for 60 years, and to programmers for 40. There are many resources out there on patterns for monads. Take a look at them! Here are a select few: - [_Inventing Monads_](https://stopa.io/post/247) by Stepan Parunashvili - [_How Monads Solve Problems_](https://thatsnomoon.dev/posts/ts-monads/) by ThatsNoMoon - Wiki page [list of monad tutorials](https://wiki.haskell.org/Monad_tutorials_timeline) - [Typeclassopedia](https://wiki.haskell.org/Typeclassopedia) (for the curious) Learning a neat trick on using promises may well translate to atoms, as `Promise.all` and `sequenceAtomArray` did. Monads are not magic, just unusually useful, and a tool worth knowing. --- _Notes_ **[†]** The ES6 Promise is not a completely valid monad because it cannot nest other Promises, e.g. `Promise>` is semantically equivalent to `Promise`. This is why Promises only have a `.then`, and not both a `.map` and `.flatMap`. ES6 Promises are probably more properly described as "monadic" rather than as monads. Unlike ES6 Promises, the ES6 Array is a completely lawful monad. ================================================ FILE: docs/basics/showcase.mdx ================================================ --- title: Showcase nav: 7.03 --- - Text Length example [![Open in CodeSandbox](https://img.shields.io/badge/Open%20in-CodeSandbox-blue?style=flat-square&logo=codesandbox)](https://githubbox.com/pmndrs/jotai/tree/main/examples/text_length) Count the length and show the uppercase of any text. - Hacker News example [![Open in CodeSandbox](https://img.shields.io/badge/Open%20in-CodeSandbox-blue?style=flat-square&logo=codesandbox)](https://githubbox.com/pmndrs/jotai/tree/main/examples/hacker_news) Demonstrate a news article with Jotai, hit next to see more articles. - Todos example [![Open in CodeSandbox](https://img.shields.io/badge/Open%20in-CodeSandbox-blue?style=flat-square&logo=codesandbox)](https://githubbox.com/pmndrs/jotai/tree/main/examples/todos) Record your todo list by typing them into this app, check them off if you have completed the task, and switch between `Completed` and `Incompleted` to see the status of your task. - Todos example with atomFamily and localStorage [![Open in CodeSandbox](https://img.shields.io/badge/Open%20in-CodeSandbox-blue?style=flat-square&logo=codesandbox)](https://githubbox.com/pmndrs/jotai/tree/main/examples/todos_with_atomFamily) Implement a todo list using atomFamily and localStorage. You can store your todo list to localStorage by clicking `Save to localStorage`, then remove your todo list and try restoring them by clicking `Load from localStorage`. - Clock with Next.js [![Open in CodeSandbox](https://img.shields.io/badge/Open%20in-CodeSandbox-blue?style=flat-square&logo=codesandbox)](https://codesandbox.io/s/nextjs-with-jotai-5ylrj) An UTC time electronic clock implementation using Next.js and Jotai. - Tic Tac Toe game [![Open in CodeSandbox](https://img.shields.io/badge/Open%20in-CodeSandbox-blue?style=flat-square&logo=codesandbox)](https://codesandbox.io/s/jotai-tic-tac-6cg3h) A game of tic tac toe implemented with Jotai. - React Three Fiber demo [![Open in CodeSandbox](https://img.shields.io/badge/Open%20in-CodeSandbox-blue?style=flat-square&logo=codesandbox)](https://codesandbox.io/s/jotai-r3f-fri9d) A simple demo to use Jotai with react-three-fiber ================================================ FILE: docs/core/atom.mdx ================================================ --- title: atom description: This doc describes core `jotai` bundle. nav: 2.01 keywords: atom,primitive,derived,debug,label,onmount --- ## atom The `atom` function is to create an atom config. We call it "atom config" as it's just a definition and it doesn't yet hold a value. We may also call it just "atom" if the context is clear. An atom config is an immutable object. The atom config object doesn't hold a value. The atom value exists in a store. To create a primitive atom (config), all you need is to provide an initial value. ```js import { atom } from 'jotai' const priceAtom = atom(10) const messageAtom = atom('hello') const productAtom = atom({ id: 12, name: 'good stuff' }) ``` You can also create derived atoms. We have three patterns: - Read-only atom - Write-only atom - Read-Write atom To create derived atoms, we pass a read function and an optional write function. ```js const readOnlyAtom = atom((get) => get(priceAtom) * 2) const writeOnlyAtom = atom( null, // it's a convention to pass `null` for the first argument (get, set, update) => { // `update` is any single value we receive for updating this atom set(priceAtom, get(priceAtom) - update.discount) // or we can pass a function as the second parameter // the function will be invoked, // receiving the atom's current value as its first parameter set(priceAtom, (price) => price - update.discount) }, ) const readWriteAtom = atom( (get) => get(priceAtom) * 2, (get, set, newPrice) => { set(priceAtom, newPrice / 2) // you can set as many atoms as you want at the same time }, ) ``` `get` in the read function is to read the atom value. It's reactive and read dependencies are tracked. `get` in the write function is also to read atom value, but it's not tracked. Furthermore, it can't read unresolved async values in Jotai v1 API. `set` in the write function is to write atom value. It will invoke the write function of the target atom. ### Note about creating an atom in render function Atom configs can be created anywhere, but referential equality is important. They can be created dynamically too. To create an atom in render function, `useMemo` or `useRef` is required to get a stable reference. If in doubt about using `useMemo` or `useRef` for memoization, use `useMemo`. Otherwise, it can cause infinite loop with `useAtom`. ```js const Component = ({ value }) => { const valueAtom = useMemo(() => atom({ value }), [value]) // ... } ``` ### Signatures ```ts // primitive atom function atom(initialValue: Value): PrimitiveAtom // read-only atom function atom(read: (get: Getter) => Value): Atom // writable derived atom function atom( read: (get: Getter) => Value, write: (get: Getter, set: Setter, ...args: Args) => Result, ): WritableAtom // write-only derived atom function atom( read: Value, write: (get: Getter, set: Setter, ...args: Args) => Result, ): WritableAtom ``` - `initialValue`: the initial value that the atom will return until its value is changed. - `read`: a function that's evaluated whenever the atom is read. The signature of `read` is `(get) => Value`, and `get` is a function that takes an atom config and returns its value stored in Provider as described below. Dependency is tracked, so if `get` is used for an atom at least once, the `read` will be reevaluated whenever the atom value is changed. - `write`: a function mostly used for mutating atom's values, for a better description; it gets called whenever we call the second value of the returned pair of `useAtom`, the `useAtom()[1]`. The default value of this function in the primitive atom will change the value of that atom. The signature of `write` is `(get, set, ...args) => Result`. `get` is similar to the one described above, but it doesn't track the dependency. `set` is a function that takes an atom config and a new value which then updates the atom value in Provider. `...args` is the arguments that we receive when we call `useAtom()[1]`. `Result` is the return value of the `write` function. ```js const primitiveAtom = atom(initialValue) const derivedAtomWithRead = atom(read) const derivedAtomWithReadWrite = atom(read, write) const derivedAtomWithWriteOnly = atom(null, write) ``` There are two kinds of atoms: a writable atom and a read-only atom. Primitive atoms are always writable. Derived atoms are writable if the `write` is specified. The `write` of primitive atoms is equivalent to the `setState` of `React.useState`. ### `debugLabel` property The created atom config can have an optional property `debugLabel`. The debug label is used to display the atom in debugging. See [Debugging guide](../guides/debugging.mdx) for more information. Note: While, the debug labels don’t have to be unique, it’s generally recommended to make them distinguishable. ### `onMount` property The created atom config can have an optional property `onMount`. `onMount` is a function which takes a function `setAtom` and returns `onUnmount` function optionally. The `onMount` function is called when the atom is subscribed in the provider in the first time, and `onUnmount` is called when it’s no longer subscribed. In some cases (like [React strict mode](https://react.dev/reference/react/StrictMode)), an atom can be unmounted and then mounted immediately. ```js const anAtom = atom(1) anAtom.onMount = (setAtom) => { console.log('atom is mounted in provider') setAtom(c => c + 1) // increment count on mount return () => { ... } // return optional onUnmount function } const Component = () => { // `onMount` will be called when the component is mounted in the following cases: useAtom(anAtom) useAtomValue(anAtom) // however, in the following cases, // `onMount` will not be called because the atom is not subscribed: useSetAtom(anAtom) useAtomCallback( useCallback((get) => get(anAtom), []), ) // ... } ``` Calling `setAtom` function will invoke the atom’s `write`. Customizing `write` allows changing the behavior. ```js const countAtom = atom(1) const derivedAtom = atom( (get) => get(countAtom), (get, set, action) => { if (action.type === 'init') { set(countAtom, 10) } else if (action.type === 'inc') { set(countAtom, (c) => c + 1) } }, ) derivedAtom.onMount = (setAtom) => { setAtom({ type: 'init' }) } ``` ### Advanced API Since Jotai v2, the `read` function has the second argument `options`. #### `options.signal` It uses [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) so that you can abort async functions. Abort is triggered before new calculation (invoking `read` function) is started. How to use it: ```ts const readOnlyDerivedAtom = atom(async (get, { signal }) => { // use signal to abort your function }) const writableDerivedAtom = atom( async (get, { signal }) => { // use signal to abort your function }, (get, set, arg) => { // ... }, ) ``` The `signal` value is [AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal). You can check `signal.aborted` boolean value, or use `abort` event with `addEventListener`. For `fetch` use case, we can simply pass `signal`. See the below example for `fetch` usage. #### `options.setSelf` It's a special function to invoke the write function of the self atom. ⚠️ It's provided primarily for internal usage and third-party library authors. Read the source code carefully to understand the behavior. Check release notes for any breaking/non-breaking changes. ## Stackblitz ```tsx import { Suspense } from 'react' import { atom, useAtom } from 'jotai' const userIdAtom = atom(1) const userAtom = atom(async (get, { signal }) => { const userId = get(userIdAtom) const response = await fetch( `https://jsonplaceholder.typicode.com/users/${userId}?_delay=2000`, { signal }, ) return response.json() }) const Controls = () => { const [userId, setUserId] = useAtom(userIdAtom) return (
User Id: {userId}
) } const UserName = () => { const [user] = useAtom(userAtom) return
User name: {user.name}
} const App = () => ( <> ) export default App ``` ================================================ FILE: docs/core/provider.mdx ================================================ --- title: Provider description: This doc describes core `jotai` bundle. nav: 2.04 keywords: provider,usestore,ssr --- ## Provider The `Provider` component is to provide state for a component sub tree. Multiple Providers can be used for multiple subtrees, and they can even be nested. This works just like React Context. If an atom is used in a tree without a Provider, it will use the default state. This is so-called provider-less mode. Providers are useful for three reasons: 1. To provide a different state for each sub tree. 2. To accept initial values of atoms. 3. To clear all atoms by remounting. ```jsx const SubTree = () => ( ) ``` ### Signatures ```ts const Provider: React.FC<{ store?: Store }> ``` Atom configs don't hold values. Atom values reside in separate stores. A Provider is a component that contains a store and provides atom values under the component tree. A Provider works like React context provider. If you don't use a Provider, it works as provider-less mode with a default store. A Provider will be necessary if we need to hold different atom values for different component trees. Provider can take an optional prop `store`. ```jsx const Root = () => ( ) ``` ### `store` prop A Provider accepts an optional prop `store` that you can use for the Provider subtree. #### Example ```jsx const myStore = createStore() const Root = () => ( ) ``` ## useStore This hook returns a store within the component tree. ```jsx const Component = () => { const store = useStore() // ... } ``` ================================================ FILE: docs/core/store.mdx ================================================ --- title: Store description: This doc describes core `jotai` bundle. nav: 2.03 keywords: store,createstore,getdefaultstore,defaultstore --- ## createStore This function is to create a new empty store. The store can be used to pass in `Provider`. The store has three methods: `get` for getting atom values, `set` for setting atom values, and `sub` for subscribing to atom changes. ```jsx const myStore = createStore() const countAtom = atom(0) myStore.set(countAtom, 1) const unsub = myStore.sub(countAtom, () => { console.log('countAtom value is changed to', myStore.get(countAtom)) }) // unsub() to unsubscribe const Root = () => ( ) ``` ## getDefaultStore This function returns a default store that is used in provider-less mode. ```js const defaultStore = getDefaultStore() ``` ================================================ FILE: docs/core/use-atom.mdx ================================================ --- title: useAtom description: This doc describes core `jotai` bundle. nav: 2.02 keywords: use,useatom,useatomvalue,usesetatom,atomvalue,setatom --- ## useAtom The `useAtom` hook is used to read an atom from the state. The state can be seen as a WeakMap of atom configs and atom values. The `useAtom` hook returns the atom value and an update function as a tuple, just like React's `useState`. It takes an atom config created with `atom()` as a parameter. At the creation of the atom config, there is no value associated with it. Only once the atom is used via `useAtom`, does the initial value get stored in the state. If the atom is a derived atom, the read function is called to compute its initial value. When an atom is no longer used, meaning all the components using it are unmounted and the atom config no longer exists, the value in the state is garbage collected. ```js const [value, setValue] = useAtom(anAtom) ``` The `setValue` takes just one argument, which will be passed to the write function of the atom as the third parameter. The end result depends on how the write function is implemented. If the write function is not explicitly set, the atom will simply receive the value passed as a parameter to `setValue`. **Note:** as mentioned in the _atom_ section, referential equality is important when creating atoms, so you need to handle it properly otherwise it can cause infinite loops. ```js const stableAtom = atom(0) const Component = () => { const [atomValue] = useAtom(atom(0)) // This will cause an infinite loop since the atom instance is being recreated in every render const [atomValue] = useAtom(stableAtom) // This is fine const [derivedAtomValue] = useAtom( useMemo( // This is also fine () => atom((get) => get(stableAtom) * 2), [], ), ) } ``` **Note**: Remember that React is responsible for calling your component, meaning it has to be idempotent, ready to be called multiple times. You will often see an extra re-render even if no props or atoms have changed. An extra re-render without a commit is an expected behavior, since it is the default behavior of useReducer in React 18. ### Signatures ```ts // primitive or writable derived atom function useAtom( atom: WritableAtom, options?: { store?: Store }, ): [Value, SetAtom] // read-only atom function useAtom( atom: Atom, options?: { store?: Store }, ): [Value, never] ``` ### How atom dependency works Every time we invoke the "read" function, we refresh the dependencies and dependents. > The read function is the first parameter of the atom. > If B depends on A, it means that A is a dependency of B, and B is a dependent on A. ```js const uppercaseAtom = atom((get) => get(textAtom).toUpperCase()) ``` When you create the atom, the dependency will not be present. On first use, we run the read function and conclude that `uppercaseAtom` depends on `textAtom`. So `uppercaseAtom` is added to the dependents of `textAtom`. When we re-run the read function of `uppercaseAtom` (because its `textAtom` dependency is updated), the dependency is created again, which is the same in this case. We then remove stale dependents from `textAtom` and replace them with their latest versions. ### Atoms can be created on demand While the basic examples here show defining atoms globally outside components, there's no restrictions about where or when we can create an atom. As long as we remember that atoms are identified by their object referential identity, we can create them anytime. If you create atoms in render functions, you would typically want to use a hook like `useRef` or `useMemo` for memoization. If not, the atom would be re-created each time the component renders. You can create an atom and store it with `useState` or even in another atom. See an example in [issue #5](https://github.com/pmndrs/jotai/issues/5). You can also cache atoms somewhere globally. See [this example](https://twitter.com/dai_shi/status/1317653548314718208) or [that example](https://github.com/pmndrs/jotai/issues/119#issuecomment-706046321). Check [`atomFamily`](../utilities/family.mdx) in utils for parameterized atoms. ## useAtomValue ```jsx const countAtom = atom(0) const Counter = () => { const setCount = useSetAtom(countAtom) const count = useAtomValue(countAtom) return ( <>
count: {count}
) } ``` Similar to the `useSetAtom` hook, `useAtomValue` allows you to access a read-only atom. Nonetheless, it can also be used to access read-write atom's values. ## useSetAtom ```jsx const switchAtom = atom(false) const SetTrueButton = () => { const setCount = useSetAtom(switchAtom) const setTrue = () => setCount(true) return (
) } const SetFalseButton = () => { const setCount = useSetAtom(switchAtom) const setFalse = () => setCount(false) return (
) } export default function App() { const state = useAtomValue(switchAtom) return (
State: {state.toString()}
) } ``` In case you need to update a value of an atom without reading it, you can use `useSetAtom()`. This is especially useful when the performance is a concern, as the `const [, setValue] = useAtom(valueAtom)` will cause unnecessary rerenders on each `valueAtom` update. ================================================ FILE: docs/extensions/cache.mdx ================================================ --- title: Cache description: This doc describes cache extension. nav: 4.07 keywords: cache --- Jotai provides primitive functions to optimize re-renders. It's designed to hold only "current" atom values, and it doesn't cache older values. Caching is sometimes useful. For example, if an async atom triggers network requests, we may want to cache the responses. [jotai-cache](https://github.com/jotai-labs/jotai-cache) is a third-party library to help such use cases. ### Install ``` npm install jotai-cache ``` ## atomWithCache ```js atomWithCache(read, options): Atom ``` `atomWithCache` creates a new read-only atom with cache. ### Parameters **read**: read function to define the read-only atom. **options** (optional): an object of options to customize the behavior of the atom ### Options **size** (optional): maximum size of cache items. **shouldRemove** (optional): a function to check if cache items should be removed. **areEqual** (optional): a function to compare atom values. ### Examples ```jsx import { atom, useAtom } from 'jotai' import { atomWithCache } from 'jotai-cache' const idAtom = atom(1) const normalAtom = atom(async (get) => { const id = get(idAtom) const response = await fetch(`https://reqres.in/api/users/${id}?delay=1`) return response.json() }) const cachedAtom = atomWithCache(async (get) => { const id = get(idAtom) const response = await fetch(`https://reqres.in/api/users/${id}?delay=1`) return response.json() }) const NormalUser = () => { const [{ data }] = useAtom(normalAtom) return (

User (normal atom)

  • ID: {data.id}
  • First name: {data.first_name}
  • Last name: {data.last_name}
) } const CachedUser = () => { const [{ data }] = useAtom(cachedAtom) return (

User (cached atom)

  • ID: {data.id}
  • First name: {data.first_name}
  • Last name: {data.last_name}
) } const App = () => { const [id, setId] = useAtom(idAtom) return (
ID: {id}{' '} {' '}

) } ``` ### Stackblitz ================================================ FILE: docs/extensions/effect.mdx ================================================ --- title: Effect description: A Jōtai utility package for reactive side effects nav: 4.03 keywords: effect, atom effect, side effect, side-effect, sideeffect --- [jotai-effect](https://github.com/jotaijs/jotai-effect) is a utility package for reactive side effects in Jotai. ## Install ``` npm install jotai-effect ``` ## observe `observe` mounts an `effect` to watch state changes on a Jotai `store`. It's useful for running global side effects or logic at the store level. If you don't have access to the store object and are not using the default store, use `atomEffect` or `withAtomEffect` instead. ### Signature ```ts type Cleanup = () => void type Effect = ( get: Getter & { peek: Getter } set: Setter & { recurse: Setter } ) => Cleanup | void type Unobserve = () => void function observe(effect: Effect, store?: Store): Unobserve ``` **effect:** A function for observing and reacting to atom state changes. **store:** A Jotai store to mount the effect on. Defaults to the global store if not provided. **returns:** A stable function that removes the effect from the store and cleans up any internal references. ### Usage ```js import { observe } from 'jotai-effect' const unobserve = observe((get, set) => { set(logAtom, `someAtom changed: ${get(someAtom)}`) }) unobserve() ``` This allows you to run Jotai state-dependent logic outside React's lifecycle, ideal for application-wide effects. ### Usage With React Pass the store to both `observe` and the `Provider` to ensure the effect is mounted to the correct store. ```tsx const store = createStore() const unobserve = observe((get, set) => { set(logAtom, `someAtom changed: ${get(someAtom)}`) }, store) ... ``` ## atomEffect `atomEffect` creates an atom for declaring side effects that react to state changes when mounted. ### Signature ```ts function atomEffect(effect: Effect): Atom ``` **effect:** A function for observing and reacting to atom state changes. ### Usage ```js import { atomEffect } from 'jotai-effect' const logEffect = atomEffect((get, set) => { set(logAtom, get(someAtom)) // Runs on mount or when someAtom changes return () => { set(logAtom, 'unmounting') // Cleanup on unmount } }) // activates the atomEffect while Component is mounted function Component() { useAtom(logEffect) } ``` ## withAtomEffect `withAtomEffect` binds an effect to a clone of the target atom. The effect is active while the cloned atom is mounted. ### Signature ```ts function withAtomEffect(targetAtom: Atom, effect: Effect): Atom ``` **targetAtom:** The atom to which the effect is bound. **effect:** A function for observing and reacting to atom state changes. **Returns:** An atom that is equivalent to the target atom but having a bound effect. ### Usage ```js import { withAtomEffect } from 'jotai-effect' const valuesAtom = withAtomEffect(atom(null), (get, set) => { set(valuesAtom, get(countAtom)) return () => { // cleanup } }) ``` ## Dependency Management Aside from mount events, the effect runs when any of its dependencies change value. - **Sync:** All atoms accessed with `get` inside the effect are added to the atom's dependencies.
Example ```js atomEffect((get, set) => { // updates whenever `anAtom` changes value get(anAtom) }) ```
- **Async:** Asynchronous `get` calls do not add dependencies.
Example ```js atomEffect((get, set) => { setTimeout(() => { // does not add `anAtom` as a dependency get(anAtom) }) }) ```
- **Cleanup:** `get` calls in cleanup do not add dependencies.
Example ```js atomEffect((get, set) => { return () => { // does not add `anAtom` as a dependency get(anAtom) } }) ```
- **Dependency Map Recalculation:** Dependencies are recalculated on every run.
Example ```js atomEffect((get, set) => { if (get(isEnabledAtom)) { // `isEnabledAtom` and `anAtom` are dependencies const aValue = get(anAtom) } else { // `isEnabledAtom` and `anotherAtom` are dependencies const anotherValue = get(anotherAtom) } }) ```
## Effect Behavior - **Executes Synchronously:** `effect` runs synchronous in the current task after synchronous evaluations complete.
Example ```js const logCounts = atomEffect((get, set) => { set(logAtom, `count is ${get(countAtom)}`) }) const actionAtom = atom(null, (get, set) => { get(logAtom) // 'count is 0' set(countAtom, (value) => value + 1) // effect runs synchronously get(logAtom) // 'count is 1' }) store.sub(logCounts, () => {}) store.set(actionAtom) ```
- **Batched Updates:** Multiple synchronous updates are batched as a single atomic transaction.
Example ```js const tensAtom = atom(0) const onesAtom = atom(0) const updateTensAndOnes = atom(null, (get, set) => { set(tensAtom, (value) => value + 1) set(onesAtom, (value) => value + 1) }) const combos = atom([]) const effectAtom = atomEffect((get, set) => { const value = get(tensAtom) * 10 + get(onesAtom) set(combos, (arr) => [...arr, value]) }) store.sub(effectAtom, () => {}) store.set(updateTensAndOnes) store.get(combos) // [00, 11] ```
- **Resistant to Infinite Loops:** `atomEffect` avoids rerunning when it updates a value that it is watching.
Example ```js atomEffect((get, set) => { get(countAtom) set(countAtom, (value) => value + 1) // Will not loop }) ```
- **Cleanup Function:** The cleanup function is invoked on unmount or before re-evaluation.
Example ```js atomEffect((get, set) => { const intervalId = setInterval(() => set(clockAtom, Date.now())) return () => clearInterval(intervalId) }) ```
- **Idempotency:** `atomEffect` runs once per state change, regardless of how many times it is referenced.
Example ```js let i = 0 const effectAtom = atomEffect(() => { get(countAtom) i++ }) store.sub(effectAtom, () => {}) store.sub(effectAtom, () => {}) store.set(countAtom, (value) => value + 1) console.log(i) // 1 ```
- **Conditionally Running Effects:** `atomEffect` only runs when mounted.
Example ```js atom((get) => { if (get(isEnabledAtom)) { get(effectAtom) } }) ```
- **Supports Peek:** Use `get.peek` to read atom data without subscribing.
Example ```js const countAtom = atom(0) atomEffect((get, set) => { const count = get.peek(countAtom) // Will not add `countAtom` as a dependency }) ```
- **Supports Recursion:** Recursion is supported with `set.recurse` but not in cleanup.
Example ```js atomEffect((get, set) => { const count = get(countAtom) if (count % 10 === 0) { return } set.recurse(countAtom, (value) => value + 1) }) ```
================================================ FILE: docs/extensions/immer.mdx ================================================ --- title: Immer description: This doc describes Immer extension. nav: 4.04 keywords: immer --- ### Install You have to install `immer` and `jotai-immer` to use this feature. ``` npm install immer jotai-immer ``` ## atomWithImmer `atomWithImmer` creates a new atom similar to the regular [`atom`](../core/atom.mdx) with a different `writeFunction`. In this bundle, we don't have read-only atoms, because the point of these functions is the immer produce(mutability) function. The signature of writeFunction is `(get, set, update: (draft: Draft) => void) => void`. ```jsx import { useAtom } from 'jotai' import { atomWithImmer } from 'jotai-immer' const countAtom = atomWithImmer({ value: 0 }) const Counter = () => { const [count] = useAtom(countAtom) return
count: {count.value}
} const Controls = () => { const [, setCount] = useAtom(countAtom) // setCount === update : (draft: Draft) => void const inc = () => setCount((draft) => { ++draft.value }) return } ``` ### Examples Check this example with atomWithImmer: ## withImmer `withImmer` takes an atom and returns a derived atom, same as `atomWithImmer` it has a different `writeFunction`. ```jsx import { useAtom, atom } from 'jotai' import { withImmer } from 'jotai-immer' const primitiveAtom = atom({ value: 0 }) const countAtom = withImmer(primitiveAtom) const Counter = () => { const [count] = useAtom(countAtom) return
count: {count.value}
} const Controls = () => { const [, setCount] = useAtom(countAtom) // setCount === update : (draft: Draft) => void const inc = () => setCount((draft) => { ++draft.value }) return } ``` ### Examples Check this example with withImmer: ## useImmerAtom This hook takes an atom and replaces the atom's `writeFunction` with the new immer-like `writeFunction` like the previous helpers. ```jsx import { atom } from 'jotai' import { useImmerAtom } from 'jotai-immer' const primitiveAtom = atom({ value: 0 }) const Counter = () => { const [count] = useImmerAtom(primitiveAtom) return
count: {count.value}
} const Controls = () => { const [, setCount] = useImmerAtom(primitiveAtom) // setCount === update : (draft: Draft) => void const inc = () => setCount((draft) => { ++draft.value }) return } ``` It would be better if you don't use `withImmer` and `atomWithImmer` with `useImmerAtom` because they provide the immer-like `writeFunction` and we don't need to create a new one. You can use `useSetImmerAtom` if you need only the setter part of `useImmerAtom`. ### Examples Check this example with useImmerAtom: ## Demo ================================================ FILE: docs/extensions/location.mdx ================================================ --- title: Location description: This doc describes window.location extension. nav: 4.06 keywords: location,hash --- To deal with `window.location`, we have some functions to create atoms. ### Install You have to install `jotai-location` to use this feature. ``` npm install jotai-location ``` ## atomWithLocation ```js atomWithLocation(options): PrimitiveAtom ``` `atomWithLocation` creates a new atom that links to `window.location`. Typically, you should only instantiate `atomWithLocation` once per application. This is because changes made to one instance might not be reflected in others. As this atom is designed to synchronize with the `window.location` object, using multiple instances can lead to unpredictable behavior. ### Parameters **options** (optional): an object of options to customize the behavior of the atom ### Options **preloaded** (optional): preloaded location value to avoid getting location at initialization. **replace** (optional): a boolean to indicate to use `replaceState` instead of `pushState`. **getLocation** (optional): a custom function to get location value. **applyLocation** (optional): a custom function to set location value. **subscribe** (optional): a custom function to subscribe to location change. ### Examples ```jsx import { useAtom } from 'jotai' import { atomWithLocation } from 'jotai-location' const locationAtom = atomWithLocation() const App = () => { const [loc, setLoc] = useAtom(locationAtom) return (
) } ``` ### Stackblitz ## atomWithHash ```js atomWithHash(key, initialValue, options): PrimitiveAtom ``` This creates a new atom that is connected with URL hash. The hash must be in the URLSearchParams format. It's a two-way binding: changing the atom value will change the hash and changing the hash will change the atom value. This function works only with DOM. ### Parameters **key** (required): a unique string used as the key when syncing state with localStorage, sessionStorage, or AsyncStorage **initialValue** (required): the initial value of the atom **options** (optional): an object of options to customize the behavior of the atom ### Options **serialize** (optional): a custom function to serialize the atom value to the hash. Defaults to `JSON.stringify`. **deserialize** (optional): a custom function to deserialize the hash to the atom value. Defaults to `JSON.parse`. **subscribe** (optional): custom hash change subscribe function **setHash** (optional): `replaceState` or a custom function that describes how hash gets updated on the side of the browser. Defaults to pushing a new entry to the history via `window.location.hash = searchParams` ([jotai-location#4](https://github.com/jotaijs/jotai-location/pull/4)) ### Examples ```jsx import { useAtom } from 'jotai' import { atomWithHash } from 'jotai-location' const countAtom = atomWithHash('count', 1) const Counter = () => { const [count, setCount] = useAtom(countAtom) return (
count: {count}
) } ``` ### Stackblitz ### Deleting Item Please refer [atomWithStorage](../utilities/storage.mdx) for the usage about deleting items. ================================================ FILE: docs/extensions/optics.mdx ================================================ --- title: Optics description: This doc describes Optics-ts extension. nav: 4.09 keywords: optics --- ### Install You have to install `optics-ts` and `jotai-optics` to use this feature. ``` npm install optics-ts jotai-optics ``` ## focusAtom `focusAtom` creates a new atom, based on the focus that you pass to it. This creates a derived atom that will focus on the specified part of the atom, and when the derived atom is updated, the derivee is notified of the update, and the equivalent update is done on the derivee. See this: ```js const baseAtom = atom({ a: 5 }) // PrimitiveAtom<{a: number}> const derivedAtom = focusAtom(baseAtom, (optic) => optic.prop('a')) // PrimitiveAtom ``` So basically, we started with a `PrimitiveAtom<{a: number}>`, which has a getter and a setter, and then used `focusAtom` to zoom in on the `a`-property of the `baseAtom`, and got a `PrimitiveAtom`. What is noteworthy here is that this `derivedAtom` is not only a getter, it is also a setter. If `derivedAtom` is updated, then equivalent update is done on the `baseAtom`. The example below is simple, but it's a starting point. `focusAtom` supports many kinds of optics, including `Lens`, `Prism`, `Isomorphism`. To see more advanced optics, please see the example at: https://github.com/akheron/optics-ts ### Example ```jsx import { atom } from 'jotai' import { focusAtom } from 'jotai-optics' const objectAtom = atom({ a: 5, b: 10 }) const aAtom = focusAtom(objectAtom, (optic) => optic.prop('a')) const bAtom = focusAtom(objectAtom, (optic) => optic.prop('b')) const Controls = () => { const [a, setA] = useAtom(aAtom) const [b, setB] = useAtom(bAtom) return (
Value of a: {a} Value of b: {b}
) } ``` #### Stackblitz ================================================ FILE: docs/extensions/query.mdx ================================================ --- title: Query description: This doc describes TanStack Query extension. nav: 4.02 keywords: tanstack,query --- [TanStack Query](https://tanstack.com/query/) provides a set of functions for managing async state (typically external data). From the [Overview docs](https://tanstack.com/query/v5/docs/framework/react/overview): > React Query is often described as the missing data-fetching library for React, but in more technical terms, it makes **fetching, caching, synchronizing and updating server state** in your React applications a breeze. [jotai-tanstack-query](https://github.com/jotai-labs/jotai-tanstack-query) is a Jotai extension library for TanStack Query. It provides a wonderful interface with all of the TanStack Query features, providing you the ability to use those features in combination with your existing Jotai state. ### Support jotai-tanstack-query currently supports TanStack Query v5. ### Install In addition to `jotai`, you have to install `jotai-tanstack-query` and `@tanstack/query-core` to use the extension. ```bash npm install jotai-tanstack-query @tanstack/query-core ``` ### Incremental Adoption You can incrementally adopt `jotai-tanstack-query` in your app. It's not an all or nothing solution. You just have to ensure you are using the same QueryClient instance. [QueryClient Setup](#referencing-the-same-instance-of-query-client-in-your-project). ```jsx // existing useQueryHook const { data, isPending, isError } = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, }) // jotai-tanstack-query const todosAtom = atomWithQuery(() => ({ queryKey: ['todos'], })) const [{ data, isPending, isError }] = useAtom(todosAtom) ``` ### Exported functions - `atomWithQuery` for [useQuery](https://tanstack.com/query/v5/docs/react/reference/useQuery) - `atomWithInfiniteQuery` for [useInfiniteQuery](https://tanstack.com/query/v5/docs/react/reference/useInfiniteQuery) - `atomWithMutation` for [useMutation](https://tanstack.com/query/v5/docs/react/reference/useMutation) - `atomWithSuspenseQuery` for [useSuspenseQuery](https://tanstack.com/query/v5/docs/react/reference/useSuspenseQuery) - `atomWithSuspenseInfiniteQuery` for [useSuspenseInfiniteQuery](https://tanstack.com/query/v5/docs/react/reference/useSuspenseInfiniteQuery) - `atomWithMutationState` for [useMutationState](https://tanstack.com/query/v5/docs/react/reference/useMutationState) All functions follow the same signature. ```ts const dataAtom = atomWithSomething(getOptions, getQueryClient) ``` The first `getOptions` parameter is a function that returns an input to the observer. The second optional `getQueryClient` parameter is a function that return [QueryClient](https://tanstack.com/query/v5/docs/reference/QueryClient). ### atomWithQuery usage `atomWithQuery` creates a new atom that implements a standard [`Query`](https://tanstack.com/query/v5/docs/react/guides/queries) from TanStack Query. ```jsx import { atom, useAtom } from 'jotai' import { atomWithQuery } from 'jotai-tanstack-query' const idAtom = atom(1) const userAtom = atomWithQuery((get) => ({ queryKey: ['users', get(idAtom)], queryFn: async ({ queryKey: [, id] }) => { const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`) return res.json() }, })) const UserData = () => { const [{ data, isPending, isError }] = useAtom(userAtom) if (isPending) return
Loading...
if (isError) return
Error
return
{JSON.stringify(data)}
} ``` ### atomWithInfiniteQuery usage `atomWithInfiniteQuery` is very similar to `atomWithQuery`, however it is for an `InfiniteQuery`, which is used for data that is meant to be paginated. You can [read more about Infinite Queries here](https://tanstack.com/query/v5/docs/framework/react/guides/infinite-queries). > Rendering lists that can additively "load more" data onto an existing set of data or "infinite scroll" is also a very common UI pattern. React Query supports a useful version of useQuery called useInfiniteQuery for querying these types of lists. A notable difference between a standard query atom is the additional option `getNextPageParam` and `getPreviousPageParam`, which is what you'll use to instruct the query on how to fetch any additional pages. ```jsx import { atom, useAtom } from 'jotai' import { atomWithInfiniteQuery } from 'jotai-tanstack-query' const postsAtom = atomWithInfiniteQuery(() => ({ queryKey: ['posts'], queryFn: async ({ pageParam }) => { const res = await fetch(`https://jsonplaceholder.typicode.com/posts?_page=${pageParam}`) return res.json() }, getNextPageParam: (lastPage, allPages, lastPageParam) => lastPageParam + 1, initialPageParam: 1, })) const Posts = () => { const [{ data, fetchNextPage, isPending, isError, isFetching }] = useAtom(postsAtom) if (isPending) return
Loading...
if (isError) return
Error
return ( <> {data.pages.map((page, index) => (
{page.map((post: any) => (
{post.title}
))}
))} ) } ``` ### atomWithMutation usage `atomWithMutation` creates a new atom that implements a standard [`Mutation`](https://tanstack.com/query/v5/docs/framework/react/guides/mutations) from TanStack Query. > Unlike queries, mutations are typically used to create/update/delete data or perform server side-effects. ```tsx const postAtom = atomWithMutation(() => ({ mutationKey: ['posts'], mutationFn: async ({ title }: { title: string }) => { const res = await fetch(`https://jsonplaceholder.typicode.com/posts`, { method: 'POST', body: JSON.stringify({ title, body: 'body', userId: 1, }), headers: { 'Content-type': 'application/json; charset=UTF-8', }, }) const data = await res.json() return data }, })) const Posts = () => { const [{ mutate, status }] = useAtom(postAtom) return (
{JSON.stringify(status, null, 2)}
) } ``` ### atomWithMutationState usage `atomWithMutationState` creates a new atom that gives you access to all mutations in the [`MutationCache`](https://tanstack.com/query/v5/docs/react/reference/useMutationState). ```jsx const mutationStateAtom = atomWithMutationState((get) => ({ filters: { mutationKey: ['posts'], }, })) ``` ### Suspense jotai-tanstack-query can also be used with React's Suspense. ### atomWithSuspenseQuery usage ```jsx import { atom, useAtom } from 'jotai' import { atomWithSuspenseQuery } from 'jotai-tanstack-query' const idAtom = atom(1) const userAtom = atomWithSuspenseQuery((get) => ({ queryKey: ['users', get(idAtom)], queryFn: async ({ queryKey: [, id] }) => { const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`) return res.json() }, })) const UserData = () => { const [{ data }] = useAtom(userAtom) return
{JSON.stringify(data)}
} ``` ### atomWithSuspenseInfiniteQuery usage ```jsx import { atom, useAtom } from 'jotai' import { atomWithSuspenseInfiniteQuery } from 'jotai-tanstack-query' const postsAtom = atomWithSuspenseInfiniteQuery(() => ({ queryKey: ['posts'], queryFn: async ({ pageParam }) => { const res = await fetch(`https://jsonplaceholder.typicode.com/posts?_page=${pageParam}`) return res.json() }, getNextPageParam: (lastPage, allPages, lastPageParam) => lastPageParam + 1, initialPageParam: 1, })) const Posts = () => { const [{ data, fetchNextPage, isPending, isError, isFetching }] = useAtom(postsAtom) return ( <> {data.pages.map((page, index) => (
{page.map((post: any) => (
{post.title}
))}
))} ) } ``` ### Referencing the same instance of Query Client in your project Perhaps you have some custom hooks in your project that utilises the `useQueryClient()` hook to obtain the `QueryClient` object and call its methods. To ensure that you reference the same `QueryClient` object, be sure to wrap the root of your project in a `` and initialise `queryClientAtom` with the same `queryClient` value you provided to `QueryClientProvider`. Without this step, `useQueryAtom` will reference a separate `QueryClient` from any hooks that utilise the `useQueryClient()` hook to get the queryClient. Alternatively, you can specify your `queryClient` with `getQueryClient` parameter. #### Example In the example below, we have a mutation hook, `useTodoMutation` and a query `todosAtom`. We included an initialisation step in our root `` node. Although they reference methods same query key (`'todos'`), the `onSuccess` invalidation in `useTodoMutation` will not trigger **if the `Provider` initialisation step was not done.** This will result in `todosAtom` showing stale data as it was not prompted to refetch. ⚠️ Note: When using **Typescript**, it is recommended to use a Map when passing the queryClient value to useHydrateAtoms. You can find a working example in the [Initializing State on Render docs](https://jotai.org/docs/guides/initialize-atom-on-render#using-typescript) ```jsx import { Provider } from 'jotai/react' import { useHydrateAtoms } from 'jotai/react/utils' import { useMutation, useQueryClient, QueryClient, QueryClientProvider, } from '@tanstack/react-query' import { atomWithQuery, queryClientAtom } from 'jotai-tanstack-query' const queryClient = new QueryClient() const HydrateAtoms = ({ children }) => { useHydrateAtoms([[queryClientAtom, queryClient]]) return children } export const App = () => { return ( {/* This Provider initialisation step is needed so that we reference the same queryClient in both atomWithQuery and other parts of the app. Without this, our useQueryClient() hook will return a different QueryClient object */} ) } export const todosAtom = atomWithQuery((get) => { return { queryKey: ['todos'], queryFn: () => fetch('/todos'), } }) export const useTodoMutation = () => { const queryClient = useQueryClient() return useMutation( async (body: todo) => { await fetch('/todo', { Method: 'POST', Body: body }) }, { onSuccess: () => { void queryClient.invalidateQueries(['todos']) }, onError, } ) } ``` ### SSR support All atoms can be used within the context of a server side rendered app, such as a next.js app or Gatsby app. You can [use both options](https://tanstack.com/query/v5/docs/framework/react/guides/ssr) that React Query supports for use within SSR apps, [hydration](https://tanstack.com/query/v5/docs/react/guides/ssr#using-the-hydration-apis) or [`initialData`](https://tanstack.com/query/v5/docs/react/guides/ssr#get-started-fast-with-initialdata). ### Error handling Fetch error will be thrown and can be caught with ErrorBoundary. Refetching may recover from a temporary error. See [a working example](https://codesandbox.io/s/4gfp6z) to learn more. ### Devtools In order to use the Devtools, you need to install it additionally. ```bash npm install @tanstack/react-query-devtools ``` All you have to do is put the `` within ``. ```tsx import { QueryClientProvider, QueryClient, QueryCache, } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' import { queryClientAtom } from 'jotai-tanstack-query' const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: Infinity, }, }, }) const HydrateAtoms = ({ children }) => { useHydrateAtoms([[queryClientAtom, queryClient]]) return children } export const App = () => { return ( ) } ``` ## Migrate to v0.8.0 ### Change in atom signature All atom signatures have changed to be more consistent with TanStack Query. v0.8.0 returns only a single atom, instead of a tuple of atoms, and hence the name change from `atomsWithSomething` to`atomWithSomething`. ```diff - const [dataAtom, statusAtom] = atomsWithSomething(getOptions, getQueryClient) + const dataAtom = atomWithSomething(getOptions, getQueryClient) ``` ### Simplified Return Structure In the previous version of `jotai-tanstack-query`, the query atoms `atomsWithQuery` and `atomsWithInfiniteQuery` returned a tuple of atoms: `[dataAtom, statusAtom]`. This design separated the data and its status into two different atoms. #### atomWithQuery and atomWithInfiniteQuery - `dataAtom` was used to access the actual data (`TData`). - `statusAtom` provided the status object (`QueryObserverResult`), which included additional attributes like `isPending`, `isError`, etc. In v0.8.0, they have been replaced by `atomWithQuery` and `atomWithInfiniteQuery` to return only a single `dataAtom`. This `dataAtom` now directly provides the `QueryObserverResult`, aligning it closely with the behavior of Tanstack Query's bindings. To migrate to the new version, replace the separate `dataAtom` and `statusAtom` usage with the unified `dataAtom` that now contains both data and status information. ```diff - const [dataAtom, statusAtom] = atomsWithQuery(/* ... */); - const [data] = useAtom(dataAtom); - const [status] = useAtom(statusAtom); + const dataAtom = atomWithQuery(/* ... */); + const [{ data, isPending, isError }] = useAtom(dataAtom); ``` #### atomWithMutation Similar to `atomsWithQuery` and `atomsWithInfiniteQuery`, `atomWithMutation` also returns a single atom instead of a tuple of atoms. The return type of the atom value is `MutationObserverResult`. ```diff - const [, postAtom] = atomsWithMutation(/* ... */); - const [post, mutate] = useAtom(postAtom); // Accessing mutation status from post; and mutate() to execute the mutation + const postAtom = atomWithMutation(/* ... */); + const [{ data, error, mutate }] = useAtom(postAtom); // Accessing mutation result and mutate method from the same atom ``` ### Examples #### Basic demo #### Devtools demo #### Hackernews ================================================ FILE: docs/extensions/redux.mdx ================================================ --- title: Redux description: This doc describes Redux extension. nav: 4.98 keywords: redux published: false --- Jotai's state resides in React, but sometimes it would be nice to interact with the world outside React. Redux provides a store interface that can be used to store some values and sync with atoms in Jotai. ### Install You have to install `redux` and `jotai-redux` to use this feature. ``` npm install redux jotai-redux ``` ## atomWithStore `atomWithStore` creates a new atom with redux store. It's two-way binding and you can change the value from both ends. ```jsx import { useAtom } from 'jotai' import { atomWithStore } from 'jotai-redux' import { createStore } from 'redux' const initialState = { count: 0 } const reducer = (state = initialState, action: { type: 'INC' }) => { if (action.type === 'INC') { return { ...state, count: state.count + 1 } } return state } const store = createStore(reducer) const storeAtom = atomWithStore(store) const Counter = () => { const [state, dispatch] = useAtom(storeAtom) return ( <> count: {state.count} ) } ``` ### Examples ================================================ FILE: docs/extensions/relay.mdx ================================================ --- title: Relay description: This doc describes Relay extension. nav: 4.98 keywords: relay published: false --- You can use Jotai with [Relay](https://relay.dev). ### Install You have to install `jotai-relay` and `relay-runtime`. ``` npm install jotai-relay relay-runtime ``` ### Usage See [Relay Docs](https://relay.dev/docs/) to learn about basics and how to use compiler in advance. ### atomWithQuery `atomWithQuery` creates a new atom with [fetchQuery](https://relay.dev/docs/api-reference/fetch-query/). ```jsx import React, { Suspense } from 'react' import { Provider, useAtom } from 'jotai' import { useHydrateAtoms } from 'jotai/utils' import { environmentAtom, atomWithQuery } from 'jotai-relay' import { Environment, Network, RecordSource, Store } from 'relay-runtime' import graphql from 'babel-plugin-relay/macro' const myEnvironment = new Environment({ network: Network.create(async (params, variables) => { const response = await fetch('https://countries.trevorblades.com/', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ query: params.text, variables, }), }) return response.json() }), store: new Store(new RecordSource()), }) const countriesAtom = atomWithQuery( graphql` query AppCountriesQuery { countries { name } } `, () => ({}), ) const Main = () => { const [data] = useAtom(countriesAtom) return (
    {data.countries.map(({ name }) => (
  • {name}
  • ))}
) } const HydrateAtoms = ({ children }) => { useHydrateAtoms([[environmentAtom, myEnvironment]]) return children } const App = () => { return (
) } ``` #### Examples ### atomWithMutation `atomWithMutation` creates a new atom with [commitMutation](https://relay.dev/docs/api-reference/commit-mutation/). FIXME: add code example and codesandbox ### atomWithSubscription `atomWithSubscription` creates a new atom with [requestSubscription](https://relay.dev/docs/api-reference/request-subscription/). FIXME: add code example and codesandbox ================================================ FILE: docs/extensions/scope.mdx ================================================ --- title: Scope description: This doc describes scope extension. nav: 4.08 keywords: scope --- There are a few libraries to extend Jotai's usage in React. ## `jotai-scope` While Jotai's Provider allows to scope Jotai's store under a subtree, we can't use the store above the tree within the subtree. A workaround is to use `store` option in useAtom and other hooks. Instead of specifying the `store` option, `` lets you reuse the _same_ atoms in different parts of the React tree **without sharing state** while still being able to read other atoms from the parent store. ### Install ``` npm install jotai-scope ``` ### Counter Example ```tsx import { atom, useAtom } from 'jotai' import { ScopeProvider } from 'jotai-scope' const countAtom = atom(0) function Counter() { const [count, setCount] = useAtom(countAtom) const [anotherCount, setAnotherCount] = useAtom(anotherCountAtom) return ( <>
count: {count}
another count: {anotherCount}
) } function App() { return (
) } ``` ## `createIsolation` Both Jotai's Provider and `jotai-scope`'s scoped provider are still using global contexts. If you are developing a library that depends on Jotai and the library user may use Jotai separately in their apps, they can share the same context. This can be troublesome because they point to unexpected Jotai stores. To avoid conflicting the contexts, a utility function called `createIsolation` is exported from `jotai-scope`. ```tsx import { createIsolation } from 'jotai-scope' const { Provider, useStore, useAtom, useAtomValue, useSetAtom } = createIsolation() function Library() { return ( ) } ``` ## `bunshi` (formerly `jotai-molecules`) Jotai atoms provide a basic solution to optimize re-renders. Atoms defined globally can depend on other atoms, but they can't depend on props and state within a component tree. It's possible to define atoms within a component tree, but then you would need to pass those atoms in some ways (for example, [atoms-in-atom](../guides/atoms-in-atom.mdx).) [bunshi](https://github.com/saasquatch/bunshi) is a third-party library to help such use cases. See [Motivation](https://github.com/saasquatch/bunshi/tree/v1.1.1#motivation) for more details. ### Install ``` npm install bunshi ``` ### Counter Example ```jsx import { atom, useAtom } from 'jotai' import { molecule, useMolecule, createScope, ScopeProvider } from 'bunshi/react' const InitialCountScope = createScope({ initialCount: 0 }) const countMolecule = molecule((getMol, getScope) => { const { initialCount } = getScope(InitialCountScope) return atom(initialCount) }) function Counter() { const countAtom = useMolecule(countMolecule) const [count, setCount] = useAtom(countAtom) return (
{count}
) } function App() { return (

With initial value 1

With initial value 2

Default

) } ``` ================================================ FILE: docs/extensions/trpc.mdx ================================================ --- title: tRPC description: This doc describes tRPC extension. nav: 4.01 keywords: rpc,trpc,typescript,t3 --- You can use Jotai with [tRPC](https://trpc.io). ### Install You have to install `jotai-trpc`, `@trpc/client` and `@trpc/server` to use the extension. ``` npm install jotai-trpc @trpc/client @trpc/server ``` ### Usage ```ts import { createTRPCJotai } from 'jotai-trpc' const trpc = createTRPCJotai({ links: [ httpLink({ url: myUrl, }), ], }) const idAtom = atom('foo') const queryAtom = trpc.bar.baz.atomWithQuery((get) => get(idAtom)) ``` ### atomWithQuery `...atomWithQuery` creates a new atom with "query". It internally uses [Vanilla Client](https://trpc.io/docs/vanilla)'s `...query` procedure. ```tsx import { atom, useAtom } from 'jotai' import { httpLink } from '@trpc/client' import { createTRPCJotai } from 'jotai-trpc' import { trpcPokemonUrl } from 'trpc-pokemon' import type { PokemonRouter } from 'trpc-pokemon' const trpc = createTRPCJotai({ links: [ httpLink({ url: trpcPokemonUrl, }), ], }) const NAMES = [ 'bulbasaur', 'ivysaur', 'venusaur', 'charmander', 'charmeleon', 'charizard', 'squirtle', 'wartortle', 'blastoise', ] const nameAtom = atom(NAMES[0]) const pokemonAtom = trpc.pokemon.byId.atomWithQuery((get) => get(nameAtom)) const Pokemon = () => { const [data, refresh] = useAtom(pokemonAtom) return (
ID: {data.id}
Height: {data.height}
Weight: {data.weight}
) } ``` #### Examples ### atomWithMutation `...atomWithMutation` creates a new atom with "mutate". It internally uses [Vanilla Client](https://trpc.io/docs/vanilla)'s `...mutate` procedure. FIXME: add code example and codesandbox ### atomWithSubscription `...atomWithSubscription` creates a new atom with "subscribe". It internally uses [Vanilla Client](https://trpc.io/docs/vanilla)'s `...subscribe` procedure. FIXME: add code example and codesandbox ================================================ FILE: docs/extensions/urql.mdx ================================================ --- title: URQL description: This doc describes URQL extension. nav: 4.03 keywords: urql --- [urql](https://formidable.com/open-source/urql/) offers a toolkit for GraphQL querying, caching, and state management. From the [Overview docs](https://formidable.com/open-source/urql/docs/): > urql is a highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow. It's built to be both easy to use for newcomers to GraphQL, and extensible, to grow to support dynamic single-app applications and highly customized GraphQL infrastructure. In short, urql prioritizes usability and adaptability. [jotai-urql](https://github.com/jotaijs/jotai-urql) is a Jotai extension library for URQL. It offers a cohesive interface that incorporates all of URQL's GraphQL features, allowing you to leverage these functionalities alongside your existing Jotai state. ### Install You have to install `jotai-urql`, `@urql/core` and `wonka` to use the extension. ``` npm install jotai-urql @urql/core wonka ``` ### Exported functions - `atomWithQuery` for [client.query](https://formidable.com/open-source/urql/docs/api/core/#clientquery) - `atomWithMutation` for [client.mutation](https://formidable.com/open-source/urql/docs/api/core/#clientmutation) - `atomWithSubscription` for [client.subscription](https://formidable.com/open-source/urql/docs/api/core/#clientsubscription) ### Basic usage #### Query: ```tsx import { useAtom } from 'jotai' const countQueryAtom = atomWithQuery<{ count: number }>({ query: 'query Count { count }', getClient: () => client, // This option is optional if `useRehydrateAtom([[clientAtom, client]])` is used globally }) const Counter = () => { // Will suspend until first operation result is resolved. Either with error, partial data, data const [operationResult, reexecute] = useAtom(countQueryAtom) if (operationResult.error) { // This shall be handled in the parent ErrorBoundary above throw operationResult.error } // You have to use optional chaining here, as data may be undefined at this point (only in case of error) return <>{operationResult.data?.count} } ``` #### Mutation: ```tsx import { useAtom } from 'jotai' const incrementMutationAtom = atomWithMutation<{ increment: number }>({ query: 'mutation Increment { increment }', }) const Counter = () => { const [operationResult, executeMutation] = useAtom(incrementMutationAtom) return (
{operationResult.data?.increment}
) } ``` ### Simplified type of options passed to functions ```tsx type AtomWithQueryOptions< Data = unknown, Variables extends AnyVariables = AnyVariables, > = { // Supports string query, typed-document-node, document node etc. query: DocumentInput // Will be enforced dynamically based on generic/typed-document-node types. getVariables?: (get: Getter) => Variables getContext?: (get: Getter) => Partial getPause?: (get: Getter) => boolean getClient?: (get: Getter) => Client } type AtomWithMutationOptions< Data = unknown, Variables extends AnyVariables = AnyVariables, > = { query: DocumentInput getClient?: (get: Getter) => Client } // Subscription type is the same as AtomWithQueryOptions ``` ### Disable suspense Usage of `import { loadable } from "jotai/utils"` is preferred instead as proven more stable. However is you still want that here is how you do it: ```tsx import { suspenseAtom } from 'jotai-urql' export const App = () => { // We disable suspense for the entire app useHydrateAtoms([[suspenseAtom, false]]) return } ``` ### Useful helper hook Here is the helper hook, to cover one rare corner case, and make use of these bindings similar to `@tanstack/react-query` default behavior where errors are treated as errors (in case of Promise reject) and are handled mostly in the nearby ErrorBoundaries. Only valid for suspended version. #### useQueryAtomData Neatly returns `data` after the resolution + handles all the error throwing/reexecute cases/corner cases. Note that Type is overridden so `data` it never `undefined` nor `null` (unless that's expected return type of the query itself) ```tsx import type { AnyVariables, OperationResult } from '@urql/core' import { useAtom } from 'jotai' import type { AtomWithQuery } from 'jotai-urql' export const useQueryAtomData = < Data = unknown, Variables extends AnyVariables = AnyVariables, >( queryAtom: AtomWithQuery, ) => { const [opResult, dispatch] = useAtom(queryAtom) if (opResult.error && opResult.stale) { use( // Here we suspend the tree. This will only be triggered in the scenario // when you use `network-only` for refetch in Error Boundary retry logic, in that case tree doesn't suspend // causing possible "throwed - retry in boundary - throwed - retry in boundary" cycle. // (in case of Jotai URQL bindings only). // eslint-disable-next-line promise/avoid-new new Promise((resolve) => { setTimeout(resolve, 10000) // This timeout time is going to cause suspense of this component up until // new operation result will come. After 10 second it will simply try to render component itself and suspend again // in an endless loop }), ) } if (opResult.error) { throw opResult.error } if (!opResult.data) { throw Error( 'Query data is undefined. Probably you paused the query? In that case use `useQueryAtom` instead.', ) } return [opResult.data, dispatch, opResult] as [ Exclude, typeof dispatch, typeof opResult, ] } // Suspense tree while promise is resolving (not going to be needed in next versions of React) function use(promise: Promise | any) { if (promise.status === 'fulfilled') { return promise.value } if (promise.status === 'rejected') { throw promise.reason } else if (promise.status === 'pending') { throw promise } else { promise.status = 'pending' // eslint-disable-next-line promise/catch-or-return ;(promise as Promise).then( (result: any) => { promise.status = 'fulfilled' promise.value = result }, (reason: any) => { promise.status = 'rejected' promise.reason = reason }, ) throw promise } } ``` #### Basic demo ### Referencing the same instance of the client for both atoms and urql provider To ensure that you reference the same urqlClient object, be sure to wrap the root of your project in a `` and initialise clientAtom with the same urqlClient value you provided to UrqlProvider. Without this step, you may end up specifying client each time when you use `atomWithQuery`. Now you can just ignore the optional `getClient` parameter, and it will use the client from the context. ```jsx import { Suspense } from 'react' import { Provider } from 'jotai/react' import { useHydrateAtoms } from 'jotai/react/utils' import { clientAtom } from 'jotai-urql' import { createClient, cacheExchange, fetchExchange, Provider as UrqlProvider, } from 'urql' const urqlClient = createClient({ url: 'https://countries.trevorblades.com/', exchanges: [cacheExchange, fetchExchange], fetchOptions: () => { return { headers: {} } }, }) const HydrateAtoms = ({ children }) => { useHydrateAtoms([[clientAtom, urqlClient]]) return children } export default function MyApp({ Component, pageProps }) { return ( ) } ``` ================================================ FILE: docs/extensions/valtio.mdx ================================================ --- title: Valtio description: This doc describes Valtio extension. nav: 4.98 keywords: valtio,proxy published: false --- Jotai's state resides in React, but sometimes it would be nice to interact with the world outside React. Valtio provides a proxy interface that can be used to store some values and sync with atoms in Jotai. This only uses the vanilla api of valtio. ### Install You have to install `valtio` and `jotai-valtio` to use this feature. ``` npm install valtio jotai-valtio ``` ## atomWithProxy `atomWithProxy` creates a new atom with valtio proxy. It's two-way binding and you can change the value from both ends. ```jsx import { useAtom } from 'jotai' import { atomWithProxy } from 'jotai-valtio' import { proxy } from 'valtio/vanilla' const proxyState = proxy({ count: 0 }) const stateAtom = atomWithProxy(proxyState) const Counter = () => { const [state, setState] = useAtom(stateAtom) return ( <> count: {state.count} ) } ``` ### Parameters ``` atomWithProxy(proxyObject, options?) ``` **proxyObject** (required): the Valtio proxy object you want to derive the atom from **options.sync** (optional): makes the atom update synchronously instead of waiting for batched updates, similar to [`valtio/useSnapshot`](https://github.com/pmndrs/valtio#update-synchronously). This will result in more renders, but have more guarantees that it syncs with other Jotai atoms. ``` atomWithProxy(proxyObject, { sync: true }) ``` ### Examples ## mutableAtom `mutableAtom` wraps a value in a self-aware Valtio proxy. You can make changes to it in the same way you would to a normal js-object. Count value is stored under the `value` property. ```jsx const countProxyAtom = mutableAtom(0) function IncrementButton() { const countProxy = useAtomValue(countProxyAtom) return } ``` ### Parameters ```js mutableAtom(value, options?) ``` **value** (required): the value to proxy. **options.proxyFn** (optional): allows customization with `proxyFn` for custom proxy functions. Can be `proxy` (default) or a custom function. ### Examples ### Caution on Mutating Proxies Be careful to not mutate the proxy directly in the atom's read function or during render. Doing so could cause an infinite render loop. ```ts const countProxyAtom = mutableAtom(0) atom( (get) => { const countProxy = get(countProxyAtom) ++countProxy.value // This will cause an infinite loop }, (get, set) => { const countProxy = get(countProxyAtom) ++countProxy.value // This is fine }, ) ``` ================================================ FILE: docs/extensions/xstate.mdx ================================================ --- title: XState description: This doc describes XState extension. nav: 4.05 keywords: xstate,machine,atomwithmachine --- Jotai's state management is primitive and flexible, but that sometimes means too free. XState is a sophisticated library to provide a better and safer abstraction for state management. ### Install You have to install `xstate` and `jotai-xstate` to use this feature. ``` npm install xstate jotai-xstate ``` ## atomWithMachine `atomWithMachine` creates a new atom with XState machine. It receives a function `getMachine` to create a new machine. `getMachine` is invoked at the first use with `get` argument, with which you can read other atom values. ```tsx import { useAtom } from 'jotai' import { atomWithMachine } from 'jotai-xstate' import { assign, createMachine } from 'xstate' const createEditableMachine = (value: string) => createMachine<{ value: string }>({ id: 'editable', initial: 'reading', context: { value, }, states: { reading: { on: { dblclick: 'editing', }, }, editing: { on: { cancel: 'reading', commit: { target: 'reading', actions: assign({ value: (_, { value }) => value, }), }, }, }, }, }) const defaultTextAtom = atom('edit me') const editableMachineAtom = atomWithMachine((get) => // `get` is available only for initializing a machine createEditableMachine(get(defaultTextAtom)), ) const Toggle = () => { const [state, send] = useAtom(editableMachineAtom) return (
{state.matches('reading') && ( {state.context.value} )} {state.matches('editing') && ( send({ type: 'commit', value: e.target.value })} onKeyDown={(e) => { if (e.key === 'Enter') { send({ type: 'commit', value: e.target.value }) } if (e.key === 'Escape') { send('cancel') } }} /> )}

Double-click to edit. Blur the input or press enter to commit. Press esc to cancel.
) } ``` ### Restartable machine stored in a global Provider (provider-less mode) When your machine reaches its final state it cannot receive any more events. If your atomWithMachine is initialized in global store (aka provider-less mode), to restart it you need to send a `RESTART` event to your machine like so: ```tsx import { RESTART } from 'jotai-xstate' const YourComponent = () => { const [current, send] = useAtom(yourMachineAtom) const isFinalState = current.matches('myFinalState') useEffect(() => { // restart globally initialized machine on component unmount return () => { if (isFinalState) send(RESTART) } }, [isFinalState]) } ``` ### Examples Check examples with atomWithMachine: Restartable machine: ### Tutorials Check out a course about Jotai and XState. [Complex State Management in React with Jotai and XState](https://egghead.io/courses/complex-state-management-in-react-with-jotai-and-xstate-3be0a740) (Note: In the course, it uses `jotai/xstate` which is supersede by `jotai-xstate`.) ================================================ FILE: docs/extensions/zustand.mdx ================================================ --- title: Zustand description: This doc describes Zustand extension. nav: 4.98 keywords: zustand published: false --- Jotai's state resides in React, but sometimes it would be nice to interact with the world outside React. Zustand provides a store interface that can be used to hold some values and sync with atoms in Jotai. This only uses the vanilla api of zustand. ### Install You have to install `zustand` and `jotai-zustand` to use this feature. ``` npm install zustand jotai-zustand ``` ## atomWithStore `atomWithStore` creates a new atom with zustand store. It's two-way binding and you can change the value from both ends. ```jsx import { useAtom } from 'jotai' import { atomWithStore } from 'jotai-zustand' import create from 'zustand/vanilla' const store = create(() => ({ count: 0 })) const stateAtom = atomWithStore(store) const Counter = () => { const [state, setState] = useAtom(stateAtom) return ( <> count: {state.count} ) } ``` ### Examples ================================================ FILE: docs/guides/async.mdx ================================================ --- title: Async description: This doc describes about the behavior with async. nav: 8.99 keywords: async published: false --- Using async atoms, you gain access to real-world data while still managing them directly from your atoms and with incredible ease. We can separate them in two main categories: - Async read atoms: async request is started instantly as soon as you try to get its value. You could relate to them as "smart getters". - Async write atoms: async request is started at a specific moment. You could relate to them as "actions". ## Async read atom The `read` function of an atom can return a promise. ```js const countAtom = atom(1) const asyncAtom = atom(async (get) => get(countAtom) * 2) ``` Jotai is inherently leveraging `Suspense` to handle asynchronous flows. ```jsx const ComponentUsingAsyncAtoms = () => { const [num] = useAtom(asyncAtom) // here `num` is always `number` even though asyncAtom returns a Promise } const App = () => { return ( ) } ``` Alternatively, you could avoid the inherent suspending that Jotai does for you, by wrapping your atoms with the [`loadable` API](../utilities/async.mdx). If another atom uses an async atom, it will return a promise. So, we need to make the atom also async. ```js const anotherAtom = atom(async (get) => (await get(asyncAtom)) / 2) ``` This also applies to an atom with write function. ```js const asyncAtom = atom(async (get) => ...) const writeAtom = atom(null, async (get, set, payload) => { await get(asyncAtom) // ... }) ``` ## Async write atom Async write atoms are another kind of async atom. When the `write` function of atom returns a promise. ```js const countAtom = atom(1) const asyncIncrementAtom = atom(null, async (get, set) => { // await something set(countAtom, get(countAtom) + 1) }) const Component = () => { const [, increment] = useAtom(asyncIncrementAtom) const handleClick = () => { increment() } // ... } ``` ## Async sometimes An interesting pattern that can be achieved with Jotai is switching from async to sync to trigger suspending when wanted. ```js const request = async () => fetch('https://jsonplaceholder.typicode.com/todos/1').then((res) => res.json(), ) const baseAtom = atom(0) const Component = () => { const [value, setValue] = useAtom(baseAtom) const handleClick = () => { setValue(request()) // Will suspend until request resolves } // ... } ``` ### Usage in TypeScript In TypeScript `atom(0)` is inferred as `PrimitiveAtom`. It cannot accept `Promise` as a value so preceding code would not typecheck. To accommodate for that you need to type your atom explicitly and add `Promise` as accepted value. ```ts const baseAtom = atom>(0) // Will accept sync and async values ``` ## Async forever Sometimes you may want to suspend until an unpredetermined moment (or never). ```js const baseAtom = atom(new Promise(() => {})) // Will be suspend until set otherwise ``` ## Suspense Async support is first class in Jotai. It fully leverages React Suspense at its core. > Technically, Suspense usage other than React.lazy is still unsupported / undocumented in React 17. If this is blocking, so you can still use the [`loadable` API](../utilities/async.mdx) to avoid suspending To use async atoms, you need to wrap your component tree with ``. > If you have a ``, place **at least one** `` inside said ``; otherwise, it may cause an endless loop while rendering the components. ```jsx const App = () => ( ) ``` Having more ``s in the component tree is also possible and must be considered to profit from Jotai inherent handling at best. ================================================ FILE: docs/guides/atoms-in-atom.mdx ================================================ --- title: Atoms in atom nav: 8.12 --- `atom()` creates an atom config, which is an object, but it doesn't hold a value. Atom configs don't have string keys and we identify them with referential equality. In other words, we can use an atom config like a key. ### Storing an atom config in useState First things first, we can store an atom config in useState. ```jsx const Component = ({ atom1, atom2 }) => { const [selectedAtom, setSelectedAtom] = useState(atom1) const [value] = useAtom(selectedAtom) return (
Selected value: {value}
) } ``` Note that we can pass atoms configs as props. It might not make any sense, but we could create an atom config on demand. ```jsx const Component = () => { const [currentAtom, setCurrentAtom] = useState(() => atom(0)) const [count, setCount] = useAtom(currentAtom) return (
Count: {count}
) } ``` ### Storing an atom config in atom Likewise, we can store an atom config as a value of another atom. ```jsx const firstNameAtom = atom('Tanjiro') const lastNameAtom = atom('Kamado') const showingNameAtom = atom(firstNameAtom) const Component = () => { const [nameAtom, setNameAtom] = useAtom(showingNameAtom) const [name] = useAtom(nameAtom) return (
Name: {name}
) } ``` It's possible to create a derived atom. ```js const derivedNameAtom = atom((get) => { const nameAtom = get(showingNameAtom) return get(nameAtom) }) // Or a shorter version const derivedNameAtom = atom((get) => get(get(showingNameAtom))) ``` To avoid confusing what is in atoms, naming atoms explicitly would be important. Also, TypeScript type information would be helpful. ### Storing an array of atom configs in atom Finally, the atoms in atom pattern is to store an array of atom config into an atom. ```jsx const countsAtom = atom([atom(1), atom(2), atom(3)]) const Counter = ({ countAtom }) => { const [count, setCount] = useAtom(countAtom) return (
{count}
) } const Parent = () => { const [counts, setCounts] = useAtom(countsAtom) const addNewCount = () => { const newAtom = atom(0) setCounts((prev) => [...prev, newAtom]) } return (
{counts.map((countAtom) => ( ))}
) } ``` The benefit of this approach is, if you increment a count, only the corresponding Counter component re-renders and no other components re-render. It is important to note that `anAtom.toString()` returns a unique id, which can be used as a `key` in a map. #### Hint for TypeScript users ```jsx ``` ### Storing a map of atom configs in atom Likewise, we can store an object map instead of an array. ```jsx const pricesAtom = atom({ apple: atom(15), orange: atom(12), pineapple: atom(25), }) const Fruit = ({ name, priceAtom }) => { const [price] = useAtom(priceAtom) return (
{name}: {price}
) } const Parent = () => { const [prices] = useAtom(pricesAtom) return (
{Object.keys(prices).map((name) => ( ))}
) } ``` ================================================ FILE: docs/guides/composing-atoms.mdx ================================================ --- title: Composing atoms nav: 8.11 --- The `atom` function provided by library is very primitive, but it's also so flexible that you can combine multiple atoms to implement a functionality. > Note again that `atom()` creates an atom config, which is an object > to define a behavior of the atom. Let's recap how we can derive an atom. ### Basic derived atoms Here's one of the simplest examples of a derived atom: ```js export const textAtom = atom('hello') export const textLenAtom = atom((get) => get(textAtom).length) ``` The `textLenAtom` is called read-only atom, because it doesn't have a `write` function defined. The following is another simple example with the `write` function: ```js const textAtom = atom('hello') export const textUpperCaseAtom = atom( (get) => get(textAtom).toUpperCase(), (_get, set, newText) => set(textAtom, newText), ) ``` In this case, `textUpperCaseAtom` is capable to set the original `textAtom`. So, we can only export `textUpperCaseAtom` and can hide `textAtom` in a smaller scope. Now, let's see some real examples. ### Overriding default atom values Suppose we have a read-only atom. Obviously read-only atoms are not writable, but we can combine two atoms to override the read-only atom value. ```js const rawNumberAtom = atom(10.1) // can be exported const roundNumberAtom = atom((get) => Math.round(get(rawNumberAtom))) const overwrittenAtom = atom(null) export const numberAtom = atom( (get) => get(overwrittenAtom) ?? get(roundNumberAtom), (get, set, newValue) => { const nextValue = typeof newValue === 'function' ? newValue(get(numberAtom)) : newValue set(overwrittenAtom, nextValue) }, ) ``` The final `numberAtom` just works like a normal primitive atom like `atom(10)`. If you set a number value, it will override the `overwrittenAtom` value, and if you set `null`, it will be the `roundNumberAtom` value. The reusable implementation is available as `atomWithDefault` in `jotai/utils`. See [atomWithDefault](../utilities/resettable.mdx). Next, let's see another example to sync with external value. ### Syncing atom values with external values There are some external values we want to deal with. `localStorage` is the one. Another is `window.title`. Let's see how to create an atom that is in sync with `localStorage`. ```js const baseAtom = atom(localStorage.getItem('mykey') || '') export const persistedAtom = atom( (get) => get(baseAtom), (get, set, newValue) => { const nextValue = typeof newValue === 'function' ? newValue(get(baseAtom)) : newValue set(baseAtom, nextValue) localStorage.setItem('mykey', nextValue) }, ) ``` The `persistedAtom` works like a primitive atom, but its value is persisted in `localStorage`. The reusable implementation is available as `atomWithStorage` in `jotai/utils`. See [atomWithStorage](../utilities/storage.mdx). There is a caveat with this usage. While atom config doesn't hold a value, the external value is a singleton value. So, if we use this atom in two different Providers, There will be an inconsistency between the two `persistedAtom` values. This could be solved if the external value had a subscription mechanism. For example, `atomWithProxy` in `jotai-valtio` comes with subscription, so we don't have such a limitation. Values in different Providers will be in sync. Back to the main topic, let's explore another example. ### Extending atoms with `atomWith*` utils We have several utils whose names start with `atomWith`. They create an atom with a certain functionality. Unfortunately, we can't combine two atom utils. For example, `atomWithStorage` and `atomWithReducer` can't be used to define a single atom. In such a case, we need to derive an atom by ourselves. Let's try adding reducer functionality to `atomWithStorage`: ```js const reducer = ... const baseAtom = atomWithStorage('mykey', '') export const derivedAtom = atom( (get) => get(baseAtom), (get, set, action) => { set(baseAtom, reducer(get(baseAtom), action)) } ) ``` This is easy, because in this case, `atomWithReducer` is a simple implementation compared to `atomWithStorage`. For more complex cases, it wouldn't be very easy. It would still be a open research field. Finally, let's see another example with actions. ### Action atoms This should be known pattern as it's described in README. Nonetheless, it might to be useful to revisit. Let's create a counter that you can only increment or decrement by one. One solution is `atomWithReducer`: ```js const countAtom = atomWithReducer(0, (prev, action) => { if (action === 'INC') { return prev + 1 } if (action === 'DEC') { return prev - 1 } throw new Error('unknown action') }) ``` This is fine, but not very atomic. If we want to get benefit from code splitting / lazy loading, We want to create write only atoms, or action atoms. ```js const baseAtom = atom(0) // do not export export const countAtom = atom((get) => get(baseAtom)) // read only export const incAtom = atom(null, (_get, set) => { set(baseAtom, (prev) => prev + 1) }) export const decAtom = atom(null, (_get, set) => { set(baseAtom, (prev) => prev - 1) }) ``` This is more atomic and looks like a Jotai way. You can also create an action atom that will call another action atom: ```js // continued from the previous code export const dispatchAtom = atom(null, (_get, set, action) => { if (action === 'INC') { set(incAtom) } else if (action === 'DEC') { set(decAtom) } else { throw new Error('unknown action') } }) ``` Why do we want it? Because it will be used only when needed. It allows code splitting and dead code elimination. ### In summary Atoms are building block. By composing atoms based on other atoms, we can implement complicated logic. This is not only for read derived atoms, but also for write action atoms. Essentially, atoms are like functions, so composing atoms is like composing functions with other functions. **Note**: We mentioned that our atoms can contain any kind of data, it can be a string, Blob, Observer, anything really. There is just one exception. Because derived atoms are defined using a function, Jotai will not understand if we pass it a function that isn't exactly a pure getter. So what you can do is simply wrap your function in an object. ```js const doublerAtom = atom({ callback: (n) => n * 2 }) // Usage const [doubler] = useAtom(doublerAtom) const doubledValue = doubler.callback(50) // Will compute to 100 ``` ================================================ FILE: docs/guides/core-internals.mdx ================================================ --- title: Core internals description: A simplified version of the core implementation nav: 8.10 --- This guide is beneficial for those who want to understand the core implementation of Jotai. It's not meant to be a complete example of the core implementation but rather a simplified version. It's inspired by the collection of tweets by Daishi Kato([@dai_shi](https://twitter.com/dai_shi)). ### First Version Let's start with an easy example. An atom is just a function that will return a configuration object. We are using WeakMap to map atom with their state. WeakMap doesn't keep its keys in memory, so if an atom is garbage collected, its state will be garbage collected too. This helps avoid memory leaks. ```js import { useState, useEffect } from 'react' // atom function returns a config object which contains initial value export const atom = (initialValue) => ({ init: initialValue }) // we need to keep track of the state of the atom. // we are using weakmap to avoid memory leaks const atomStateMap = new WeakMap() const getAtomState = (atom) => { let atomState = atomStateMap.get(atom) if (!atomState) { atomState = { value: atom.init, listeners: new Set() } atomStateMap.set(atom, atomState) } return atomState } // useAtom hook returns a tuple of the current value // and a function to update the atom's value export const useAtom = (atom) => { const atomState = getAtomState(atom) const [value, setValue] = useState(atomState.value) useEffect(() => { const callback = () => setValue(atomState.value) // same atom can be used at multiple components, so we need to // keep listening for atom's state change till component is unmounted. atomState.listeners.add(callback) callback() return () => atomState.listeners.delete(callback) }, [atomState]) const setAtom = (nextValue) => { atomState.value = nextValue // let all the subscribed components know that the atom's state has changed atomState.listeners.forEach((l) => l()) } return [value, setAtom] } ``` Here's an example using our simplified atom implementation. [Counter example](https://codesandbox.io/s/zealous-field-z2xk6?file=/src/App.js) Ref tweet: [Demystifying the internal of jotai](https://twitter.com/dai_shi/status/1484835169475653634) ### Second Version Hang on! We can do better. In Jotai, we can create derived atom. A derived atom is an atom that depends on other atoms. ```js const priceAtom = atom(10) const readOnlyAtom = atom((get) => get(priceAtom) * 2) const writeOnlyAtom = atom( null, // it's a convention to pass `null` for the first argument (get, set, args) => { set(priceAtom, get(priceAtom) - args) }, ) const readWriteAtom = atom( (get) => get(priceAtom) * 2, (get, set, newPrice) => { set(priceAtom, newPrice / 2) // you can set as many atoms as you want at the same time }, ) ``` To keep track of all the dependents, we need to add one more property to the atom's state. Let's say atom X depends on atom Y, so when we update atom Y, we also update atom X. This is called dependency tracking. ```js const atomState = { value: atom.init, listeners: new Set(), dependents: new Set(), } ``` We now need to create functions for reading and writing an atom that can handle updating dependent atoms' state. ```js import { useState, useEffect } from 'react' export const atom = (read, write) => { if (typeof read === 'function') { return { read, write } } const config = { init: read, // get in the read function is to read the atom value. // It's reactive and read dependencies are tracked. read: (get) => get(config), // get in the write function is also to read atom value, but it's not tracked. // set in the write function is to write atom value and // it will invoke the write function of the target atom. write: write || ((get, set, arg) => { if (typeof arg === 'function') { set(config, arg(get(config))) } else { set(config, arg) } }), } return config } // same as above but the state has one extra property: dependents const atomStateMap = new WeakMap() const getAtomState = (atom) => { let atomState = atomStateMap.get(atom) if (!atomState) { atomState = { value: atom.init, listeners: new Set(), dependents: new Set(), } atomStateMap.set(atom, atomState) } return atomState } // If atom is primitive, we return it's value. // If atom is derived, we read the parent atom's value // and add current atom to parent's the dependent set (recursively). const readAtom = (atom) => { const atomState = getAtomState(atom) const get = (a) => { if (a === atom) { return atomState.value } const aState = getAtomState(a) aState.dependents.add(atom) // XXX add only return readAtom(a) // XXX no caching } const value = atom.read(get) atomState.value = value return value } // if atomState is modified, we need to notify all the dependent atoms (recursively) // now run callbacks for all the components that are dependent on this atom const notify = (atom) => { const atomState = getAtomState(atom) atomState.dependents.forEach((d) => { if (d !== atom) notify(d) }) atomState.listeners.forEach((l) => l()) } // writeAtom calls atom.write with the necessary params and triggers notify function const writeAtom = (atom, value) => { const atomState = getAtomState(atom) // 'a' is some atom from atomStateMap const get = (a) => { const aState = getAtomState(a) return aState.value } // if 'a' is the same as atom, update the value, notify that atom and return // else calls writeAtom for 'a' (recursively) const set = (a, v) => { if (a === atom) { atomState.value = v notify(atom) return } writeAtom(a, v) } atom.write(get, set, value) } export const useAtom = (atom) => { const [value, setValue] = useState() useEffect(() => { const callback = () => setValue(readAtom(atom)) const atomState = getAtomState(atom) atomState.listeners.add(callback) callback() return () => atomState.listeners.delete(callback) }, [atom]) const setAtom = (nextValue) => { writeAtom(atom, nextValue) } return [value, setAtom] } ``` Here is an example using our derived atom implementation. [Derived counter example](https://codesandbox.io/s/affectionate-chandrasekhar-nuxms?file=/src/App.js) Ref tweet: [Supporting derived atoms](https://twitter.com/dai_shi/status/1485434083778117632) ================================================ FILE: docs/guides/debugging.mdx ================================================ --- title: Debugging nav: 8.07 keywords: debug,labels,devtools,freeze --- In basic apps, `console.log` can be our best friend for debugging atoms, but when applications get bigger and we have more atoms to use, logging would not be a good way of debugging atoms. Jotai provides two ways of debugging atoms, **React Dev Tools** and **Redux Dev tools**. For reading values and simple debugging, React Dev Tools might suit you, but for more complicated tasks like Time-travelling and setting values, Redux Dev Tools would be a better option. ## Debug labels It is worth mentioning that we have a concept called **Debug labels** in Jotai which may help us with debugging. By default each Jotai state has the label like `1:` with number being internal `key` assigned to each atom automatically. But you can add labels to atoms to help you distinguish them more easily with `debugLabel`. ```js const countAtom = atom(0) // countAtom's debugLabel by default is 'atom1' if (process.env.NODE_ENV !== 'production') { countAtom.debugLabel = 'count' // debugLabel is 'count' now } ``` Jotai provides both a Babel and a SWC plugin, that adds a debugLabel automatically to every atom, which makes things easier for us. For more info, check out [jotai-babel](https://github.com/jotaijs/jotai-babel) and [@swc-jotai/debug-label](https://github.com/pmndrs/jotai/blob/main/docs/tools/swc.mdx) ## Using React Dev Tools You can use [React Dev Tools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi) to inspect Jotai state. To achieve that [useDebugValue](https://react.dev/reference/react/useDebugValue) is used inside custom hooks. Keep in mind that it only works in dev mode (such as `NODE_ENV === 'development'`). ### useAtom `useAtom` calls `useDebugValue` for atom values, so if you select the component that consumes Jotai atoms in React Dev Tools, you would see "Atom" hooks for each atom that is used in the component along with the value it has right now. ### useAtomsDebugValue `useAtomsDebugValue` catches all atoms in a component tree under Provider (or an entire tree for Provider-less mode), and `useDebugValue` for all atoms values. If you navigate to the component that has `useAtomsDebugValue` in the React Dev Tools, we can see a custom hook "AtomsDebugValue" which allows you to see all atom values and their dependents. One use case is to put the hook just under the `Provider` component: ```jsx const DebugAtoms = () => { useAtomsDebugValue() return null } const Root = () => ( ) ``` ## Using Redux DevTools You can also use [Redux DevTools](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd) to inspect atoms, with many features like Time-travelling and value dispatching. ### [useAtomDevtools](https://jotai.org/docs/api/devtools#use-atom-devtools) > `useAtomDevtools` is a React hook that manages ReduxDevTools extension for a particular atom. If you have a specific atom in mind that you may want to debug, `useAtomDevtools` can be a good option. ```js const countAtom = atom(0) // setting countAtom.debugLabel is recommended if we have more atoms function Counter() { const [count, setCount] = useAtom(countAtom) useAtomDevtools(countAtom) } ``` Now if we try `setCount`, we can see that the Redux Dev Tools logs those changes immediately. ![](https://lh3.googleusercontent.com/pw/AP1GczMgeYQOyCAc69gJIAcRBtKO9BOUEE3SQB5Bl-7IScJfChWGnVb3B0OmlhrjK8caQVnj-HtyN1cpv1l1K9kE4pxwapUwu_2OB-dO_G18ZUC1NbDJFiXYRW9jX8OeDBJeWg1Qx9_IdkfoaoIin90A8gSE=w828-h268-s-no-gm) ![]() #### Time travel Sometimes we need to switch to a specific value of our atoms' state, with Time travelling this is possible. You can hover on each action you see in the devtools and see the **Jump** option there, with clicking it you'd be able to switch to that specific value. #### Pause If we don't record changes on atoms, we can stop watching those using the **Pausing** feature. ![](https://lh3.googleusercontent.com/pw/AP1GczP8hTBFtwlx0BJGGbbcXgfhMNG2Vz_uozdVnrTJHwMb1gKx55TP59WgvsMwgIyExwscgYZSpYDmxCJXjk_pKy6wP-K-0p287lkRXdTZEf074xUZr8fnIpkwg-zN14VXZ2STet1sVgTTawm49mc8Oygb=w395-h87-s-no-gm) #### Dispatch It's possible to set values on atoms with the **Dispatch** feature. You can do that by clicking on the **Show Dispatcher** button. ![](https://lh3.googleusercontent.com/pw/AP1GczMNn6aXTA7K8ZFzUj17I40cm0o7joOG6E76Q6UVnXYJ3TO7ItRI6Jr1EIxogfY9P2xkiQfyYqB7_aU--R_vdSyNXAtTfPuxxLymApRoZov0-6ZHS7mmxxxD4Ku1JnqTRyPyZaQHyQPkq8j4CciQaISV=w832-h149-s-no-gm) This would set the `countAtoms`'s value to `5`. > We should note that the value will be parsed by JSON.parse, so pass supported values. ### [useAtomsDevtools](../tools/devtools.mdx) > `useAtomsDevtools` is a catch-all version of `useAtomDevtools` where it shows all atoms in the store instead of showing a specific one. We'd recommend this hook if you want to keep track of all of your atoms in one place. It means every action on every atom that is placed in the bottom of this hook (in the React tree) will be caught by the Redux Dev Tools. Every feature of `useAtomDevtools` is supported in this hook, but there's an extra feature, which includes giving more information about atoms dependents like: ```json { "values": { "atom1:count": 0, "atom2:doubleCount": 0, "atom3:half": 0, "atom4:sum": 0 }, "dependents": { "atom1:count": ["atom1:count", "atom2:doubleCount", "atom4:sum"], "atom2:doubleCount": ["atom3:half", "atom4:sum"], "atom3:half": ["atom4:sum"], "atom4:sum": [] } } ``` ## Frozen Atoms To find bugs where you accidentally tried to mutate objects stored in atoms you could use `freezeAtom` or `freezeAtomCreator`from `jotai/utils` bundle. Which returns atoms value that is deeply freezed with `Object.freeze`. ### freezeAtom ```ts freezeAtom(anAtom): AtomType ``` `freezeAtom` takes an existing atom and make it "frozen". It returns the same atom. The atom value will be deeply frozen by `Object.freeze`. It is useful to find bugs where you unintentionally tried to change objects (states) which can lead to unexpected behavior. You may use `freezeAtom` with all atoms to prevent this situation. #### Parameters **anAtom** (required): An atom you wish to freeze. #### Examples ```js import { atom } from 'jotai' import { freezeAtom } from 'jotai/utils' const objAtom = freezeAtom(atom({ count: 0 })) ``` ### freezeAtomCreator If you need, you can define a factory for `freezeAtom`. ```ts import { freezeAtom } from 'jotai/utils' export function freezeAtomCreator< CreateAtom extends (...args: unknown[]) => Atom, >(createAtom: CreateAtom): CreateAtom { return ((...args: unknown[]) => freezeAtom(createAtom(...args))) as never } ``` ================================================ FILE: docs/guides/initialize-atom-on-render.mdx ================================================ --- title: Initializing state on render description: How to initialize atom state on initial render nav: 8.13 --- There are times when you need to create an reusable component which uses atoms. These atoms' initial state are determined by the props passed to the component. Below is a basic example illustrating how you can use `Provider` and its prop, `initialValues`, to initialize state. ### Basic Example > CodeSandbox link: [codesandbox](https://codesandbox.io/s/init-atoms-with-usehydrateatoms-nryk1w). Consider a basic example where you have a reusable `TextDisplay` component that allows you to display and update plain text. This component has two child components, `PrettyText` and `UpdateTextInput`. - `PrettyText` displays the text in blue. - `UpdateTextInput` is an input field which updates the text value. As opposed to passing `text` as a prop in the two child components, you decided that the `text` state should be shared between components as an atom. To make `TextDisplay` component reusable, we take in a prop `initialTextValue`, which determines the initial state of the `text` atom. To tie `initialTextValue` to `textAtom`, we wrap the child components in a component where we create a new store and pass it to a `Provider` component. ```jsx const textAtom = atom('') const PrettyText = () => { const [text] = useAtom(textAtom) return ( <> {text} ) } const UpdateTextInput = () => { const [text, setText] = useAtom(textAtom) const handleInputChange = (e) => { setText(e.target.value) } return ( <> ) } const HydrateAtoms = ({ initialValues, children }) => { // initialising on state with prop on render here useHydrateAtoms(initialValues) return children } export const TextDisplay = ({ initialTextValue }) => (
) ``` Now, we can easily reuse `TextDisplay` component with different initial text values despite them referencing the "same" atom. ```jsx export default function App() { return (
) } ``` This behavior is due to our child components looking for the lowest common `Provider` ancestor to derive its value. For more information on `Provider` behavior, please read the docs [here](../core/provider.mdx). For more complex use cases, check out [Scope extension](../extensions/scope.mdx). ### Using Typescript `useHydrateAtoms` has overloaded types and typescript cannot extract types from overloaded function. It is recommended to use a `Map` when passing initial atom values to the `useHydrateAtoms`. Here is a working example: ```tsx import type { ReactNode } from 'react' import { Provider, atom, useAtomValue } from 'jotai' import type { WritableAtom } from 'jotai' import { useHydrateAtoms } from 'jotai/utils' const testAtom = atom('') export default function App() { return ( ) } //This component contains all the states and the logic function Component() { const testAtomValue = useAtomValue(testAtom) return
{testAtomValue}
} function AtomsHydrator({ atomValues, children, }: { // eslint-disable-next-line @typescript-eslint/no-explicit-any atomValues: Iterable< readonly [WritableAtom, unknown] > children: ReactNode }) { useHydrateAtoms(new Map(atomValues)) return children } ``` ================================================ FILE: docs/guides/migrating-to-v2-api.mdx ================================================ --- title: v2 API migration description: New "Async" API nav: 8.0 --- RFC: https://github.com/pmndrs/jotai/discussions/1514 Jotai v1 is released at June 2022, and there has been various feedbacks. React also proposes first-class support for promises. Jotai v2 will have a new API. Unfortunately, there are some breaking changes along with new features. ### What are new features #### Vanilla library Jotai comes with vanilla (non-React) functions and React functions separately. They are provided from alternate entry points like `jotai/vanilla`. #### Store API Jotai exposes store interface so that you can directly manipulate atom values. ```js import { createStore } from 'jotai' // or from 'jotai/vanilla' const store = createStore() store.set(fooAtom, 'foo') console.log(store.get(fooAtom)) // prints "foo" const unsub = store.sub(fooAtom, () => { console.log('fooAtom value in store is changed') }) // call unsub() to unsubscribe. ``` You can also create your own React Context to pass a store. #### More flexible atom `write` function The write function can accept multiple arguments, and return a value. ```js atom( (get) => get(...), (get, set, arg1, arg2, ...) => { ... return someValue } ) ``` ### What are breaking #### Async atoms are no longer special Async atoms are just normal atoms with promise values. Atoms getter functions don't resolve promises. On the other hand, `useAtom` hook continues to resolve promises. Some utils like `splitAtom` expects sync atoms, and won't work with async atoms. #### Writable atom type is changed (TypeScript only) ```ts // Old WritableAtom> // New WritableAtom ``` In general, we should avoid using `WritableAtom` type directly. #### Some functions are dropped - Provider's `initialValues` prop is removed, because `store` is more flexible. - Provider's scope props is removed, because you can create own context. - `abortableAtom` util is removed, because the feature is included by default - `waitForAll` util is removed, because `Promise.all` just works ### Migration guides #### Async atoms `get` function for read function of async atoms doesn't resolve promises, so you have to put `await` or `.then()`. In short, the change is something like the following. (If you are TypeScript users, types will tell where to changes.) ##### Previous API ```js const asyncAtom = atom(async () => 'hello') const derivedAtom = atom((get) => get(asyncAtom).toUppercase()) ``` ##### New API ```js const asyncAtom = atom(async () => 'hello') const derivedAtom = atom(async (get) => (await get(asyncAtom)).toUppercase()) // or const derivedAtom = atom((get) => get(asyncAtom).then((x) => x.toUppercase())) ``` #### Provider's `initialValues` prop ##### Previous API ```jsx const countAtom = atom(0) // in component ... ``` ##### New API ```jsx const countAtom = atom(0) const HydrateAtoms = ({ initialValues, children }) => { useHydrateAtoms(initialValues) return children } // in component ... ``` #### Provider's `scope` prop ##### Previous API ```jsx const myScope = Symbol() // Parent component ... // Child component useAtom(..., myScope) ``` ##### New API ```jsx const MyContext = createContext() const store = createStore() // Parent component ... // Child Component const store = useContext(MyContext) useAtom(..., { store }) ``` #### `abortableAtom` util You no longer need the previous `abortableAtom` util, because it's now supported with the normal `atom`. ##### Previous API ```js const asyncAtom = abortableAtom(async (get, { signal }) => { ... } ``` ##### New API ```js const asyncAtom = atom(async (get, { signal }) => { ... } ``` #### `waitForAll` util You no longer need the previous `waitForAll` util, because we can use native Promise APIs. ##### Previous API ```js const allAtom = waitForAll([fooAtom, barAtom]) ``` ##### New API ```js const allAtom = atom((get) => Promise.all([get(fooAtom), get(barAtom)])) ``` Note that creating an atom in render function can cause [infinite loop](../core/atom.mdx#note-about-creating-an-atom-in-render-function) #### `splitAtom` util (or some other utils) with async atoms `splitAtom` util only accepts sync atoms. You need to unwrap async atoms before passing. This applies to some other utils like `atomsWithQuery` from `jotai-tanstack-query`. ##### Previous API ```js const splittedAtom = splitAtom(asyncArrayAtom) ``` ##### New API ```js const splittedAtom = splitAtom(unwrap(asyncArrayAtom, () => [])) ``` As of writing, `unwrap` is unstable and not documented. You can instead use `loadable`, which gives more control on loading status. If you need to use ``, atoms-in-atom pattern would help. For more information, refer the following discussions: - https://github.com/pmndrs/jotai/discussions/1615 - https://github.com/jotaijs/jotai-tanstack-query/issues/21 - https://github.com/pmndrs/jotai/discussions/1751 ### Some other changes #### Utils - `atomWithStorage` util's `delayInit` is removed as being default. Also it will always render `initialValue` on first render, and the stored value, if any, on subsequent renders. The new behavior differs from v1. See https://github.com/pmndrs/jotai/discussions/1737 for more information. - `useHydrateAtoms` can only accept writable atoms. #### Import statements The v2 API is also provided from alternate entry points for library authors and non-React users. - `jotai/vanilla` - `jotai/vanilla/utils` - `jotai/react` - `jotai/react/utils` ```js // Available since v1.11.0 import { atom } from 'jotai/vanilla' import { useAtom } from 'jotai/react' // Available since v2.0.0 import { atom } from 'jotai' // is same as 'jotai/vanilla' import { useAtom } from 'jotai' // is same as 'jotai/react' ``` Note: If you are not using ESM, you want to prefer using `jotai/vanilla` etc. instead of `jotai`, for better tree shaking. ================================================ FILE: docs/guides/nextjs.mdx ================================================ --- title: Next.js description: How to use Jotai with Next.js nav: 8.03 keywords: next,nextjs --- ### Hydration Jotai has support for hydration of atoms with `useHydrateAtoms`. The documentation for the hook can be seen [here](../utilities/ssr.mdx). ### Sync with router It's possible to sync Jotai with the router. You can achieve this with `atomWithHash`: ```js const pageAtom = atomWithHash('page', 1, { replaceState: true, subscribe: (callback) => { Router.events.on('routeChangeComplete', callback) window.addEventListener('hashchange', callback) return () => { Router.events.off('routeChangeComplete', callback) window.removeEventListener('hashchange', callback) } }, }) ``` This way you have full control over what [router event](https://nextjs.org/docs/api-reference/next/router#routerevents) you want to subscribe to. > #### In Next.js 13 > > As of Next.js 13 there have been some changes to the `Router.events.on()` which no longer expose events. There are plans in the [App Router Roadmap](https://beta.nextjs.org/docs/app-directory-roadmap#planned-features) for event intercepting and hash handling. However there is no ETA on when this will be available or what it will look like. For now when trying to the `atomWithHash()` you will not get the atom loading with any data when navigating using the router, only when the page is reloaded or the component is rerendered. It is also recommended that you set the `setHash` option to `replaceState` as Next.js appears to use window.history in the background and this will allow the user to use the browser back button. ### You can't return promises in server side rendering It's important to note that you can't return promises with SSR - However, it's possible to guard against it inside the atom definition. If possible use `useHydrateAtoms` to hydrate values from the server. ```js const postData = atom((get) => { const id = get(postId) if (isSSR || prefetchedPostData[id]) { return prefetchedPostData[id] || EMPTY_POST_DATA } return fetchData(id) // returns a promise }) ``` ### Provider By default, Jotai uses an implicit global store to keep track of atom values. This is what is referred to as "provider-less" mode. This becomes an issue in SSR scenario because this global store is kept alive and is shared between multiple requests, which can lead to bugs and security risks. To limit the lifetime of the store to the scope of one request, you need to use a [Provider](../core/provider.mdx) at the root of your app (or a subtree if you're using Jotai only for a part of your application). ```typescript import { Provider } from 'jotai' function App({ Component, pageProps }: AppProps) { return ( ) } ``` In this case: 1. `Provider` will hold the state of the atoms used in its subtree instead of the global store. 2. `Provider`'s lifetime will be the same as the app itself, and since the app is recreated on each SSR request we essentially limit the lifetime of the store to a single request as well. ### SWC plugins Jotai provides SWC plugins for better DX while developing with Next.js. [Find more info in the SWC section.](../tools/swc.mdx) ### Examples #### Clock #### HN Posts Page Router demo: App Router [demo on Stackblitz](https://stackblitz.com/edit/jotai-nextjs-app-router-demo?file=store%2Findex.ts,app%2Flayout.tsx,components%2FPost.tsx,app%2Fpage.tsx) #### Next.js repo ```bash npx create-next-app --example with-jotai with-jotai-app ``` Here's a [link](https://github.com/vercel/next.js/tree/canary/examples/with-jotai). ================================================ FILE: docs/guides/performance.mdx ================================================ --- title: Performance description: How to limit extra re-renders nav: 8.08 keywords: performance --- **Note**: This guide has room for improvement. Consider it as FYI for now. Jotai & React gives us quite a few tools to manage the re-renders that happen in the app lifecycle. First, please read about the difference [between render & commit](https://legacy.reactjs.org/blog/2018/09/10/introducing-the-react-profiler.html#browsing-commits), because that's very important to understand before going further. ### Cheap renders As seen in the [core section](../core/atom.mdx), due to React 18 default behaviour (but overall good practice), you have to make sure your component functions are _idempotent_. They will be called multiple times during the render phase, even at mount. So we need to keep our renders cheap at all cost! #### Heavy computation Always make heavy computation outside of the React lifecycle (in actions for example) Dont's: ```js // Heavy computation for each item const selector = (s) => s.filter(heavyComputation) const Profile = () => { const [computed] = useAtom(selectAtom(friendsAtom, selector)) } ``` Do's: ```js const friendsAtom = atom([]) const fetchFriendsAtom = atom(null, async (get, set, payload) => { // Fetch all friends const res = await fetch('https://jsonplaceholder.typicode.com/users') const data = await res.json() // Make heavy computation once only const computed = data.filter(heavyComputation) set(friendsAtom, computed) }) // Usage in components const Profile = () => { const [friends] = useAtom(friendsAtom) } ``` #### Small components Observed atoms should only re-render small parts of your application that required an update. The less comparison React has to make, the shorter your render time will be. Dont's: ```jsx const Profile = () => { const [name] = useAtom(nameAtom) const [age] = useAtom(ageAtom) return ( <>
{name}
{age}
) } ``` Do's: ```jsx const NameComponent = () => { const [name] = useAtom(nameAtom) return
{name}
} const AgeComponent = () => { const [age] = useAtom(ageAtom) return
{age}
} const Profile = () => { return ( <> ) } ``` ### Render on demand Usually, the main performance overhead will come from re-rendering parts of your app that did not need to, or way more than they should. We have a few tools to deal with "when" React should render our components. If you have not seen the usage of `useMemo` and `useCallback`, please check the official React documentation for more info before going further. They are of great use to reduce un-necessary renders where your app is not fluid. But Jotai also provides its set of tools to handle the "when" our atoms should trigger a re-render. - Out of the box, Jotai encourages you to split your data into atomic parts, hence each atom is stored separately and will only trigger a re-render when their own value change - `selectAtom` allows you to subscribe to specific part of a large object and only re-render on value change - `focusAtom` same as selectAtom, but creating a new atom for the part, giving a setter to update that specific part easily - `splitAtom` does the work of selectAtom/focusAtom for a dynamic list While this seems simplistic, it is simple to reason about. That was the goal, let's keep it simple to keep it fast. #### Frequent or rare updates Ask yourself whether your atom is usually going to be frequently update or more rarely. Let's imagine an atom containing an object that changes almost every second, it may not be best suited to "focus" on a specific properties of this object using `focusAtom`, because anyway they will all re-render in the same time, so best adding no overhead and not create any more atoms. On the other hand, if your object has properties that rarely change, and most importantly, that change independently from the other properties, then you may want to use `focusAtom` or `selectAtom` to prevent un-necessary renders. ================================================ FILE: docs/guides/persistence.mdx ================================================ --- title: Persistence description: How to persist atoms nav: 8.14 --- Jotai has an [atomWithStorage function in the utils bundle](../utilities/storage.mdx) for persistence that supports persisting state in `sessionStorage`, `localStorage`, `AsyncStorage`, or the URL hash. (Note: This guide is a bit outdated and requires some rewrites.) There are also several alternate implementations here: ### A simple pattern with localStorage ```js const strAtom = atom(localStorage.getItem('myKey') ?? 'foo') const strAtomWithPersistence = atom( (get) => get(strAtom), (get, set, newStr) => { set(strAtom, newStr) localStorage.setItem('myKey', newStr) }, ) ``` ### A helper function with localStorage and JSON parse ```js const atomWithLocalStorage = (key, initialValue) => { const getInitialValue = () => { const item = localStorage.getItem(key) if (item !== null) { return JSON.parse(item) } return initialValue } const baseAtom = atom(getInitialValue()) const derivedAtom = atom( (get) => get(baseAtom), (get, set, update) => { const nextValue = typeof update === 'function' ? update(get(baseAtom)) : update set(baseAtom, nextValue) localStorage.setItem(key, JSON.stringify(nextValue)) }, ) return derivedAtom } ``` (Error handling should be added.) ### A helper function with AsyncStorage and JSON parse This requires [onMount](../core/atom.mdx#onmount-property). ```js const atomWithAsyncStorage = (key, initialValue) => { const baseAtom = atom(initialValue) baseAtom.onMount = (setValue) => { ;(async () => { const item = await AsyncStorage.getItem(key) setValue(JSON.parse(item)) })() } const derivedAtom = atom( (get) => get(baseAtom), (get, set, update) => { const nextValue = typeof update === 'function' ? update(get(baseAtom)) : update set(baseAtom, nextValue) AsyncStorage.setItem(key, JSON.stringify(nextValue)) }, ) return derivedAtom } ``` Don't forget to check out the [Async documentation](../guides/async.mdx) for more details on how to use async atoms. ### Example with sessionStorage Same as AsyncStorage, just use `atomWithStorage` util and override the default storage with the `sessionStorage` ```js import { atomWithStorage, createJSONStorage } from 'jotai/utils' const storage = createJSONStorage(() => sessionStorage) const someAtom = atomWithStorage('some-key', someInitialValue, storage) ``` ### A serialize atom pattern ```tsx type Actions = | { type: 'serialize'; callback: (value: string) => void } | { type: 'deserialize'; value: string } const serializeAtom = atom(null, (get, set, action: Actions) => { if (action.type === 'serialize') { const obj = { todos: get(todosAtom).map(get), } action.callback(JSON.stringify(obj)) } else if (action.type === 'deserialize') { const obj = JSON.parse(action.value) // needs error handling and type checking set( todosAtom, obj.todos.map((todo: Todo) => atom(todo)), ) } }) const Persist = () => { const [, dispatch] = useAtom(serializeAtom) const save = () => { dispatch({ type: 'serialize', callback: (value) => { localStorage.setItem('serializedTodos', value) }, }) } const load = () => { const value = localStorage.getItem('serializedTodos') if (value) { dispatch({ type: 'deserialize', value }) } } return (
) } ``` #### Examples ### A pattern with atomFamily ```tsx type Actions = | { type: 'serialize'; callback: (value: string) => void } | { type: 'deserialize'; value: string } const serializeAtom = atom(null, (get, set, action: Actions) => { if (action.type === 'serialize') { const todos = get(todosAtom) const todoMap: Record = {} todos.forEach((id) => { todoMap[id] = get(todoAtomFamily({ id })) }) const obj = { todos, todoMap, filter: get(filterAtom), } action.callback(JSON.stringify(obj)) } else if (action.type === 'deserialize') { const obj = JSON.parse(action.value) // needs error handling and type checking set(filterAtom, obj.filter) obj.todos.forEach((id: string) => { const todo = obj.todoMap[id] set(todoAtomFamily({ id, ...todo }), todo) }) set(todosAtom, obj.todos) } }) const Persist = () => { const [, dispatch] = useAtom(serializeAtom) const save = () => { dispatch({ type: 'serialize', callback: (value) => { localStorage.setItem('serializedTodos', value) }, }) } const load = () => { const value = localStorage.getItem('serializedTodos') if (value) { dispatch({ type: 'deserialize', value }) } } return (
) } ``` #### Examples ================================================ FILE: docs/guides/react-native.mdx ================================================ --- title: React Native description: Using Jotai in React Native nav: 8.06 keywords: native,ios,android --- Jotai atoms can be used in React Native applications with absolutely no changes. Our goal is to always be 100% compatible with React-Native. ### Persistence When it comes to persistence feature, the implementation specific to React Native are detailed in the [atomWithStorage function in the utils bundle](../utilities/storage.mdx). ### Performance There is no known specific overhead when using Jotai in your app. Some libraries will add some/lots of additional properties and methods to the stored data for the practical usage, but Jotai behaves differently and you're always manipulating simple stuff that could barely be shortcuted. Jotai atomic architecture will encourage you to split logic and data, providing a top-most experience to control every one of your render ([or commits, to be precise](https://legacy.reactjs.org/blog/2018/09/10/introducing-the-react-profiler.html#browsing-commits)) and therefore reach the best performances. And always remember that renders have to be fast, split calculation logic to async actions. ================================================ FILE: docs/guides/remix.mdx ================================================ --- title: Remix description: How to use Jotai with Remix nav: 8.05 keywords: remix status: draft --- ### Hydration Jotai has support for hydration of atoms with `useHydrateAtoms`. The documentation for the hook can be seen [here](../utilities/ssr.mdx). ================================================ FILE: docs/guides/resettable.mdx ================================================ --- title: Resettable description: How to use resettable atoms nav: 8.99 published: false --- The Jotai core doesn't support resettable atoms. But you can create those with helper functions from `jotai/utils`. ### Primitive resettable atom with atomWithReset / useResetAtom ```jsx import { useAtom } from 'jotai' import { atomWithReset, useResetAtom } from 'jotai/utils' const todoListAtom = atomWithReset([ { description: 'Add a todo', checked: false }, ]) const TodoList = () => { const [todoList, setTodoList] = useAtom(todoListAtom) const resetTodoList = useResetAtom(todoListAtom) return ( <>
    {todoList.map((todo) => (
  • {todo.description}
  • ))}
) } ``` ### Examples ### Derived atom with RESET symbol ```jsx import { atom, useAtom, useSetAtom } from 'jotai' import { atomWithReset, useResetAtom, RESET } from 'jotai/utils' const dollarsAtom = atomWithReset(0) const centsAtom = atom( (get) => get(dollarsAtom) * 100, (get, set, newValue: number | typeof RESET) => set(dollarsAtom, newValue === RESET ? newValue : newValue / 100) ) const ResetExample = () => { const [dollars] = useAtom(dollarsAtom) const setCents = useSetAtom(centsAtom) const resetCents = useResetAtom(centsAtom) return ( <>

Current balance ${dollars}

) } ``` ### Examples ================================================ FILE: docs/guides/testing.mdx ================================================ --- title: Testing description: How to test your code using Jotai nav: 8.09 keywords: test,testing --- We echo the [guiding principles of Testing library](https://testing-library.com/docs/guiding-principles/): - "The more your tests resemble the way your software is used, the more confidence they can give you." We encourage you to write tests, like the user would interact with your atoms and components, therefore treating Jotai as an implementation detail. Here's an example using [React testing library](https://github.com/testing-library/react-testing-library): `Counter.tsx`: ```jsx import { atom, useAtom } from 'jotai' export const countAtom = atom(0) export function Counter() { const [count, setCount] = useAtom(countAtom) return (

{count}

) } ``` `Counter.test.ts`: ```jsx import React from 'react' import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { Counter } from './Counter' test('should increment counter', () => { // Arrange render() const counter = screen.getByText('0') const incrementButton = screen.getByText('one up') // Act await userEvent.click(incrementButton) // Assert expect(counter.textContent).toEqual('1') }) ``` ### Injected Values You may want to inject arbitrary values to your atom before starting some tests. Maybe the counter should be limited to 100. Let's see how to test that it doesn't increase after reaching 100. In order to do that, simply use a [Provider](../core/provider.mdx), and export your atom to be filled-in. ```tsx import React from 'react' import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { useHydrateAtoms } from 'jotai/utils' import { countAtom, Counter } from './Counter' import { Provider } from 'jotai' const HydrateAtoms = ({ initialValues, children }) => { useHydrateAtoms(initialValues) return children } const TestProvider = ({ initialValues, children }) => ( {children} ) const CounterProvider = () => { return ( ) } test('should not increment on max (100)', () => { render() const counter = screen.getByText('100') const incrementButton = screen.getByText('one up') await userEvent.click(incrementButton) expect(counter.textContent).toEqual('100') }) ``` ### Custom hooks If you have complex atoms, sometimes you want to test them in isolation. For that, you can use [React Hooks Testing Library](https://github.com/testing-library/react-hooks-testing-library). Here's an example below: `countAtom.ts`: ```ts import { useAtom } from 'jotai' import { atomWithReducer } from 'jotai/utils' const reducer = (state: number, action?: 'INCREASE' | 'DECREASE') => { switch (action) { case 'INCREASE': return state + 1 case 'DECREASE': return state - 1 case undefined: return state } } export const countAtom = atomWithReducer(0, reducer) ``` `countAtom.test.ts`: ```ts import { renderHook, act } from '@testing-library/react-hooks' import { useAtom } from 'jotai' import { countAtom } from './countAtom' test('should increment counter', () => { const { result } = renderHook(() => useAtom(countAtom)) act(() => { result.current[1]('INCREASE') }) expect(result.current[0]).toBe(1) }) ``` ### Example with React-Native Of course, you can test React-Native components too the same way, with or without `Provider`. ```tsx import React from 'react' import { render, fireEvent } from '@testing-library/react-native' import { Counter } from './counter' test('should increment counter', () => { // Arrange const { getByText } = render() const counter = getByText('0') const incrementButton = getByText('one up') // Act fireEvent.press(incrementButton) // Assert expect(counter.props.children.toString()).toEqual('1') }) ``` ================================================ FILE: docs/guides/typescript.mdx ================================================ --- title: TypeScript description: How to use Jotai with TypeScript nav: 8.02 keywords: typescript,types --- ### Version requirement Jotai uses TypeScript 3.8+ syntax. Upgrade your TypeScript version if you're on 3.7.5 or lower. Jotai relies heavily on type inferences and requires `strictNullChecks` to be enabled. Consider adding `"strict": true` in your tsconfig.json. [#550](https://github.com/pmndrs/jotai/issues/550) [#802](https://github.com/pmndrs/jotai/issues/802) [#838](https://github.com/pmndrs/jotai/issues/838) ### Notes #### Primitive atoms are basically type inferred ```ts const numAtom = atom(0) // primitive number atom const strAtom = atom('') // primitive string atom ``` ### Primitive atoms can be explicitly typed ```ts const numAtom = atom(0) const numAtom = atom(0) const arrAtom = atom([]) ``` #### Derived atoms can mostly have their types inferred In general, this is the recommended approach since typing derived atoms can get a little confusing, particularly for those who are doing it for the first time. ```ts # Read only derived atoms const readOnlyAtom = atom((get) => get(numAtom)) const asyncReadOnlyAtom = atom(async (get) => await get(someAsyncAtom)) # Write only atoms const writeOnlyAtom = atom(null, (_get, set, str: string) => set(fooAtom, str)) const multipleArgumentsAtom = atom( null, (_get, set, valueOne: number, valueTwo: number) => set(fooAtom, Math.max(valueOne, valueTwo)) ); # Read/Write atoms const readWriteAtom = atom( (get) => get(strAtom), (_get, set, num: number) => set(strAtom, String(num)) ) const asyncReadWriteAtom = atom( async (get) => await get(asyncStrAtom), (_get, set, num: number) => set(strAtom, String(num)) ) ``` #### Derived atoms can also be explicitly typed If you encounter a situation where you need or want to explicitly type your derived atoms, you can do that as well. ```ts const asyncStrAtom = atom>(async () => 'foo') /** * For write only atoms you'll need to supply three type parameters. * The first type parameter describes the value returned from the atom. In the following example this is `null`. * The second type parameter describes the arguments (plural) you will pass to the "write" function. Even if you only * plan to have one argument, this type must be an array as show in the example. * The third type parameter describes the return value of the "write" function. Normally, there is no return value, * which is why we use `void` in the example below. */ const writeOnlyAtom = atom( null, (_get, set, stringValue, numberValue) => set(fooAtom, stringValue), ) /** * Read/Write atoms also take the same three type parameters. * Just for the sake of completeness, in this example, we show that the first type parameter * can also describe an async atom. */ const readWriteAtom = atom, [number], void>( async (get) => await get(asyncStrAtom), (_get, set, num) => set(strAtom, String(num)), ) ``` #### useAtom is typed based on atom types ```ts const [num, setNum] = useAtom(primitiveNumAtom) const [num] = useAtom(readOnlyNumAtom) const [, setNum] = useAtom(writeOnlyNumAtom) ``` #### Access to the value type of an atom ```ts import { ExtractAtomValue, useAtomValue } from 'jotai' import { userAtom } from 'state' import { useQuery } from '@tanstack/react-query' export default function WriteReview(hid) { const user = useAtomValue(userAtom) const res = useGetReviewQuery(user) } function useGetReviewQuery(user: ExtractAtomValue) { return fetch('/api/user/' + user.id + '/review') } ``` ================================================ FILE: docs/guides/using-store-outside-react.mdx ================================================ --- title: Using store outside React description: Using store outside React nav: 8.98 keywords: state, outside, react published: false --- Jotai's state resides in React, but sometimes it would be nice to interact with the world outside React. ## createStore [`createStore`](../core/store.mdx#createstore) provides a store interface that can be used to store your atoms. Using the store, you can access and mutate the state of your stored atoms from outside React. ```jsx import { atom, useAtomValue, createStore, Provider } from 'jotai' const timeAtom = atom(0) const store = createStore() store.set(timeAtom, (prev) => prev + 1) // Update atom's value store.get(timeAtom) // Read atom's value function Component() { const time = useAtomValue(timeAtom) // Inside React return (

{time}

) } export default function App() { return ( ) } ``` ### Examples ================================================ FILE: docs/guides/vite.mdx ================================================ --- title: Vite description: How to use Jotai with Vite nav: 8.99 keywords: vite published: false --- You can use the plugins from the `jotai-babel` package to enhance your developer experience when using Vite and Jotai. In your `vite.config.ts`: ```js import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import jotaiDebugLabel from 'jotai-babel/plugin-debug-label' import jotaiReactRefresh from 'jotai-babel/plugin-react-refresh' // https://vitejs.dev/config/ export default defineConfig({ plugins: [ react({ babel: { plugins: [jotaiDebugLabel, jotaiReactRefresh] } }), ], // ... The rest of your configuration }) ``` There's a template below to try it yourself. ### Examples #### Vite Template ================================================ FILE: docs/guides/waku.mdx ================================================ --- title: Waku description: How to use Jotai with Waku nav: 8.04 keywords: waku status: draft --- ### Hydration Jotai has support for hydration of atoms with `useHydrateAtoms`. The documentation for the hook can be seen [here](../utils/ssr.mdx). ================================================ FILE: docs/index.mdx ================================================ --- title: Documentation description: Table of contents nav: 0 --- Welcome to the Jotai v2 documentation! Jotai's atomic approach to global React state management scales from a simple `useState` replacement to an enterprise application with complex requirements. ## Features - Minimal core API (2kb) - Many utilities and extensions - TypeScript oriented - Works with Next.js, Waku, Remix, and React Native ## Core Jotai has a very minimal API, exposing only a few exports from the main `jotai` bundle. They are split into four categories below. ## Utilities Jotai also includes a `jotai/utils` bundle with a variety of extra utility functions. One example is `atomWithStorage`, which includes localStorage persistence and cross browser tab synchronization. ## Extensions Jotai has many officially maintained extensions including `atomWithQuery` for React Query and `atomWithMachine` for XState, among many others. ## Third-party Beyond the official extensions, there are many third-party community packages as well. ## Tools Use SWC and Babel compiler plugins for React Fast Refresh support and debugging labels. This creates the best developer experience when using a React framework such as Next.js or Waku. ## Basics Learn the basic concepts of the library, discover how it compares with others, and see usage examples. ## Guides Guides can help with use common cases such as TypeScript, React frameworks, and basic patterns. ## Recipes Recipes can help with more advanced patterns. ================================================ FILE: docs/recipes/atom-with-broadcast.mdx ================================================ --- title: atomWithBroadcast nav: 9.09 keywords: creators,broadcast --- > `atomWithBroadcast` creates an atom. The atom will be shared between > browser tabs and frames, similar to `atomWithStorage` but with the > initialization limitation. This can be useful when you want states to interact with each other without the use of localStorage. By using the BroadcastChannel API, you can enable basic communication between browsing contexts such as windows, tabs, frames, components, or iframes, and workers on the same origin. According to the MDN documentation, receiving a message during initialization is not supported in the BroadcastChannel, but if you want to support that functionality, you may need to add extra option to atomWithBroadcast, such as local storage. ```tsx import { atom, SetStateAction } from 'jotai' export function atomWithBroadcast(key: string, initialValue: Value) { const baseAtom = atom(initialValue) const listeners = new Set<(event: MessageEvent) => void>() const channel = new BroadcastChannel(key) channel.onmessage = (event) => { listeners.forEach((l) => l(event)) } const broadcastAtom = atom( (get) => get(baseAtom), (get, set, update: { isEvent: boolean; value: SetStateAction }) => { set(baseAtom, update.value) if (!update.isEvent) { channel.postMessage(get(baseAtom)) } }, ) broadcastAtom.onMount = (setAtom) => { const listener = (event: MessageEvent) => { setAtom({ isEvent: true, value: event.data }) } listeners.add(listener) return () => { listeners.delete(listener) } } const returnedAtom = atom( (get) => get(broadcastAtom), (_get, set, update: SetStateAction) => { set(broadcastAtom, { isEvent: false, value: update }) }, ) return returnedAtom } const broadAtom = atomWithBroadcast('count', 0) const ListOfThings = () => { const [count, setCount] = useAtom(broadAtom) return (
{count}
) } ``` ================================================ FILE: docs/recipes/atom-with-compare.mdx ================================================ --- title: atomWithCompare nav: 9.05 keywords: creators,compare --- > `atomWithCompare` creates atom that triggers updates when custom compare function `areEqual(prev, next)` is false. This can help you avoid unwanted re-renders by ignoring state changes that don't matter to your application. Note: Jotai uses `Object.is` internally to compare values when changes occur. If `areEqual(a, b)` returns false, but `Object.is(a, b)` returns true, Jotai will not trigger an update. ```ts import { atomWithReducer } from 'jotai/utils' export function atomWithCompare( initialValue: Value, areEqual: (prev: Value, next: Value) => boolean, ) { return atomWithReducer(initialValue, (prev: Value, next: Value) => { if (areEqual(prev, next)) { return prev } return next }) } ``` Here's how you'd use it to make an atom that ignores updates that are shallow-equal: ```ts import { atomWithCompare } from 'XXX' import { shallowEquals } from 'YYY' import { CSSProperties } from 'react' const styleAtom = atomWithCompare( { backgroundColor: 'blue' }, shallowEquals, ) ``` In a component: ```jsx const StylePreview = () => { const [styles, setStyles] = useAtom(styleAtom) return (
Style preview
{/* Clicking this button twice will only trigger one render */} {/* Clicking this button twice will only trigger one render */}
) } ``` ================================================ FILE: docs/recipes/atom-with-debounce.mdx ================================================ --- title: atomWithDebounce nav: 9.10 keywords: creators,debounce --- > `atomWithDebounce` helps with creating an atom where state set should be debounced. This util is useful for text search inputs, where you would like to call **functions in derived atoms only once** after waiting for a duration, instead of firing an action on every keystroke. ```tsx import { atom, SetStateAction } from 'jotai' export default function atomWithDebounce( initialValue: T, delayMilliseconds = 500, shouldDebounceOnReset = false, ) { const prevTimeoutAtom = atom | undefined>( undefined, ) // DO NOT EXPORT currentValueAtom as using this atom to set state can cause // inconsistent state between currentValueAtom and debouncedValueAtom const _currentValueAtom = atom(initialValue) const isDebouncingAtom = atom(false) const debouncedValueAtom = atom( initialValue, (get, set, update: SetStateAction) => { clearTimeout(get(prevTimeoutAtom)) const prevValue = get(_currentValueAtom) const nextValue = typeof update === 'function' ? (update as (prev: T) => T)(prevValue) : update const onDebounceStart = () => { set(_currentValueAtom, nextValue) set(isDebouncingAtom, true) } const onDebounceEnd = () => { set(debouncedValueAtom, nextValue) set(isDebouncingAtom, false) } onDebounceStart() if (!shouldDebounceOnReset && nextValue === initialValue) { onDebounceEnd() return } const nextTimeoutId = setTimeout(() => { onDebounceEnd() }, delayMilliseconds) // set previous timeout atom in case it needs to get cleared set(prevTimeoutAtom, nextTimeoutId) }, ) // exported atom setter to clear timeout if needed const clearTimeoutAtom = atom(null, (get, set, _arg) => { clearTimeout(get(prevTimeoutAtom)) set(isDebouncingAtom, false) }) return { currentValueAtom: atom((get) => get(_currentValueAtom)), isDebouncingAtom, clearTimeoutAtom, debouncedValueAtom, } } ``` ### Caveat Please note that this atom has different objectives from concurrent features in React 18 such as `useTransition` and `useDeferredValue` whose main aim is to prevent blocking of interaction with the page for expensive updates. For more info, please read this github discussion https://github.com/reactwg/react-18/discussions/41 under the section titled **"How is it different from setTimeout?"** ### Example Usage The sandbox link below shows how we would use a derived atom to fetch state based on the value of `debouncedValueAtom`. When typing a pokemon's name in ``, we do not send a get request on every letter, but only after `delayMilliseconds` has passed since the last text input. This reduces the number of backend requests to the server. ================================================ FILE: docs/recipes/atom-with-listeners.mdx ================================================ --- title: atomWithListeners nav: 9.08 keywords: creators,listeners --- > `atomWithListeners` creates an atom and a hook. The hook can be called to > add a new listener. The hook takes as an argument a callback, and that > callback is called every time the atom's value is set. The hook also > returns a function to remove the listener. This can be useful when you want to create a component that can listen to when an atom's state changes without having to re-render that component with each of those state changes. ```ts import { useEffect } from 'react' import { atom, useAtom, useSetAtom, Getter, Setter, SetStateAction, } from 'jotai' type Callback = ( get: Getter, set: Setter, newVal: Value, prevVal: Value, ) => void export function atomWithListeners(initialValue: Value) { const baseAtom = atom(initialValue) const listenersAtom = atom[]>([]) const anAtom = atom( (get) => get(baseAtom), (get, set, arg: SetStateAction) => { const prevVal = get(baseAtom) set(baseAtom, arg) const newVal = get(baseAtom) get(listenersAtom).forEach((callback) => { callback(get, set, newVal, prevVal) }) }, ) const useListener = (callback: Callback) => { const setListeners = useSetAtom(listenersAtom) useEffect(() => { setListeners((prev) => [...prev, callback]) return () => setListeners((prev) => { const index = prev.indexOf(callback) return [...prev.slice(0, index), ...prev.slice(index + 1)] }) }, [setListeners, callback]) } return [anAtom, useListener] as const } ``` In a component: ```jsx const [countAtom, useCountListener] = atomWithListeners(0) function EvenCounter() { const [evenCount, setEvenCount] = useAtom(countAtom) useCountListener( useCallback( (get, set, newVal, prevVal) => { // Every time `countAtom`'s value is set, we check if its new value // is even, and if it is, we increment `evenCount`. if (newVal % 2 === 0) { setEvenCount((c) => c + 1) } }, [setEvenCount], ), ) return <>Count was set to an even number {evenCount} times. } ``` ================================================ FILE: docs/recipes/atom-with-refresh-and-default.mdx ================================================ --- title: atomWithRefreshAndDefault nav: 9.07 keywords: creators,refresh,default --- > This is for another implementation of [atomWithDefault](../utilities/resettable.mdx#atomwithdefault) ### Look back to atomWithDefault behavior As you can see in the example code in atomWithDefault section, the two atoms' relation is disconnected after updating created one, `count2Atom = atomWithDefault((get) => get(count1Atom) * 2)`. Let's confirm what's occurred, - 1. Click "increment count1", then count1 is 2 and count2 is 4 - 2. Click "increment count2", then count1 is 2 and count2 is 5 (Disconnected!!) Those atoms have no relation after updating count2Atom. So, - Click "increment count1", count1 is incremented only - Even if you reset count2Atom, these dependency relation never come back ### Motivation In some cases, - After disconnecting and resetting, they should come back to their relation - Derived atoms should be reset based on updated the original atom - We'd like to reset all derived atoms but just want to operate as simply as possible How do we make those cases? Here is a declarative way to create a function to provide a refreshable atom instead of atomWithDefault. ```js const refreshCountAtom = atom(0) const baseDataAtom = atom(1) // original data, e.g. base count1Atom const dataAtom = atom( (get) => { get(refreshCountAtom) // it's introduced at atomWithRefresh return get(baseDataAtom) }, (get, set, update) => { set(baseDataAtom, update) }, ) const atomWithRefreshAndDefault = (refreshAtom, getDefault) => { const overwrittenAtom = atom(null) return atom( (get) => { const lastState = get(overwrittenAtom) if (lastState && lastState.refresh === get(refreshAtom)) { return lastState.value } return getDefault(get) }, (get, set, update) => { set(overwrittenAtom, { refresh: get(refreshAtom), value: update }) }, ) } // This is an alternative of `atomWithDefault((get) => get(count1Atom) * 2)` const refreshableAtom = atomWithRefreshAndDefault( refreshCountAtom, (get) => get(dataAtom) * 2, ) // You can reset by updating just one atom const resetRootAtom = atom(null, (get, set) => { set(refreshCountAtom, get(refreshCountAtom) + 1) }) ``` ================================================ FILE: docs/recipes/atom-with-refresh.mdx ================================================ --- title: atomWithRefresh nav: 9.06 keywords: creators,refresh --- `atomWithRefresh` has been provided by `jotai/utils` since v2.7.0. [Jump to the doc](../utilities/resettable.mdx#atomwithrefresh) ================================================ FILE: docs/recipes/atom-with-toggle-and-storage.mdx ================================================ --- title: atomWithToggleAndStorage nav: 9.05 keywords: creators,storage --- > `atomWithToggleAndStorage` is like `atomWithToggle` but also persist the state anytime it changes in given storage using [`atomWithStorage`](../utilities/storage.mdx). Here is the source: ```ts import { WritableAtom, atom } from 'jotai' import { atomWithStorage } from 'jotai/utils' export function atomWithToggleAndStorage( key: string, initialValue?: boolean, storage?: any, ): WritableAtom { const anAtom = atomWithStorage(key, initialValue, storage) const derivedAtom = atom( (get) => get(anAtom), (get, set, nextValue?: boolean) => { const update = nextValue ?? !get(anAtom) void set(anAtom, update) }, ) return derivedAtom as WritableAtom } ``` And how it's used: ```js import { atomWithToggleAndStorage } from 'XXX' // will have an initial value set to false & get stored in localStorage under the key "isActive" const isActiveAtom = atomWithToggleAndStorage('isActive') ``` The usage in a component is also the same as `atomWithToggle`. ================================================ FILE: docs/recipes/atom-with-toggle.mdx ================================================ --- title: atomWithToggle nav: 9.04 keywords: creators,toggle --- > `atomWithToggle` creates a new atom with a boolean as initial state & a setter function to toggle it. This avoids the boilerplate of having to set up another atom just to update the state of the first. ```ts import { WritableAtom, atom } from 'jotai' export function atomWithToggle( initialValue?: boolean, ): WritableAtom { const anAtom = atom(initialValue, (get, set, nextValue?: boolean) => { const update = nextValue ?? !get(anAtom) set(anAtom, update) }) return anAtom as WritableAtom } ``` An optional initial state can be provided as the first argument. The setter function can have an optional argument to force a particular state, such as if you want to make a setActive function out of it. Here is how it's used. ```js import { atomWithToggle } from 'XXX' // will have an initial value set to true const isActiveAtom = atomWithToggle(true) ``` And in a component: ```jsx const Toggle = () => { const [isActive, toggle] = useAtom(isActiveAtom) return ( <> ) } ``` ================================================ FILE: docs/recipes/custom-useatom-hooks.mdx ================================================ --- title: Custom useAtom hooks nav: 9.02 keywords: custom,hook --- This page shows the ways of creating different utility functions. Utility functions save your time on coding, and you can preserve your base atom for other usage. ### utils #### useSelectAtom ```js import { useAtomValue } from 'jotai' import { selectAtom } from 'jotai/utils' export function useSelectAtom(anAtom, selector) { const selectorAtom = selectAtom( anAtom, selector, // Alternatively, you can customize `equalityFn` to determine when it will rerender // Check selectAtom's signature for details. ) return useAtomValue(selectorAtom) } // how to use it function useN(n) { const selector = useCallback((v) => v[n], [n]) return useSelectAtom(arrayAtom, selector) } ``` Please note that in this case `keyFn` must be stable, either define outside render or wrap with `useCallback`. #### useFreezeAtom ```js import { useAtom } from 'jotai' import { freezeAtom } from 'jotai/utils' export function useFreezeAtom(anAtom) { return useAtom(freezeAtom(anAtom)) } ``` #### useSplitAtom ```js import { useAtom } from 'jotai' import { splitAtom } from 'jotai/utils' export function useSplitAtom(anAtom) { return useAtom(splitAtom(anAtom)) } ``` ### extensions #### useFocusAtom ```js import { useAtom } from 'jotai' import { focusAtom } from 'jotai-optics' /* if an atom is created here, please use `useMemo(() => atom(initValue), [initValue])` instead. */ export function useFocusAtom(anAtom, keyFn) { return useAtom(focusAtom(anAtom, keyFn)) } // how to use it useFocusAtom(anAtom) { useMemo(() => atom(initValue), [initValue]), useCallback((optic) => optic.prop('key'), []) } ``` #### Stackblitz Please note that in this case `keyFn` must be stable, either define outside render or wrap with `useCallback`. ================================================ FILE: docs/recipes/large-objects.mdx ================================================ --- title: Large objects nav: 9.01 keywords: large,object --- > The examples and descriptions below are based on this [codesandbox](https://codesandbox.io/s/zealous-sun-f2qnl?file=/src/App.tsx), so it will give you a better understanding if you check it out along with these examples. Sometimes we have nested data we need to store in atoms, and we may need to change that data at different levels, or we need to use part of that data without listening to all changes. Consider this example: ```js const initialData = { people: [ { name: 'Luke Skywalker', information: { height: 172 }, siblings: ['John Skywalker', 'Doe Skywalker'], }, { name: 'C-3PO', information: { height: 167 }, siblings: ['John Doe', 'Doe John'], }, ], films: [ { title: 'A New Hope', planets: ['Tatooine', 'Alderaan'], }, { title: 'The Empire Strikes Back', planets: ['Hoth'], }, ], info: { tags: ['People', 'Films', 'Planets', 'Titles'], }, } ``` ## focusAtom > `focusAtom` creates a new atom, based on the focus that you pass to it. [jotai-optics](../extensions/optics.mdx#focusatom) We use this utility to focus an atom and create an atom from a specific part of the data. For example we may need to consume the people property of the above data, Here's how we do it: ```js import { atom } from 'jotai' import { focusAtom } from 'jotai-optics' const dataAtom = atom(initialData) const peopleAtom = focusAtom(dataAtom, (optic) => optic.prop('people')) ``` `focusAtom` returns `WritableAtom` which means it's possible to change the `peopleAtom` data. If we change the `films` property of the above data example, the `peopleAtom` won't cause a re-render, so that's one of the benefits of using `focusAtom`. ## splitAtom > The `splitAtom` utility is useful when you want to get an atom for each element in a list. [jotai/utils](../utilities/split.mdx) We use this utility for atoms that return arrays as their values. For example, the `peopleAtom` we made above returns the people property array, so we can return an atom for each item of that array. If the array atom is writable, `splitAtom` returned atoms are going to be writable, if the array atom is read-only, the returned atoms will be read-only too. ```js import { splitAtom } from 'jotai/utils' const peopleAtomsAtom = splitAtom(peopleAtom) ``` And this is how we use it in components. ```jsx const People = () => { const [peopleAtoms] = useAtom(peopleAtomsAtom) return (
{peopleAtoms.map((personAtom) => ( ))}
) } ``` ## selectAtom > This function creates a derived atom whose value is a function of the original atom's value. [jotai/utils](../utilities/select.mdx) This utility is like `focusAtom`, but it always returns a read-only atom. Assume we want to consume the info data, and its data is always unchangeable. We can make a read-only atom from it and select that created atom. ```js // first we create a derived atom based on initialData.info const infoAtom = atom((get) => get(dataAtom).info) ``` Then we use it in our component: ```jsx import { atom, useAtom } from 'jotai' import { selectAtom, splitAtom } from 'jotai/utils' const tagsSelector = (s) => s.tags const Tags = () => { const tagsAtom = selectAtom(infoAtom, tagsSelector) const tagsAtomsAtom = splitAtom(tagsAtom) const [tagAtoms] = useAtom(tagsAtomsAtom) return (
{tagAtoms.map((tagAtom) => ( ))}
) } ``` ================================================ FILE: docs/recipes/use-atom-effect.mdx ================================================ --- title: useAtomEffect nav: 9.03 keywords: effect, atom effect, side effect, side-effect, sideeffect, hook, useAtomEffect --- > `useAtomEffect` runs side effects in response to changes in atoms or props using [atomEffect](../extensions/effect.mdx). The effectFn reruns whenever the atoms it depends on change or the effectFn itself changes. Be sure to memoize the effectFn if it's a function defined in the component. ⚠️ Note: always prefer to use a [stable version of useMemo and useCallback](https://github.com/alexreardon/use-memo-one) to avoid extra atomEffect recomputations. You may rely on useMemo as a performance optimization, but not as a semantic guarantee. In the future, React may choose to “forget” some previously memoized values and recalculate them on next render, e.g. to free memory for offscreen components. ```ts import { useMemoOne as useStableMemo } from 'use-memo-one' import { useAtomValue } from 'jotai/react' import { atomEffect } from 'jotai-effect' type EffectFn = Parameters[0] export function useAtomEffect(effectFn: EffectFn) { useAtomValue(useStableMemo(() => atomEffect(effectFn), [effectFn])) } ``` ### Example Usage ```tsx import { useCallbackOne as useStableCallback } from 'use-memo-one' import { atom, useAtom } from 'jotai' import { atomFamily } from 'jotai/utils' import { useAtomEffect } from './useAtomEffect' const channelSubscriptionAtomFamily = atomFamily( (channelId: string) => { return atom(new Channel(channelId)) }, ) const messagesAtom = atom([]) function Messages({ channelId }: { channelId: string }) { const [messages] = useAtom(messagesAtom) useAtomEffect( useStableCallback( (get, set) => { const channel = get(channelSubscriptionAtomFamily(channelId)) const unsubscribe = channel.subscribe((message) => { set(messagesAtom, (prev) => [...prev, message]) }) return unsubscribe }, [channelId], ), ) return ( <>

You have {messages.length} messages


{messages.map((message) => (
{message.text}
))} ) } ``` ================================================ FILE: docs/recipes/use-reducer-atom.mdx ================================================ --- title: useReducerAtom nav: 9.11 keywords: reducer, hook, useReducerAtom --- `useReducerAtom` is a custom hook to apply a reducer to a primitive atom. It's useful to change the update behavior temporarily. Also, consider [atomWithReducer](../utilities/reducer.mdx) for the atom-level solution. ```ts import { useCallback } from 'react' import { useAtom } from 'jotai' import type { PrimitiveAtom } from 'jotai' export function useReducerAtom( anAtom: PrimitiveAtom, reducer: (v: Value, a: Action) => Value, ) { const [state, setState] = useAtom(anAtom) const dispatch = useCallback( (action: Action) => setState((prev) => reducer(prev, action)), [setState, reducer], ) return [state, dispatch] as const } ``` ### Example Usage ```jsx import { atom } from 'jotai' const countReducer = (prev, action) => { if (action.type === 'inc') return prev + 1 if (action.type === 'dec') return prev - 1 throw new Error('unknown action type') } const countAtom = atom(0) const Counter = () => { const [count, dispatch] = useReducerAtom(countAtom, countReducer) return (
{count}
) } ``` ================================================ FILE: docs/third-party/bunja.mdx ================================================ --- title: Bunja description: State Lifetime Manager nav: 5.03 keywords: scope,di,raii,lifetime --- [Bunja](https://github.com/disjukr/bunja) is lightweight State Lifetime Manager. It provides an RAII wrapper for jōtai atoms. --- See also: - [Bunja README](https://github.com/disjukr/bunja/blob/main/README.md) - [Presentations](https://github.com/disjukr/bunja/tree/main/presentations) ## install ``` npm install bunja ``` ### Defining a Bunja You can define a bunja using the `bunja` function. When you access the defined bunja with the `useBunja` hook, a bunja instance is created. If all components in the render tree that refer to the bunja disappear, the bunja instance is automatically destroyed. If you want to trigger effects when the lifetime of a bunja starts and ends, you can use the `bunja.effect` function. ```ts import { bunja } from 'bunja' import { useBunja } from 'bunja/react' const countBunja = bunja(() => { const countAtom = atom(0) bunja.effect(() => { console.log('mounted') return () => console.log('unmounted') }) return { countAtom } }) function MyComponent() { const { countAtom } = useBunja(countBunja) const [count, setCount] = useAtom(countAtom) // Your component logic here } ``` ### Defining a Bunja that relies on other Bunja If you want to manage a state with a broad lifetime and another state with a narrower lifetime, you can create a (narrower) bunja that depends on a (broader) bunja. For example, you can think of a bunja that holds the page state and another bunja that holds the modal state. The page state lives longer than the modal state, and the modal state should exist from the moment the modal opens until it closes. In such a case, you can write the following code. ```tsx const pageBunja = bunja(() => { const pageStateAtom = atom({}) return { pageStateAtom } }) const childBunja = bunja(() => { const { pageStateAtom } = bunja.use(pageBunja) const childStateAtom = atom((get) => ({ ...get(pageStateAtom), child: 'state', })) return { childStateAtom } }) const modalBunja = bunja(() => { const { pageStateAtom } = bunja.use(pageBunja) const modalStateAtom = atom((get) => ({ ...get(pageStateAtom), modal: 'state', })) bunja.effect(() => { console.log('modal opened') return () => console.log('modal closed') }) return { modalStateAtom } }) function Page() { const [modalOpen, setModalOpen] = useState(false) return ( <> {modalOpen && } ) } function Child() { const { childStateAtom } = useBunja(childBunja) const childState = useAtomValue(childStateAtom) // ... } function Modal() { const { modalStateAtom } = useBunja(modalBunja) const modalState = useAtomValue(modalStateAtom) // ... } ``` Notice that `pageBunja` is not directly `useBunja`-ed. When you `useBunja` either `childBunja` or `modalBunja`, since they depend on `pageBunja`, it has the same effect as if `pageBunja` were also `useBunja`-ed. When the modal is unmounted, there are no longer any places using `useBunja(modalBunja)`, so the instance of `modalBunja` is automatically destroyed. ### Dependency injection using Scope You can use a bunja for local state management. When you specify a scope as a dependency of the bunja, separate bunja instances are created based on the values injected into the scope. ```ts import { bunja, createScope } from 'bunja' const UrlScope = createScope() const fetchBunja = bunja(() => { const url = bunja.use(UrlScope) const queryAtom = atomWithQuery((get) => ({ queryKey: [url], queryFn: async () => (await fetch(url)).json(), })) return { queryAtom } }) ``` #### Injecting dependencies via React context If you bind a scope to a React context, bunjas that depend on the scope can retrieve values from the corresponding React context. In the example below, there are two React instances (``) that reference the same `fetchBunja`, but since each looks at a different context value, two separate bunja instances are also created. ```tsx import { createContext } from 'react' import { bunja, createScope } from 'bunja' import { bindScope } from 'bunja/react' const UrlContext = createContext('https://example.com/') const UrlScope = createScope() bindScope(UrlScope, UrlContext) const fetchBunja = bunja(() => { const url = bunja.use(UrlScope) const queryAtom = atomWithQuery((get) => ({ queryKey: [url], queryFn: async () => (await fetch(url)).json(), })) return { queryAtom } }) function ParentComponent() { return ( <> ) } function ChildComponent() { const { queryAtom } = useBunja(fetchBunja) const { data, isPending, isError } = useAtomValue(queryAtom) // Your component logic here } ``` You can use the `createScopeFromContext` function to handle both the creation of the scope and the binding to the context in one step. ```ts import { createContext } from 'react' import { createScopeFromContext } from 'bunja/react' const UrlContext = createContext('https://example.com/') const UrlScope = createScopeFromContext(UrlContext) ``` #### Injecting dependencies directly into the scope You might want to use a bunja directly within a React component where the values to be injected into the scope are created. In such cases, you can use the second parameter of `useBunja` hook to inject values into the scope without wrapping the context separately. ```tsx function MyComponent() { const { queryAtom } = useBunja(fetchBunja, [ UrlScope.bind('https://example.com/'), ]) const { data, isPending, isError } = useAtomValue(queryAtom) // Your component logic here } ``` ##### Doing the same thing inside a bunja You can use `bunja.fork` to inject scope values from within a bunja initialization function. ```ts const myBunja = bunja(() => { const fooData = bunja.fork(fetchBunja, [ UrlScope.bind('https://example.com/foo'), ]) const barData = bunja.fork(fetchBunja, [ UrlScope.bind('https://example.com/bar'), ]) return { fooData, barData } }) ``` ================================================ FILE: docs/third-party/derive.mdx ================================================ --- title: Derive description: This doc describes Derive extension. nav: 5.01 keywords: derive,derived,async,zalgo,suspense,promise,react --- #### When is this useful? - When local updates to the cache cause micro-suspensions - When unnecessary recomputation causes performance issues Jōtai offers powerful primitives for working with asynchronous data outside of the web framework (e.g. React), and allows the UI and business logic to properly integrate with the data layer. Many data-fetching integrations offer a peek into the client-side cache via atoms. When the cache is not yet populated, the atom has to resolve to a Promise of the value. However, if the value already exists in cache, and we do an optimistic update, then the value can be made available downstream immediately. Building data graphs with these dual-natured (sometimes async, sometimes sync) atoms as a base can lead to unnecessary rerenders, stale values and micro-suspensions (in case of React) if not handled with care. **jotai-derive** provides a primitive for building asynchronous data graphs that act on values as soon as they are available (either awaiting for them, or acting on them synchronously). ### Install You have to install `jotai-derive` to use this feature. ``` npm install jotai-derive ``` ## derive Lets say we have a _dual-natured_ atom, meaning that sometimes we are yet to know the value (e.g. fetching the data), but other times we update the atom locally and can know its value immediately (e.g. optimistic updates). ```ts // `jotai-derive` is applicable to most data-fetching solutions, not just `jotai-apollo` import { atomWithQuery } from 'jotai-apollo'; // An example of a dual-natured atom const userAtom: Atom> = atomWithQuery(...); ``` Below is how a derived atom is usually created. The drawback is that always awaiting (even though the value can be known) introduces unnecessary deferring and recomputation. ```ts // Without `jotai-derive` import { atom } from 'jotai' // Type is Atom>, even though // get(userAtom) does not always return a promise, // meaning we could compute `uppercaseNameAtom` // synchronously. const uppercaseNameAtom = atom(async (get) => { const user = await get(userAtom) return user.name.toUppercase() }) ``` Here is how `jotai-derive` is used to create a tighter async data-processing pipeline. ```ts // With `jotai-derive` import { derive } from 'jotai-derive' // Atom> const uppercaseNameAtom = derive( [userAtom], // will be awaited only when necessary (user) => user.name.toUppercase(), ) ``` ### Multiple async dependencies To derive a value from multiple atoms, the array accepts more than one atom. The corresponding values are then passed into the producer function in the same order. ```ts import { derive } from 'jotai-derive' // Atom> const welcomeMessageAtom = derive( [userAtom, serverNameAtom], (user, serverName) => `Welcome ${user.name} to ${serverName}!`, ) ``` ## soon For more advances usage, for example **conditional dependencies**, the `soon` and `soonAll` functions can be used instead (`derive` is a utility wrapper around them). ### Conditional dependency ```ts // pipes allow for cleaner code when using `soon` directly. import { pipe } from 'remeda'; import { soon } from 'jotai-derive'; // Atom> const queryAtom = ...; // Atom> const isAdminAtom = ...; // Atom> const restrictedItemAtom = atom((get) => pipe( get(isAdminAtom), soon((isAdmin) => (isAdmin ? get(queryAtom) : null)) ) ); ``` ### Conditional dependency (multiple conditions) ```ts // pipes allow for cleaner code when using `soon` directly. import { pipe } from 'remeda'; import { soon, soonAll } from 'jotai-derive'; // Atom> const queryAtom = ...; // Atom> const isAdminAtom = ...; // Atom> const enabledAtom = ...; // Atom> const restrictedItemAtom = atom((get) => pipe( soonAll(get(isAdminAtom), get(enabledAtom)), soon(([isAdmin, enabled]) => (isAdmin && enabled ? get(queryAtom) : null)) ) ); ``` ## Demo ================================================ FILE: docs/third-party/history.mdx ================================================ --- title: History description: A Jōtai utility package for state history nav: 4.04 keywords: history, undo, redo, track changes --- [jotai-history](https://github.com/jotaijs/jotai-history) is a utility package for tracking state history in Jotai. ## Installation ``` npm install jotai-history ``` ## `withHistory` ```js import { withHistory } from 'jotai-history' const targetAtom = atom(0) const limit = 2 const historyAtom = withHistory(targetAtom, limit) function Component() { const [current, previous] = useAtomValue(historyAtom) ... } ``` ### Description `withHistory` creates an atom that tracks the history of states for a given `targetAtom`. The most recent `limit` states are retained. ### Action Symbols - **RESET** Clears the entire history, removing all previous states (including the undo/redo stack). ```js import { RESET } from 'jotai-history' ... function Component() { const setHistoryAtom = useSetAtom(historyAtom) ... setHistoryAtom(RESET) } ``` - **UNDO** and **REDO** Moves the `targetAtom` backward or forward in its history. ```js import { REDO, UNDO } from 'jotai-history' ... function Component() { const setHistoryAtom = useSetAtom(historyAtom) ... setHistoryAtom(UNDO) setHistoryAtom(REDO) } ``` ### Indicators - **canUndo** and **canRedo** Booleans indicating whether undo or redo actions are currently possible. These can be used to disable buttons or conditionally trigger actions. ```jsx ... function Component() { const history = useAtomValue(historyAtom) return ( <> ) } ``` ## Memory Management > Because `withHistory` maintains a list of previous states, be mindful of memory usage by setting a reasonable `limit`. Applications that update state frequently can grow significantly in memory usage. ================================================ FILE: docs/tools/babel.mdx ================================================ --- title: Babel description: This doc describes the `jotai-babel` package. nav: 6.02 keywords: babel,gatsby,fast,refresh --- > **Deprecated**: `jotai/babel` bundle is deprecated and will be removed in v3. Please use the [`jotai-babel`](https://github.com/jotaijs/jotai-babel) package instead. ## jotai-babel/plugin-react-refresh This plugin adds support for React Refresh for Jotai atoms. This makes sure that state isn't reset, when developing using React Refresh. ### Usage With a `babel` configuration file: ```json { "plugins": ["jotai-babel/plugin-react-refresh"] } ``` The plugin recognizes a predefined list of atom function names (e.g. 'atom', 'atomFamily', 'atomWithDefault'). If you're using custom atom function names, you can explicitly supply them to ensure they are recognized. Here's how you can configure it in your Babel setup: ```json { "plugins": [ ["jotai-babel/plugin-react-refresh", { "customAtomNames": ["customAtom"] }] ] } ``` Examples can be found below. ## jotai-babel/plugin-debug-label Jotai is based on object references and not keys (like Recoil). This means there's no identifier for atoms. To identify atoms, it's possible to add a `debugLabel` to an atom, which can be found in React devtools. However, this can quickly become cumbersome to add a `debugLabel` to every atom. This `babel` plugin adds a `debugLabel` to every atom, based on its identifier. The plugin transforms this code: ```js export const countAtom = atom(0) ``` Into: ```js export const countAtom = atom(0) countAtom.debugLabel = 'countAtom' ``` Default exports are also handled, based on the file naming: ```js // countAtom.ts export default atom(0) ``` Which transform into: ```js // countAtom.ts const countAtom = atom(0) countAtom.debugLabel = 'countAtom' export default countAtom ``` ### Usage It is recommended to disable this plugin for production builds to avoid unnecessary overhead. You can conditionally include it in your Babel configuration based on the environment. With a `babel` configuration file: ```json { "plugins": ["jotai-babel/plugin-debug-label"] } ``` The plugin recognizes a predefined list of atom function names (e.g. 'atom', 'atomFamily', 'atomWithDefault'). If you're using custom atom function names, you can explicitly supply them to ensure they are recognized. Here's how you can configure it in your Babel setup: ```json { "plugins": [ ["jotai-babel/plugin-debug-label", { "customAtomNames": ["customAtom"] }] ] } ``` Examples can be found below. ## jotai-babel/preset Jotai ships with a `babel preset` containing the plugins created for Jotai. ### Usage With a `babel` configuration file: ```json { "presets": ["jotai-babel/preset"] } ``` You can also supply your atom names to the preset: ```json { "presets": [["jotai-babel/preset", { "customAtomNames": ["customAtom"] }]] } ``` ### Examples #### Next.js #### Parcel ================================================ FILE: docs/tools/devtools.mdx ================================================ --- title: Devtools description: This doc describes Devtools for Jotai. nav: 6.03 keywords: devtools,debug,snapshot --- ### Install Install `jotai-devtools` to your project to get started. ```sh npm install jotai-devtools ``` ### Notes - `` is optimized to be tree-shakable for production builds, and **only works in a non-production environment** - Hooks are dev-only, and are designed to work in a non-production environment - Feedback welcome, please file issues or ask questions on the [Jotai DevTools GitHub Repo](https://github.com/jotaijs/jotai-devtools/discussions) ### Quick links - [UI Devtools](#ui-devtools) - Hooks - [useAtomsDebugValue](#useatomsdebugvalue) - [useAtomDevtools](#useatomdevtools) - [useAtomsDevtools](#useatomsdevtools) - [useAtomsSnapshot](#useatomssnapshot) - [useGotoAtomsSnapshot](#usegotoatomssnapshot) - Migration guides - [Migrate from `@emotion/react`](https://github.com/jotaijs/jotai-devtools?tab=readme-ov-file#migrate-%C6%92rom-emotionreact-to-native-css) ### UI DevTools Enhance your development experience with the UI based Jotai DevTool. #### Babel plugin setup - (_Optional but highly recommended_) Use [`jotai-babel`](https://github.com/jotaijs/jotai-babel) plugins for optimal debugging experience. Find the complete guide on the [babel](../tools/babel) page and/or [swc](../tools/swc) page. ``` npm install -D jotai-babel ``` ```ts { "presets": [ // The preset includes two plugins: // - jotai-babel/plugin-react-refresh to enable hot reload for atoms // - jotai-babel/plugin-debug-label to automatically adds debug labels to atoms 'jotai-babel/preset' ] } ``` Example for Vite + React project: ```ts // vite.config.ts export default defineConfig({ plugins: [ react({ babel: { presets: ['jotai-babel/preset'], }, }), ], }) ``` If you are using Vite 8, first install these additional packages: ``` npm install -D @rolldown/plugin-babel @babel/core ``` Then use the following example instead: ``` // vite.config.ts import babel from '@rolldown/plugin-babel'; export default defineConfig({ plugins: [ react(), babel({ presets: ['jotai-babel/preset'], }) ], }) ``` ### Next JS setup _You may skip this section if you're not using [Next.js](https://nextjs.org)._ Enable `transpilePackages` for the UI CSS and components to be transpiled correctly. ```ts // next.config.ts const nextConfig = { // Learn more here - https://nextjs.org/docs/advanced-features/compiler#module-transpilation // Required for UI css to be transpiled correctly 👇 transpilePackages: ['jotai-devtools'], } module.exports = nextConfig ``` ### Available props ```ts type DevToolsProps = { // defaults to false isInitialOpen?: boolean // pass a custom store store?: Store // Defaults to light theme?: 'dark' | 'light' // Set the position of the trigger button // Defaults to `bottom-left` position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' // Custom nonce to allowlist jotai-devtools specific inline styles via CSP nonce?: string options?: { // Private atoms are used internally in atom creators like `atomWithStorage` // or `atomWithLocation`, etc. to manage the internal state. // Defaults to `false` shouldShowPrivateAtoms?: boolean // Expands the JSON tree view on initial render on Atom Viewer tab, Timeline tab, etc. // Defaults to `false` shouldExpandJsonTreeViewInitially?: boolean // The interval (in milliseconds) between each step of the time travel playback. // Defaults to `750ms` timeTravelPlaybackInterval?: number // The maximum number of snapshots to keep in the history. // The higher the number the more memory it will consume. // Defaults to `Infinity`. Recommended: `~30` snapshotHistoryLimit?: number } } ``` ### Usage #### Provider-less ```tsx import { DevTools } from 'jotai-devtools' import 'jotai-devtools/styles.css' const App = () => { return ( <> {/* your app */} ) } ``` #### With Provider ```tsx import { createStore } from 'jotai' import { DevTools } from 'jotai-devtools' import 'jotai-devtools/styles.css' const customStore = createStore() const App = () => { return ( {/* your app */} ) } ``` ### Preview ## useAtomsDebugValue `useAtomsDebugValue` is a React hook that will show all atom values in React Devtools. ```ts function useAtomsDebugValue(options?: { store?: Store enabled?: boolean }): void ``` Internally, it uses `useDebugValue` which is only effective in DEV mode. It will catch all atoms that are accessible from the place the hook is located. ### Example ```jsx import { useAtomsDebugValue } from 'jotai-devtools/utils' const textAtom = atom('hello') textAtom.debugLabel = 'textAtom' const lenAtom = atom((get) => get(textAtom).length) lenAtom.debugLabel = 'lenAtom' const TextBox = () => { const [text, setText] = useAtom(textAtom) const [len] = useAtom(lenAtom) return ( setText(e.target.value)} />({len}) ) } const DebugAtoms = () => { useAtomsDebugValue() return null } const App = () => ( ) ``` ## useAtomDevtools `useAtomDevtools` is a React hook that manages ReduxDevTools extension for a particular atom. ```ts function useAtomDevtools( anAtom: WritableAtom, options?: { store?: Store name?: string enabled?: boolean }, ): void ``` The `useAtomDevtools` hook accepts a generic type parameter (mirroring the type stored in the atom). Additionally, the hook accepts two invocation parameters, `anAtom` and `name`. `anAtom` is the atom that will be attached to the devtools instance. `name` is an optional parameter that defines the debug label for the devtools instance. If `name` is undefined, `atom.debugLabel` will be used instead. ### Example ```ts import { useAtomDevtools } from 'jotai-devtools/utils' // The interface for the type stored in the atom. export interface Task { label: string complete: boolean } // The atom to debug. export const tasksAtom = atom([]) // If the useAtomDevtools name parameter is undefined, this value will be used instead. tasksAtom.debugLabel = 'Tasks' export const useTasksDevtools = () => { // The hook can be called simply by passing an atom for debugging. useAtomDevtools(tasksAtom) // Specify a custom type parameter useAtomDevtools(tasksAtom) // You can attach two devtools instances to the same atom and differentiate them with custom names. useAtomDevtools(tasksAtom, 'Tasks (Instance 1)') useAtomDevtools(tasksAtom, 'Tasks (Instance 2)') } ``` ## useAtomsDevtools ⚠️ Note: This hook is experimental (feedbacks are welcome) and only works in a `process.env.NODE_ENV !== 'production'` environment. `useAtomsDevtools` is a catch-all version of `useAtomDevtools` where it shows all atoms in the store instead of showing a specific one. ```ts function useAtomsDevtools( name: string, options?: { store?: Store enabled?: boolean }, ): void ``` It takes a `name` parameter that is needed for naming the Redux devtools instance and a `store` parameter. As a limitation for this API, we need to put `useAtomsDevtools` in a component where the consumed atoms should be in a lower place of the React tree than that component (`AtomsDevtools` in the below example). `AtomsDevtools` component can be considered as a best practice for our apps. ### Example ```jsx const countAtom = atom(0); const doubleCountAtom = atom((get) => get(countAtom) * 2); function Counter() { const [count, setCount] = useAtom(countAtom); const [doubleCount] = useAtom(doubleCountAtom); ... } const AtomsDevtools = ({ children }) => { useAtomsDevtools('demo') return children } export default function App() { return ( ) } ``` ## useAtomsSnapshot ⚠️ Note: This hook only works in a `process.env.NODE_ENV !== 'production'` environment. And it returns a static empty value in production. `useAtomsSnapshot` takes a snapshot of the currently mounted atoms and their state. ```ts function useAtomsSnapshot(options?: { store?: Store }): AtomsSnapshot ``` It accepts a `store` parameter and will return an `AtomsSnapshot`, which is basically a `Map`. You can use the `Map` API to iterate over atoms and their state. This hook is primarily meant for debugging and devtools use cases. Be careful using this hook because it will cause the component to re-render for all state changes. ### Example ```jsx import { Provider } from 'jotai' import { useAtomsSnapshot } from 'jotai-devtools/utils' const RegisteredAtoms = () => { const atoms = useAtomsSnapshot() return (

Atom count: {atoms.size}

{Array.from(atoms).map(([atom, atomValue]) => (

{`${atom.debugLabel}: ${atomValue}`}

))}
) } const App = () => ( ) ``` ## useGotoAtomsSnapshot ⚠️ Note: This hook only works in a `process.env.NODE_ENV !== 'production'` environment. And it works like an empty function in the production environment. `useGotoAtomsSnapshot` will update the current Jotai state to match the passed snapshot. ```ts function useGotoAtomsSnapshot(options?: { store?: Store }): (values: Iterable) => void ``` This hook returns a callback, which takes a `snapshot` from the `useAtomsSnapshot` hook and will update the Jotai state. It accepts a `store` parameter. This hook is primarily meant for debugging and devtools use cases. ### Example ```jsx import { Provider } from 'jotai' import { useAtomsSnapshot, useGotoAtomsSnapshot } from 'jotai-devtools/utils' const petAtom = atom('cat') const colorAtom = atom('blue') const UpdateSnapshot = () => { const snapshot = useAtomsSnapshot() const goToSnapshot = useGotoAtomsSnapshot() return ( ) } ``` ================================================ FILE: docs/tools/swc.mdx ================================================ --- title: SWC description: This doc describes SWC plugins for Jotai. nav: 6.01 keywords: swc,next,nextjs,fast,refresh --- ⚠️ Note: These plugins are experimental. Feedback is welcome in the [Github repo](https://github.com/pmndrs/swc-jotai). Please file issues in the separate repo instead of `jotai` repo. ## @swc-jotai/react-refresh This plugin adds support for React Refresh for Jotai atoms. This makes sure that state isn't reset, when developing using React Refresh. ### Usage Install it with: ```sh npm install --save-dev @swc-jotai/react-refresh ``` You can add the plugin to `.swcrc`: ```json { "jsc": { "experimental": { "plugins": [["@swc-jotai/react-refresh", {}]] } } } ``` You can use the plugin with the [experimental SWC plugins feature](https://nextjs.org/docs/advanced-features/compiler#swc-plugins-experimental) in Next.js. ```js module.exports = { experimental: { swcPlugins: [['@swc-jotai/react-refresh', {}]], }, } ``` Examples can be found below. ## @swc-jotai/debug-label Jotai is based on object references and not keys (like Recoil). This means there's no identifier for atoms. To identify atoms, it's possible to add a `debugLabel` to an atom, which can be found in React devtools. However, this can quickly become cumbersome to add a `debugLabel` to every atom. This `SWC` plugin adds a `debugLabel` to every atom, based on its identifier. The plugin transforms this code: ```js export const countAtom = atom(0) ``` Into: ```js export const countAtom = atom(0) countAtom.debugLabel = 'countAtom' ``` Default exports are also handled, based on the file naming: ```js // countAtom.ts export default atom(0) ``` Which transform into: ```js // countAtom.ts const countAtom = atom(0) countAtom.debugLabel = 'countAtom' export default countAtom ``` ### Usage Install it with: ```sh npm install --save-dev @swc-jotai/debug-label ``` You can add the plugin to `.swcrc`: ```json { "jsc": { "experimental": { "plugins": [["@swc-jotai/debug-label", {}]] } } } ``` Or you can use the plugin with the [experimental SWC plugins feature](https://nextjs.org/docs/advanced-features/compiler#swc-plugins-experimental) in Next.js. ```js module.exports = { experimental: { swcPlugins: [['@swc-jotai/debug-label', {}]], }, } ``` Examples can be found below. ### Custom atom names You can enable the plugins for your custom atoms. You can supply them to the plugins like below: ```js module.exports = { experimental: { swcPlugins: [ ['@swc-jotai/debug-label', { atomNames: ['customAtom'] }], ['@swc-jotai/react-refresh', { atomNames: ['customAtom'] }], ], }, } ``` ### Examples ### Next.js ================================================ FILE: docs/utilities/async.mdx ================================================ --- title: Async nav: 3.03 keywords: load,loadable,observable --- All atoms support async behavior such as async read or async write. However there are APIs for more control described here. ## loadable If you don't want async atoms to suspend or throw to an error boundary (for example, for finer-grained control of loading and error logic), you can use the `loadable` util. It would work the same way for any atom. Simply wrap your atoms with the `loadable` util. It returns a value with one of three states: `loading`, `hasData` and `hasError`. ```ts { state: 'loading' | 'hasData' | 'hasError', data?: any, error?: any, } ``` ```jsx import { loadable } from "jotai/utils" const asyncAtom = atom(async (get) => ...) const loadableAtom = loadable(asyncAtom) // Does not need to be wrapped by a element const Component = () => { const [value] = useAtom(loadableAtom) if (value.state === 'hasError') return {value.error} if (value.state === 'loading') { return Loading... } console.log(value.data) // Results of the Promise return Value: {value.data} } ``` ## atomWithObservable Ref: https://github.com/pmndrs/jotai/pull/341 ### Usage ```jsx import { useAtom } from 'jotai' import { atomWithObservable } from 'jotai/utils' import { interval } from 'rxjs' import { map } from 'rxjs/operators' const counterSubject = interval(1000).pipe(map((i) => `#${i}`)) const counterAtom = atomWithObservable(() => counterSubject) const Counter = () => { const [counter] = useAtom(counterAtom) return
count: {counter}
} ``` The `atomWithObservable` function creates an atom from a rxjs (or similar) `subject` or `observable`. Its value will be last value emitted from the stream. To use this atom, you need to wrap your component with ``. Check out [guides/async](../guides/async.mdx). ### Initial value `atomWithObservable` takes second optional parameter `{ initialValue }` that allows to specify initial value for the atom. If `initialValue` is provided then `atomWithObservable` will not suspend and will show initial value before receiving first value from observable. `initialValue` can be either a value or a function that returns a value ```js const counterAtom = atomWithObservable(() => counterSubject, { initialValue: 10, }) const counterAtom2 = atomWithObservable(() => counterSubject, { initialValue: () => Math.random(), }) ``` ### Stackblitz ## unwrap The `unwrap` util will convert an async atom to a sync atom like `loadable`. Unlike `loadable`, the fallback value can be configured. Unlike `loadable`, the error won't be handled and just thrown. The use case of `unwrap` is to ease deriving atoms. This is especially useful for v2 API, because `get` in the read function doesn't resolve promises. ### Signature ```ts function unwrap( anAtom: WritableAtom, ): WritableAtom | undefined, Args, Result> function unwrap( anAtom: WritableAtom, fallback: (prev?: Awaited) => PendingValue, ): WritableAtom | PendingValue, Args, Result> function unwrap(anAtom: Atom): Atom | undefined> function unwrap( anAtom: Atom, fallback: (prev?: Awaited) => PendingValue, ): Atom | PendingValue> ``` ### Usage ```tsx import { atom } from 'jotai' import { unwrap } from 'jotai/utils' const countAtom = atom(0) const delayedCountAtom = atom(async (get) => { await new Promise((r) => setTimeout(r, 500)) return get(countAtom) }) const unwrapped1Atom = unwrap(delayedCountAtom) // The value is `undefined` while pending const unwrapped2Atom = unwrap(delayedCountAtom, (prev) => prev ?? 0) // The value is `0` initially, and subsequent updates keep the previous value. ``` ================================================ FILE: docs/utilities/callback.mdx ================================================ --- title: Callback nav: 3.99 keywords: callback published: false --- ## useAtomCallback Ref: https://github.com/pmndrs/jotai/issues/60 ### Usage ```ts useAtomCallback( callback: (get: Getter, set: Setter, ...arg: Args) => Result, options?: Options ): (...args: Args) => Result ``` This hook is for interacting with atoms imperatively. It takes a callback function that works like atom write function, and returns a function that returns an atom value. The callback to pass in the hook must be stable (should be wrapped with useCallback). ### Examples ```jsx import { useEffect, useState, useCallback } from 'react' import { Provider, atom, useAtom } from 'jotai' import { useAtomCallback } from 'jotai/utils' const countAtom = atom(0) const Counter = () => { const [count, setCount] = useAtom(countAtom) return ( <> {count} ) } const Monitor = () => { const [count, setCount] = useState(0) const readCount = useAtomCallback( useCallback((get) => { const currCount = get(countAtom) setCount(currCount) return currCount }, []), ) useEffect(() => { const timer = setInterval(async () => { console.log(readCount()) }, 1000) return () => { clearInterval(timer) } }, [readCount]) return
current count: {count}
} ``` ### Stackblitz ================================================ FILE: docs/utilities/family.mdx ================================================ --- title: Family nav: 3.5 keywords: family --- ## atomFamily :::caution Deprecated `atomFamily` is deprecated and will be removed in Jotai v3. Please migrate to the [`jotai-family`](https://github.com/jotaijs/jotai-family) package, which provides the same API with additional features like `atomTree`. **Migration:** ```bash npm install jotai-family ``` ```ts // Before import { atomFamily } from 'jotai/utils' // After import { atomFamily } from 'jotai-family' ``` The API is identical, so no code changes are needed beyond the import statement. ::: Ref: https://github.com/pmndrs/jotai/issues/23 ### Usage ```js atomFamily(initializeAtom, areEqual): (param) => Atom ``` This will create a function that takes `param` and returns an atom. If the atom has already been created, it will be returned from the cache. `initializeAtom` is a function that can return any kind of atom (`atom()`, `atomWithDefault()`, ...). Note that the `areEqual` argument is optional and compares if two params are equal (defaults to `Object.is`). To reproduce behavior similar to [Recoil's atomFamily/selectorFamily](https://recoiljs.org/docs/api-reference/utils/atomFamily), specify a deepEqual function to `areEqual`. For example: ```js import { atom } from 'jotai' import { atomFamily } from 'jotai/utils' import deepEqual from 'fast-deep-equal' const fooFamily = atomFamily((param) => atom(param), deepEqual) ``` ### TypeScript The atom family types will be inferred from initializeAtom. Here's a typical usage with a primitive atom. ```ts import type { PrimitiveAtom } from 'jotai' /** * here the atom(id) returns a PrimitiveAtom * and PrimitiveAtom is a WritableAtom> */ const myFamily = atomFamily((id: number) => atom(id)) ``` You can explicitly declare the type of parameter and atom type using TypeScript generics. ```ts atomFamily>( initializeAtom: (param: Param) => AtomType, areEqual?: (a: Param, b: Param) => boolean ): AtomFamily ``` Example with explicit types: ```ts import { atom } from 'jotai' import type { PrimitiveAtom } from 'jotai' import { atomFamily } from 'jotai/utils' const myFamily = atomFamily>((id: number) => atom(id), ) ``` ### API Methods The `atomFamily` function returns an object with the following methods: #### `myFamily(param)` Returns an atom for the given param. If the atom has already been created, it will be returned from the cache. #### `myFamily.getParams()` Returns an iterable of all params currently in the cache. ```js const todoFamily = atomFamily((name) => atom(name)) todoFamily('foo') todoFamily('bar') for (const param of todoFamily.getParams()) { console.log(param) // 'foo', 'bar' } ``` #### `myFamily.remove(param)` Removes a specific param from the cache. ```js todoFamily.remove('foo') ``` #### `myFamily.setShouldRemove(shouldRemove)` Registers a `shouldRemove` function which runs immediately **and** when you are about to get an atom from the cache. - `shouldRemove` is a function that takes two arguments: `createdAt` (in milliseconds) and `param`, and returns a boolean value. - Setting `null` will remove the previously registered function. ```js // Remove atoms older than 1 hour todoFamily.setShouldRemove((createdAt, param) => { return Date.now() - createdAt > 60 * 60 * 1000 }) ``` #### `myFamily.unstable_listen(callback)` **⚠️ Unstable API**: This API is for advanced use cases and can change without notice. Fires when an atom is created or removed. Returns a cleanup function. ```js const cleanup = todoFamily.unstable_listen((event) => { console.log(event.type) // 'CREATE' or 'REMOVE' console.log(event.param) // the param console.log(event.atom) // the atom }) // Later, stop listening cleanup() ``` ### Caveat: Memory Leaks Internally, atomFamily is just a Map whose key is a param and whose value is an atom config. Unless you explicitly remove unused params, this leads to memory leaks. This is crucial if you use infinite number of params. Use `myFamily.remove(param)` or `myFamily.setShouldRemove(shouldRemove)` to manage memory. ### Examples ```js import { atom } from 'jotai' import { atomFamily } from 'jotai/utils' const todoFamily = atomFamily((name) => atom(name)) todoFamily('foo') // this will create a new atom('foo'), or return the one if already created ``` ```js import { atom } from 'jotai' import { atomFamily } from 'jotai/utils' const todoFamily = atomFamily((name) => atom( (get) => get(todosAtom)[name], (get, set, arg) => { const prev = get(todosAtom) set(todosAtom, { ...prev, [name]: { ...prev[name], ...arg } }) }, ), ) ``` ```js import { atom } from 'jotai' import { atomFamily } from 'jotai/utils' const todoFamily = atomFamily( ({ id, name }) => atom({ name }), (a, b) => a.id === b.id, ) ``` ### Stackblitz --- ## Migration to jotai-family For new projects or when updating existing code, we recommend using the [`jotai-family`](https://github.com/jotaijs/jotai-family) package instead. It provides: - **Same API**: Drop-in replacement for `atomFamily` - **Additional features**: Includes `atomTree` for hierarchical atom management - **Better maintenance**: Dedicated package with focused development - **Future-proof**: Will continue to be supported in Jotai v3 and beyond See the [jotai-family documentation](https://github.com/jotaijs/jotai-family#readme) for more details. ================================================ FILE: docs/utilities/lazy.mdx ================================================ --- title: Lazy nav: 3.03 keywords: lazy,initialize,init,loading --- When defining primitive atoms, their initial value has to be bound at definition time. If creating that initial value is computationally expensive, or the value is not accessible during definition, it would be best to postpone the atom's initialization until its [first use in the store](#using-multiple-stores). ```jsx const imageDataAtom = atom(initializeExpensiveImage()) // 1) has to be computed here function Home() { ... } function ImageEditor() { // 2) used only in this route const [imageData, setImageData] = useAtom(imageDataAtom); ... } function App() { return ( ) } ``` ## atomWithLazy Ref: https://github.com/pmndrs/jotai/pull/2465 We can use `atomWithLazy` to create a primitive atom whose initial value will be computed at [first use in the store](#using-multiple-stores). After initialization, it will behave like a regular primitive atom (can be written to). ### Usage ```jsx import { atomWithLazy } from 'jotai/utils' // passing the initialization function const imageDataAtom = atomWithLazy(initializeExpensiveImage) function Home() { ... } function ImageEditor() { // only gets initialized if user goes to `/edit`. const [imageData, setImageData] = useAtom(imageDataAtom); ... } function App() { return ( ) } ``` ### Using multiple stores Since each store is its separate universe, the initial value will be recreated exactly once per store (unless using something like `jotai-scope`, which fractures a store into smaller universes). ```ts type RGB = [number, number, number]; function randomRGB(): RGB { ... } const lift = (value: number) => ([r, g, b]: RGB) => { return [r + value, g + value, b + value] } const colorAtom = lazyAtom(randomRGB) let store = createStore() console.log(store.get(colorAtom)) // [0, 36, 128] store.set(colorAtom, lift(8)) console.log(store.get(colorAtom)) // [8, 44, 136] // recreating store, sometimes done when logging out or resetting the app in some way store = createStore() console.log(store.get(colorAtom)) // [255, 12, 46] -- a new random color ``` ================================================ FILE: docs/utilities/reducer.mdx ================================================ --- title: Reducer nav: 3.99 keywords: reducer,action,dispatch published: false --- ## atomWithReducer Ref: https://github.com/pmndrs/jotai/issues/38 ```js import { atomWithReducer } from 'jotai/utils' const countReducer = (prev, action) => { if (action.type === 'inc') return prev + 1 if (action.type === 'dec') return prev - 1 throw new Error('unknown action type') } const countReducerAtom = atomWithReducer(0, countReducer) ``` ### Stackblitz ## useReducerAtom See [useReducerAtom](../recipes/use-reducer-atom.mdx) recipe. ================================================ FILE: docs/utilities/resettable.mdx ================================================ --- title: Resettable nav: 3.04 keywords: reset,default --- ## atomWithReset Ref: https://github.com/pmndrs/jotai/issues/41 ```ts function atomWithReset( initialValue: Value, ): WritableAtom | typeof RESET> ``` Creates an atom that could be reset to its `initialValue` with [`useResetAtom`](use-reset-atom.mdx) hook. It works exactly the same way as primitive atom would, but you are also able to set it to a special value [`RESET`](reset.mdx). See examples in [Resettable atoms](../utilities/resettable.mdx). ### Example ```js import { atomWithReset } from 'jotai/utils' const dollarsAtom = atomWithReset(0) const todoListAtom = atomWithReset([ { description: 'Add a todo', checked: false }, ]) ``` ## RESET Ref: https://github.com/pmndrs/jotai/issues/217 ```ts const RESET: unique symbol ``` Special value that is accepted by [Resettable atoms](../utilities/resettable.mdx) created with [`atomWithReset`](../utilities/resettable.mdx), [`atomWithDefault`](../utilities/resettable.mdx) or writable atom created with `atom` if it accepts `RESET` symbol. ### Example ```jsx import { atom, useSetAtom } from 'jotai' import { atomWithReset, useResetAtom, RESET } from 'jotai/utils' const dollarsAtom = atomWithReset(0) const centsAtom = atom( (get) => get(dollarsAtom) * 100, (get, set, newValue: number | typeof RESET) => set(dollarsAtom, newValue === RESET ? newValue : newValue / 100) ) const ResetExample = () => { const setDollars = useSetAtom(dollarsAtom) const resetCents = useResetAtom(centsAtom) return ( <> ) } ``` ## useResetAtom ```ts function useResetAtom( anAtom: WritableAtom, ): () => void | Promise ``` Resets a [Resettable atom](../utilities/resettable.mdx) to its initial value. ### Example ```jsx import { useResetAtom } from 'jotai/utils' import { todoListAtom } from './store' const TodoResetButton = () => { const resetTodoList = useResetAtom(todoListAtom) return } ``` ## atomWithDefault Ref: https://github.com/pmndrs/jotai/issues/352 ### Usage This is a function to create a resettable primitive atom. Its default value can be specified with a read function instead of a static initial value. ```js import { atomWithDefault } from 'jotai/utils' const count1Atom = atom(1) const count2Atom = atomWithDefault((get) => get(count1Atom) * 2) ``` ### Stackblitz ### Resetting default values You can reset the value of an `atomWithDefault` atom to its original default value. ```jsx import { useAtom } from 'jotai' import { atomWithDefault, useResetAtom, RESET } from 'jotai/utils' const count1Atom = atom(1) const count2Atom = atomWithDefault((get) => get(count1Atom) * 2) const Counter = () => { const [count1, setCount1] = useAtom(count1Atom) const [count2, setCount2] = useAtom(count2Atom) const resetCount2 = useResetAtom(count2Atom) return ( <>
count1: {count1}, count2: {count2}
) } ``` This can be useful when an `atomWithDefault` atom value is overwritten using the `set` function, in which case the provided `getter` function is no longer used and any change in dependencies atoms will not trigger an update. Resetting the value allows us to restore its original default value, discarding changes made previously via the `set` function. ## atomWithRefresh ```ts function atomWithRefresh( read: Read, ): WritableAtom ``` Creates an atom that we can refresh, which is to force reevaluating the read function. This is helpful when you need to refresh asynchronous data. It can also be used to implement "pull to refresh" functionality. ```ts function atomWithRefresh( read: Read, write: Write, ): WritableAtom ``` Passing zero arguments to `set` will refresh. Passing one or more arguments to `set` will call "write" function. ### Example Here's how you'd use it to implement an refresh-able source of data: ```js import { atomWithRefresh } from 'jotai/utils' const postsAtom = atomWithRefresh((get) => fetch('https://jsonplaceholder.typicode.com/posts').then((r) => r.json()), ) ``` In a component: ```jsx const PostsList = () => { const [posts, refreshPosts] = useAtom(postsAtom) return (
    {posts.map((post) => (
  • {post.title}
  • ))}
{/* Clicking this button will re-fetch the posts */}
) } ``` ================================================ FILE: docs/utilities/select.mdx ================================================ --- title: Select nav: 3.99 keywords: select published: false --- ## selectAtom ⚠️ Unlike its name, `selectAtom` is provided as an escape hatch. Using it means building not 100% pure atom model. Prefer using derived atoms and use `selectAtom` only when `equalityFn` or `prevSlice` is unavoidable. ### Signatures ```ts function selectAtom( anAtom: Atom, selector: (v: Value, prevSlice?: Slice) => Slice, equalityFn: (a: Slice, b: Slice) => boolean = Object.is, ): Atom ``` This function creates a derived atom whose value is a function of the original atom's value, determined by `selector.` The selector function runs whenever the original atom changes; it updates the derived atom only if `equalityFn` reports that the derived value has changed. By default, `equalityFn` is reference equality, but you can supply your favorite deep-equals function to stabilize the derived value where necessary. ### Examples ```js const defaultPerson = { name: { first: 'Jane', last: 'Doe', }, birth: { year: 2000, month: 'Jan', day: 1, time: { hour: 1, minute: 1, }, }, } // Original atom. const personAtom = atom(defaultPerson) // Tracks person.name. Updated when person.name object changes, even // if neither name.first nor name.last actually change. const nameAtom = selectAtom(personAtom, (person) => person.name) // Tracks person.birth. Updated when year, month, day, hour, or minute changes. // Use of deepEquals means that this atom doesn't update if birth field is // replaced with a new object containing the same data. E.g., if person is re-read // from a database. const birthAtom = selectAtom(personAtom, (person) => person.birth, deepEquals) ``` ### Hold stable references As always, to prevent an infinite loop when using `useAtom` in render cycle, you must provide `useAtom` a stable reference of your atoms. For `selectAtom`, we need **both** the base atom and the selector to be stable. ```js const [value] = useAtom(selectAtom(atom(0), (val) => val)) // So this will cause an infinite loop ``` You have multiple options in order to satisfy these constraints: ```js const baseAtom = atom(0) // Stable const baseSelector = (v) => v // Stable const Component = () => { // Solution 1: Memoize the whole resulting atom with "useMemo" const [value] = useAtom(useMemo(() => selectAtom(baseAtom, (v) => v), [])) // Solution 2: Memoize the inline callback with "useCallback" const [value] = useAtom( selectAtom( baseAtom, useCallback((v) => v, []), ), ) // Solution 3: All constraints are already satisfied const [value] = useAtom(selectAtom(baseAtom, baseSelector)) } ``` ### Stackblitz ================================================ FILE: docs/utilities/split.mdx ================================================ --- title: Split nav: 3.99 keywords: select published: false --- ## splitAtom The `splitAtom` utility is designed for scenarios where you need to obtain an atom for each element in a list. It operates on read/write atoms containing a list, returning an atom that holds a list of atoms, each corresponding to an item in the original list. ### Signature A simplified type signature for `splitAtom` would be: ```ts type SplitAtom = ( arrayAtom: PrimitiveAtom>, keyExtractor?: (item: Item) => Key ): Atom>> ``` ### Key Features 1. The returned atom contains a dispatch function in the `write` direction (since v1.6.4), providing a simple way to modify the original atom with actions like `remove`, `insert`, and `move`. 2. An optional `keyExtractor` function can be provided as a second argument to enhance stability and performance. ### Key Extractor The `splitAtom` utility supports a second argument which is a key extractor function: ```ts export function splitAtom( arrAtom: WritableAtom | Atom, keyExtractor?: (item: Item) => Key, ) ``` Important considerations for the `keyExtractor`: - If `splitAtom` is used within a React render loop, the `keyExtractor` must be a stable function that maintains object equality (shallow equality). This requirement doesn't apply outside of render loops. - Providing a `keyExtractor` enhances the stability of the atom output (which makes memoization hit cache more often). It prevents unnecessary subatom recreation due to index shifts in the source array. - `keyExtractor` is an optional optimization that should only be used if the extracted key is guaranteed unique for each item in the array. ### Example Usage Here's an example demonstrating the use of `splitAtom`: ```tsx import { Provider, atom, useAtom, PrimitiveAtom } from 'jotai' import { splitAtom } from 'jotai/utils' import './styles.css' const initialState = [ { task: 'help the town', done: false, }, { task: 'feed the dragon', done: false, }, ] const todosAtom = atom(initialState) const todoAtomsAtom = splitAtom(todosAtom) type TodoType = (typeof initialState)[number] const TodoItem = ({ todoAtom, remove, }: { todoAtom: PrimitiveAtom remove: () => void }) => { const [todo, setTodo] = useAtom(todoAtom) return (
{ setTodo((oldValue) => ({ ...oldValue, task: e.target.value })) }} /> { setTodo((oldValue) => ({ ...oldValue, done: !oldValue.done })) }} />
) } const TodoList = () => { const [todoAtoms, dispatch] = useAtom(todoAtomsAtom) return (
    {todoAtoms.map((todoAtom) => ( dispatch({ type: 'remove', atom: todoAtom })} /> ))}
) } const App = () => ( ) export default App ``` This example demonstrates how to use `splitAtom` to manage a list of todo items, allowing individual manipulation of each item while maintaining the overall list atom. ================================================ FILE: docs/utilities/ssr.mdx ================================================ --- title: SSR nav: 3.02 keywords: ssr,server,hydrate,hydration,next,nextjs,gatsby,remix,waku,framework --- ## useHydrateAtoms Ref: https://github.com/pmndrs/jotai/issues/340 ### Usage ```js import { atom, useAtom } from 'jotai' import { useHydrateAtoms } from 'jotai/utils' const countAtom = atom(0) const CounterPage = ({ countFromServer }) => { useHydrateAtoms([[countAtom, countFromServer]]) const [count] = useAtom(countAtom) // count would be the value of `countFromServer`, not 0. } ``` The primary use case for `useHydrateAtoms` are SSR apps like Next.js, where an initial value is e.g. fetched on the server, which can be passed to a component by props. ⚠️ Note: Although the term "hydrate" might suggest server-side usage, this hook is designed for client-side code and should be used with the [`'use client'` directive](https://react.dev/reference/rsc/use-client). ```ts // Definition function useHydrateAtoms( values: Iterable, unknown]>, options?: { store?: Store }, ): void ``` The hook takes an iterable of tuples containing `[atom, value]` as an argument and optional options. ```js // Usage with an array, specifying a store useHydrateAtoms( [ [countAtom, 42], [frameworkAtom, 'Next.js'], ], { store: myStore }, ) // Or with a map useHydrateAtoms(new Map([[count, 42]])) ``` Atoms can only be hydrated once per store. Therefore, if the initial value used is changed during rerenders, it won't update the atom value. If there is a unique need to re-hydrate a previously hydrated atom, pass the optional dangerouslyForceHydrate as true and note that it may behave wrongly in concurrent rendering. ```js useHydrateAtoms( [ [countAtom, 42], [frameworkAtom, 'Next.js'], ], { dangerouslyForceHydrate: true, }, ) ``` If there's a need to hydrate in multiple stores, use multiple `useHydrateAtoms` hooks to achieve that. ```js useHydrateAtoms([ [countAtom, 42], [frameworkAtom, 'Next.js'], ]) useHydrateAtoms( [ [countAtom, 17], [frameworkAtom, 'Gatsby'], ], { store: myStore }, ) ``` If you are using TypeScript with target `ES5`, you might need `as const` cast on the array to preserve the tuple type. ```ts useHydrateAtoms([ [countAtom, 42], [frameworkAtom, 'Next.js'], ] as const) ``` Or you may need to use a Map when passing the atom value to useHydrateAtoms. You can find a working example in the [Initializing State on Render docs](https://jotai.org/docs/guides/initialize-atom-on-render#using-typescript). ### Demo There's more examples in the [Next.js section](../guides/nextjs.mdx). ================================================ FILE: docs/utilities/storage.mdx ================================================ --- title: Storage nav: 3.01 keywords: storage,localstorage,sessionstorage,asyncstorage,persist,persistence --- ## atomWithStorage Ref: https://github.com/pmndrs/jotai/pull/394 ```jsx import { useAtom } from 'jotai' import { atomWithStorage } from 'jotai/utils' const darkModeAtom = atomWithStorage('darkMode', false) const Page = () => { const [darkMode, setDarkMode] = useAtom(darkModeAtom) return ( <>

Welcome to {darkMode ? 'dark' : 'light'} mode!

) } ``` The `atomWithStorage` function creates an atom with a value persisted in `localStorage` or `sessionStorage` for React or `AsyncStorage` for React Native. ### Parameters **key** (required): a unique string used as the key when syncing state with localStorage, sessionStorage, or AsyncStorage **initialValue** (required): the initial value of the atom **storage** (optional): an object with the following methods: - **getItem(key, initialValue)** (required): Reads an item from storage, or falls back to the `intialValue` - **setItem(key, value)** (required): Saves an item to storage - **removeItem(key)** (required): Deletes the item from storage - **subscribe(key, callback, initialValue)** (optional): A method which subscribes to external storage updates. **options** (optional): an object with the following properties: - **getOnInit** (optional, by default **false**): A boolean value indicating whether to get item from storage on initialization. Note that in an SPA with `getOnInit` either not set or `false` you will always get the initial value instead of the stored value on initialization. If the stored value is preferred set `getOnInit` to `true`. If not specified, the default storage implementation uses `localStorage` for storage/retrieval, `JSON.stringify()`/`JSON.parse()` for serialization/deserialization, and subscribes to `storage` events for cross-tab synchronization. ### `createJSONStorage` util To create a custom storage implementation with `JSON.stringify()`/`JSON.parse()` for the `storage` option, `createJSONStorage` util is provided. Usage: ```js const storage = createJSONStorage( // getStringStorage () => localStorage, // or sessionStorage, asyncStorage or alike // options (optional) { reviver, // optional reviver option for JSON.parse replacer, // optional replacer option for JSON.stringify }, ) ``` Note: `JSON.parse` is not type safe. If it can't accept any types, some kind of validation would be necessary for production apps. ### Server-side rendering Any JSX markup that depends on the value of a stored atom (e.g., a `className` or `style` prop) will use the `initialValue` when rendered on the server (since `localStorage` and `sessionStorage` are not available on the server). This means that there will be a mismatch between what is originally served to the user's browser as HTML and what is expected by React during the rehydration process if the user has a `storedValue` that differs from the `initialValue`. The suggested workaround for this issue is to only render the content dependent on the `storedValue` client-side by wrapping it in a [custom `` wrapper](https://www.joshwcomeau.com/react/the-perils-of-rehydration/#abstractions), which only renders after rehydration. Alternative solutions are technically possible, but would require a brief "flicker" as the `initialValue` is swapped to the `storedValue`, which can result in an unpleasant user experience, so this solution is advised. ### Deleting an item from storage For the case you want to delete an item from storage, the atom created with `atomWithStorage` accepts the `RESET` symbol on write. See the following example for the usage: ```jsx import { useAtom } from 'jotai' import { atomWithStorage, RESET } from 'jotai/utils' const textAtom = atomWithStorage('text', 'hello') const TextBox = () => { const [text, setText] = useAtom(textAtom) return ( <> setText(e.target.value)} /> ) } ``` If needed, you can also do conditional resets based on previous value. This can be particularly useful if you wish to clear keys in localStorage if previous values meet a condition. Below exemplifies this usage that clears the `visible` key whenever the previous value is `true`. ```jsx import { useAtom } from 'jotai' import { atomWithStorage, RESET } from 'jotai/utils' const isVisibleAtom = atomWithStorage('visible', false) const TextBox = () => { const [isVisible, setIsVisible] = useAtom(isVisibleAtom) return ( <> { isVisible &&

Header is visible!

} ) } ``` ### React-Native implementation You can use any library that implements `getItem`, `setItem` & `removeItem`. Let's say you would use the standard AsyncStorage provided by the community. ```js import { atomWithStorage, createJSONStorage } from 'jotai/utils' import AsyncStorage from '@react-native-async-storage/async-storage' const storage = createJSONStorage(() => AsyncStorage) const content = {} // anything JSON serializable const storedAtom = atomWithStorage('stored-key', content, storage) ``` **Note** set `getOnInit` to `true` if you want the atom to return the stored value immediately on initialization instead of the default value. **getOnInit true vs. false example**
```jsx // Assume localStorage already contains: { "symbol": "BTC_USDC" } // Without getOnInit (default behavior) const symbolAtom = atomWithStorage('symbol', 'SOL_USDC') function App() { const symbol = useAtomValue(symbolAtom) useEffect(() => { console.log('symbol', symbol) }, [symbol]) return
{symbol}
} // Console output WITHOUT getOnInit: // LOG "symbol" SOL_USDC (initial render) // LOG "symbol" BTC_USDC (after storage loads) // With getOnInit set to true const symbolAtom = atomWithStorage('symbol', 'SOL_USDC', undefined, { getOnInit: true, }) function App() { const symbol = useAtomValue(symbolAtom) useEffect(() => { console.log('symbol', symbol) }, [symbol]) return
{symbol}
} // Console output WITH getOnInit: // LOG "symbol" BTC_USDC (gets stored value immediately) ```
#### Notes with AsyncStorage (since v2.2.0) With AsyncStorage (as with any asynchronous storage), the atom value becomes async. When updating the atom by referencing the current value, then you'll need to `await` it. ```js const countAtom = atomWithStorage('count-key', 0, anyAsyncStorage) const Component = () => { const [count, setCount] = useAtom(countAtom) const increment = () => { setCount(async (promiseOrValue) => (await promiseOrValue) + 1) } // ... } ``` ### Validating stored values To add runtime validation to your storage atoms, you will need to create a custom implementation of storage. Below is an example that utilizes Zod to validate values stored in `localStorage` with cross-tab synchronization. ```js import { atomWithStorage } from 'jotai/utils' import { z } from 'zod' const myNumberSchema = z.number().int().nonnegative() const storedNumberAtom = atomWithStorage('my-number', 0, { getItem(key, initialValue) { const storedValue = localStorage.getItem(key) try { return myNumberSchema.parse(JSON.parse(storedValue ?? '')) } catch { return initialValue } }, setItem(key, value) { localStorage.setItem(key, JSON.stringify(value)) }, removeItem(key) { localStorage.removeItem(key) }, subscribe(key, callback, initialValue) { if ( typeof window === 'undefined' || typeof window.addEventListener === 'undefined' ) { return } const handler = (e) => { if (e.storageArea === localStorage && e.key === key) { let newValue try { newValue = myNumberSchema.parse(JSON.parse(e.newValue ?? '')) } catch { newValue = initialValue } callback(newValue) } } window.addEventListener('storage', handler) return () => window.removeEventListener('storage', handler) }, }) ``` We also have a new util `unstable_withStorageValidator` to simplify some cases. The above case would become: ```js import { atomWithStorage, createJSONStorage, unstable_withStorageValidator as withStorageValidator, } from 'jotai/utils' import { z } from 'zod' const myNumberSchema = z.number().int().nonnegative() const isMyNumber = (v) => myNumberSchema.safeParse(v).success const storedNumberAtom = atomWithStorage( 'my-number', 0, withStorageValidator(isMyNumber)(createJSONStorage()), ) ``` ================================================ FILE: eslint.config.mjs ================================================ import eslint from '@eslint/js' import vitest from '@vitest/eslint-plugin' import { defineConfig, globalIgnores } from 'eslint/config' import importPlugin from 'eslint-plugin-import' import jestDom from 'eslint-plugin-jest-dom' import react from 'eslint-plugin-react' import reactHooks from 'eslint-plugin-react-hooks' import testingLibrary from 'eslint-plugin-testing-library' import tseslint from 'typescript-eslint' export default defineConfig( globalIgnores(['dist/', 'examples/', 'website/', 'coverage/']), eslint.configs.recommended, importPlugin.flatConfigs.recommended, tseslint.configs.recommended, react.configs.flat.recommended, react.configs.flat['jsx-runtime'], reactHooks.configs.flat.recommended, { languageOptions: { parserOptions: { project: true, }, }, settings: { react: { version: 'detect', }, 'import/resolver': { typescript: true, }, }, rules: { eqeqeq: 'error', curly: ['warn', 'multi-line', 'consistent'], 'sort-imports': [ 'error', { ignoreDeclarationSort: true, }, ], 'import/no-unresolved': ['error', { commonjs: true, amd: true }], 'import/named': 'off', 'import/namespace': 'off', 'import/no-named-as-default-member': 'off', 'import/no-duplicates': 'error', 'import/extensions': ['error', 'always', { ignorePackages: true }], 'import/order': [ 'error', { alphabetize: { order: 'asc', caseInsensitive: true }, groups: [ 'builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', ], 'newlines-between': 'never', pathGroups: [ { pattern: 'react', group: 'builtin', position: 'before', }, ], pathGroupsExcludedImportTypes: ['builtin'], }, ], '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-unused-vars': [ 'error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }, ], }, }, { files: ['tests/**/*.{ts,tsx}'], ...testingLibrary.configs['flat/react'], rules: { 'testing-library/no-unnecessary-act': 'off', }, }, { files: ['tests/**/*.{ts,tsx}'], ...jestDom.configs['flat/recommended'], }, { files: ['tests/**/*.{ts,tsx}'], ...vitest.configs.recommended, settings: { vitest: { typecheck: true } }, }, { files: ['tests/**/*.{ts,tsx}'], rules: { 'import/extensions': ['error', 'never'], 'vitest/consistent-test-it': [ 'error', { fn: 'it', withinDescribe: 'it' }, ], }, }, { files: ['*.config.*'], languageOptions: { parserOptions: { project: null, }, }, }, ) ================================================ FILE: examples/hacker_news/README.md ================================================ # Hacker News [![Open in StackBlitz](https://img.shields.io/badge/Open%20in-StackBlitz-blue?style=flat-square&logo=stackblitz)](https://stackblitz.com/github/pmndrs/jotai/tree/main/examples/hacker_news) ## Description Demonstrate a news article with jotai, hit next to see more articles. ## Set up locally ```bash git clone https://github.com/pmndrs/jotai # install project dependencies & build the library cd jotai && pnpm install # move to the examples folder & install dependencies cd examples/hacker_news && pnpm install # start the dev server pnpm dev ``` ## Set up on `StackBlitz` Link: https://stackblitz.com/github/pmndrs/jotai/tree/main/examples/hacker_news ================================================ FILE: examples/hacker_news/index.html ================================================ Jotai Examples | Hacker News
================================================ FILE: examples/hacker_news/package.json ================================================ { "name": "hacker_news", "version": "2.0.0", "description": "Demonstrate a news articles with jotai, hit next to see more articles.", "type": "module", "scripts": { "dev": "vite", "build": "tsc && vite build", "serve": "vite preview" }, "dependencies": { "@react-spring/web": "^9.2.3", "html-react-parser": "^1.2.6", "jotai": "^2.10.4", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", "@vitejs/plugin-react": "^4.3.4", "typescript": "^5.0.0", "vite": "^6.0.5" } } ================================================ FILE: examples/hacker_news/public/index.html ================================================ Jotai Examples | Hacker News
================================================ FILE: examples/hacker_news/src/App.tsx ================================================ import { Suspense } from 'react' import { a, useSpring } from '@react-spring/web' import Parser from 'html-react-parser' import { Provider, atom, useAtom, useSetAtom } from 'jotai' type PostData = { by: string descendants?: number id: number kids?: number[] parent: number score?: number text?: string time: number title?: string type: 'comment' | 'story' url?: string } const postId = atom(9001) const postData = atom(async (get) => { const id = get(postId) const response = await fetch( `https://hacker-news.firebaseio.com/v0/item/${id}.json`, ) const data: PostData = await response.json() return data }) function Id() { const [id] = useAtom(postId) const props = useSpring({ from: { id }, id, reset: true }) return {props.id.to(Math.round)} } function Next() { // Use `useSetAtom` to avoid re-render // const [, setPostId] = useAtom(postId) const setPostId = useSetAtom(postId) return ( ) } function PostTitle() { const [{ by, text, time, title, url }] = useAtom(postData) return ( <>

{by}

{new Date(time * 1000).toLocaleDateString('en-US')}
{title &&

{title}

} {url && {url}} {text &&
{Parser(text)}
} ) } export default function App() { return (
Loading...

}> ) } ================================================ FILE: examples/hacker_news/src/index.tsx ================================================ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import './styles.css' import App from './App' const rootElement = document.getElementById('root') createRoot(rootElement!).render( , ) ================================================ FILE: examples/hacker_news/src/styles.css ================================================ @import url('https://rsms.me/inter/inter.css'); * { box-sizing: border-box; outline: none !important; } html, body, #root { width: 100%; height: 100%; margin: 0; padding: 0; } body { background: white; color: black; font-family: 'Inter', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } #root { display: grid; grid-template-columns: auto 1fr auto; } h1 { writing-mode: tb-rl; font-variant-numeric: tabular-nums; font-weight: 700; font-size: 10em; letter-spacing: -10px; text-align: left; margin: 0; padding: 50px 0px 0px 20px; } h2 { margin-bottom: 0.2em; } h4 { font-weight: 500; } h6 { margin-top: 0; } #root > div { padding: 50px 20px; overflow: hidden; word-wrap: break-word; position: relative; } #root > div > div { position: absolute; } p { color: #474747; } button { text-decoration: none; background: transparent; border: none; cursor: pointer; font-family: 'Inter', sans-serif; font-weight: 200; font-size: 6em; padding: 0px 30px 20px 0px; display: flex; align-items: flex-end; color: inherit; } button:focus { outline: 0; } a { color: inherit; } ================================================ FILE: examples/hacker_news/src/vite-env.d.ts ================================================ /// ================================================ FILE: examples/hacker_news/tsconfig.json ================================================ { "compilerOptions": { "target": "es2019", "strict": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "moduleResolution": "node", "lib": ["dom", "dom.iterable", "esnext"], "jsx": "react-jsx", "allowJs": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, "module": "esnext", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true }, "include": ["./src/**/*"], "exclude": ["node_modules"] } ================================================ FILE: examples/hacker_news/vite.config.ts ================================================ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], }) ================================================ FILE: examples/hello/README.md ================================================ # Hello [![Open in StackBlitz](https://img.shields.io/badge/Open%20in-StackBlitz-blue?style=flat-square&logo=stackblitz)](https://stackblitz.com/github/pmndrs/jotai/tree/main/examples/hello) ## Set up locally ```bash git clone https://github.com/pmndrs/jotai # install project dependencies & build the library cd jotai && pnpm install # move to the examples folder & install dependencies cd examples/hello && pnpm install # start the dev server pnpm dev ``` ## Set up on `StackBlitz` Link: https://stackblitz.com/github/pmndrs/jotai/tree/main/examples/hello ================================================ FILE: examples/hello/index.html ================================================ Jotai Examples | Hello
================================================ FILE: examples/hello/package.json ================================================ { "name": "hello", "version": "2.0.0", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "tsc && vite build", "serve": "vite preview" }, "dependencies": { "jotai": "^2.10.4", "prismjs": "^1.23.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-prism": "4.3.2" }, "devDependencies": { "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", "@vitejs/plugin-react": "^4.3.4", "typescript": "^5.0.0", "vite": "^6.0.5" } } ================================================ FILE: examples/hello/public/index.html ================================================ Jotai Examples | Hello
================================================ FILE: examples/hello/src/App.tsx ================================================ import { atom, useAtom } from 'jotai' // @ts-ignore import PrismCode from 'react-prism' import 'prismjs' import 'prismjs/components/prism-jsx.min' const textAtom = atom('hello') const uppercaseAtom = atom((get) => get(textAtom).toUpperCase()) const Input = () => { const [text, setText] = useAtom(textAtom) return ( setText(e.target.value)} /> ) } const Uppercase = () => { const [uppercase] = useAtom(uppercaseAtom) return <>{uppercase} } const code = `import { atom, useAtom } from 'jotai' // Create your atoms and derivatives const textAtom = atom('hello') const uppercaseAtom = atom((get) => get(textAtom).toUpperCase()) // Use them anywhere in your app const Input = () => { const [text, setText] = useAtom(textAtom) return setText(e.target.value)} /> } const Uppercase = () => { const [uppercase] = useAtom(uppercaseAtom) return
Uppercase: {uppercase}
} // Now you have the components const MyApp = () => (
) ` const App = () => (

A simple example:

) export default App ================================================ FILE: examples/hello/src/index.tsx ================================================ import { createRoot } from 'react-dom/client' import App from './App' import './prism.css' import './style.css' const root = document.getElementById('root') createRoot(root!).render(

Jōtai

Primitive and flexible state management for React.
状態

, ) ================================================ FILE: examples/hello/src/prism.css ================================================ /** * VS theme by Andrew Lock (https://andrewlock.net) * Inspired by Visual Studio syntax coloring */ code[class*='language-'], pre[class*='language-'] { color: #393a34; font-family: 'Consolas', 'Bitstream Vera Sans Mono', 'Courier New', Courier, monospace; direction: ltr; text-align: left; white-space: pre; word-spacing: normal; word-break: normal; font-size: 0.9em; line-height: 1.2em; -moz-tab-size: 4; -o-tab-size: 4; tab-size: 4; -webkit-hyphens: none; -moz-hyphens: none; -ms-hyphens: none; hyphens: none; } pre > code[class*='language-'] { font-size: 1em; } pre[class*='language-']::-moz-selection, pre[class*='language-'] ::-moz-selection, code[class*='language-']::-moz-selection, code[class*='language-'] ::-moz-selection { background: #c1def1; } pre[class*='language-']::selection, pre[class*='language-'] ::selection, code[class*='language-']::selection, code[class*='language-'] ::selection { background: #c1def1; } /* Code blocks */ pre[class*='language-'] { padding: 1em; margin: 0.5em 0; overflow: auto; border: 1px solid #dddddd; background-color: white; } /* Inline code */ :not(pre) > code[class*='language-'] { padding: 0.2em; padding-top: 1px; padding-bottom: 1px; background: #f8f8f8; border: 1px solid #dddddd; } .token.comment, .token.prolog, .token.doctype, .token.cdata { color: #b2b8c0; font-style: italic; } .token.namespace { opacity: 0.7; } .token.string { color: #67c2c7; } .token.punctuation, .token.operator { color: #393a34 !important; /* no highlight */ } .token.url, .token.symbol, .token.number, .token.boolean, .token.variable, .token.constant, .token.inserted { color: #36acaa; } .token.atrule, .token.keyword, .token.attr-value, .language-autohotkey .token.selector, .language-json .token.boolean, .language-json .token.number, code[class*='language-css'] { color: #9494ab; } .token.function { color: #ff7bab; } .token.deleted, .language-autohotkey .token.tag { color: #9a050f; } .token.selector, .language-autohotkey .token.keyword { color: #00009f; } .token.important, .token.bold { font-weight: bold; } .token.italic { font-style: italic; } .token.class-name, .language-json .token.property { color: #67c2c7; } .token.tag, .token.selector { color: #67c2c7; } .token.attr-name, .token.property, .token.regex, .token.entity { color: #91adbd; } .token.directive.tag .tag { background: #ffff00; color: #393a34; } /* overrides color-values for the Line Numbers plugin * http://prismjs.com/plugins/line-numbers/ */ .line-numbers .line-numbers-rows { border-right-color: #a5a5a5; } .line-numbers-rows > span:before { color: #2b91af; } /* overrides color-values for the Line Highlight plugin * http://prismjs.com/plugins/line-highlight/ */ .line-highlight { background: rgba(193, 222, 241, 0.2); background: -webkit-linear-gradient( left, rgba(193, 222, 241, 0.2) 70%, rgba(221, 222, 241, 0) ); background: linear-gradient( to right, rgba(193, 222, 241, 0.2) 70%, rgba(221, 222, 241, 0) ); } ================================================ FILE: examples/hello/src/style.css ================================================ @import url('https://rsms.me/inter/inter.css'); html { font-family: 'Inter', sans-serif; } @supports (font-variation-settings: normal) { html { font-family: 'Inter var', sans-serif; } } * { box-sizing: border-box; } ::selection { background: #212121; color: white; } html, body { overflow-x: hidden; } pre { font-size: 0.8em; margin-left: -2.5rem !important; margin-right: -2.5rem !important; width: calc(100% + 5rem); padding: 3em !important; border: 1px solid #eee !important; border-radius: 4px; } .src a * { opacity: 0.5; display: inline-block; margin: 10px 5px; } @media screen and (min-width: 800px) { pre { width: 100% !important; margin: 0 !important; } } pre > span:nth-child(11), pre > span:nth-child(17), pre > span:nth-child(20), pre > span:nth-child(23), pre > span:nth-child(44), pre > span:nth-child(61), pre > span:nth-child(78), pre > span:nth-child(79) > span > span.class-name, pre > span:nth-child(85) > span > span.class-name { color: #ff7bab !important; } ================================================ FILE: examples/hello/src/vite-env.d.ts ================================================ /// ================================================ FILE: examples/hello/tsconfig.json ================================================ { "compilerOptions": { "target": "es2019", "strict": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "moduleResolution": "node", "lib": ["dom", "dom.iterable", "esnext"], "jsx": "react-jsx", "allowJs": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, "module": "esnext", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true }, "include": ["./src/**/*"], "exclude": ["node_modules"] } ================================================ FILE: examples/hello/vite.config.ts ================================================ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], }) ================================================ FILE: examples/mega-form/README.md ================================================ # Mega form [![Open in StackBlitz](https://img.shields.io/badge/Open%20in-StackBlitz-blue?style=flat-square&logo=stackblitz)](https://stackblitz.com/github/pmndrs/jotai/tree/main/examples/mega-form) ## Set up locally ```bash git clone https://github.com/pmndrs/jotai # install project dependencies & build the library cd jotai && pnpm install # move to the examples folder & install dependencies cd examples/mega-form && pnpm install # start the dev server pnpm dev ``` ## Set up on `StackBlitz` Link: https://stackblitz.com/github/pmndrs/jotai/tree/main/examples/mega-form ================================================ FILE: examples/mega-form/index.html ================================================ Jotai Examples | Mega Form
================================================ FILE: examples/mega-form/package.json ================================================ { "name": "mega-form", "version": "1.0.0", "type": "module", "scripts": { "dev": "vite", "build": "tsc && vite build", "serve": "vite preview" }, "dependencies": { "fp-ts": "^2.9.5", "io-ts": "^2.2.15", "jotai": "^2.10.4", "jotai-optics": "^0.4.0", "optics-ts": "^2.0.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", "@vitejs/plugin-react": "^4.3.4", "typescript": "^5.0.0", "vite": "^6.0.5" } } ================================================ FILE: examples/mega-form/public/index.html ================================================ Jotai Examples | Mega Form
================================================ FILE: examples/mega-form/src/App.tsx ================================================ import { useCallback, useMemo } from 'react' import { Provider, atom, useAtom } from 'jotai' import type { PrimitiveAtom } from 'jotai' import { focusAtom } from 'jotai-optics' import { useAtomCallback } from 'jotai/utils' import initialValue from './initialValue' import useAtomSlice from './useAtomSlice' const Field = ({ fieldAtom, removeField, }: { fieldAtom: PrimitiveAtom<{ name: string; value: string }> removeField: () => void }) => { const nameAtom = useMemo( () => focusAtom(fieldAtom, (o) => o.prop('name')), [fieldAtom], ) const valueAtom = useMemo( () => focusAtom(fieldAtom, (o) => o.prop('value')), [fieldAtom], ) const [name, setName] = useAtom(nameAtom) const [value, setValue] = useAtom(valueAtom) return (
setName(e.target.value)} /> setValue(e.target.value)} />
) } const Form = ({ formAtom, nameAtom, remove, }: { formAtom: PrimitiveAtom> nameAtom: PrimitiveAtom remove: () => void }) => { const objectsAtom = useMemo( () => focusAtom(formAtom, (o) => o.iso( (bigObject) => Object.entries(bigObject).map(([name, value]) => ({ name, value, })), (arrayOfObjects) => Object.fromEntries( arrayOfObjects.map(({ name, value }) => [name, value]), ), ), ), [formAtom], ) const fieldAtoms = useAtomSlice(objectsAtom) const [name, setName] = useAtom(nameAtom) const addField = useAtomCallback( useCallback( (get, set) => { const id = Math.floor(Math.random() * 1000) set(objectsAtom, (oldValue) => [ ...oldValue, { name: `new field ${id}`, value: '' }, ]) }, [objectsAtom], ), ) return (
setName(e.target.value)} />
    {fieldAtoms.map(([fieldAtom, remove]) => (
  • ))}
) } const FormList = ({ formListAtom, }: { formListAtom: PrimitiveAtom>> }) => { const entriesAtom = useMemo( () => focusAtom(formListAtom, (o) => o.iso( (obj) => Object.entries(obj), (array) => Object.fromEntries(array), ), ), [formListAtom], ) const formAtoms = useAtomSlice(entriesAtom) const addForm = useAtomCallback( useCallback( (get, set) => { const id = Math.floor(Math.random() * 1000) set(entriesAtom, (oldValue) => [...oldValue, [`new form ${id}`, {}]]) }, [entriesAtom], ), ) const formValues = useMemo(() => { return formAtoms.map(([formEntryAtom, remove]) => ({ nameAtom: focusAtom(formEntryAtom, (o) => o.nth(0)), formAtom: focusAtom(formEntryAtom, (o) => o.nth(1)), remove, })) }, [formAtoms]) return (
    {formValues.map(({ nameAtom, formAtom, remove }) => (
  • ))}
) } const formListAtom = atom(initialValue) export default function App() { return (

Mega form

) } ================================================ FILE: examples/mega-form/src/index.tsx ================================================ import { createRoot } from 'react-dom/client' import App from './App' import './style.css' const rootElement = document.getElementById('root') createRoot(rootElement!).render() ================================================ FILE: examples/mega-form/src/initialValue.ts ================================================ const initialValue: Record> = { form1: { task: 'Eat some food', checked: 'yeah' }, form2: { task: 'Eat some food', checked: 'yeah' }, form3: { task: 'Eat some food', checked: 'yeah' }, form4: { task: 'Eat some food', checked: 'yeah' }, form5: { task: 'Eat some food', checked: 'yeah' }, form6: { task: 'Eat some food', checked: 'yeah' }, form7: { task: 'Eat some food', checked: 'yeah' }, form8: { task: 'Eat some food', checked: 'yeah' }, form12: { task: 'Eat some food', checked: 'yeah' }, form22: { task: 'Eat some food', checked: 'yeah' }, form32: { task: 'Eat some food', checked: 'yeah' }, form42: { task: 'Eat some food', checked: 'yeah' }, form52: { task: 'Eat some food', checked: 'yeah' }, form62: { task: 'Eat some food', checked: 'yeah' }, form72: { task: 'Eat some food', checked: 'yeah' }, form82: { task: 'Eat some food', checked: 'yeah' }, form14: { task: 'Eat some food', checked: 'yeah' }, form24: { task: 'Eat some food', checked: 'yeah' }, form34: { task: 'Eat some food', checked: 'yeah' }, form44: { task: 'Eat some food', checked: 'yeah' }, form54: { task: 'Eat some food', checked: 'yeah' }, form64: { task: 'Eat some food', checked: 'yeah' }, form74: { task: 'Eat some food', checked: 'yeah' }, form84: { task: 'Eat some food', checked: 'yeah' }, form15: { task: 'Eat some food', checked: 'yeah' }, form25: { task: 'Eat some food', checked: 'yeah' }, form35: { task: 'Eat some food', checked: 'yeah' }, form45: { task: 'Eat some food', checked: 'yeah' }, form55: { task: 'Eat some food', checked: 'yeah' }, form65: { task: 'Eat some food', checked: 'yeah' }, form75: { task: 'Eat some food', checked: 'yeah' }, form85: { task: 'Eat some food', checked: 'yeah' }, } export default initialValue ================================================ FILE: examples/mega-form/src/style.css ================================================ html, body { position: relative; width: 100%; height: 100%; } body { color: #333; margin: 0; padding: 8px; box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif; } a { color: rgb(0, 100, 200); text-decoration: none; } a:hover { text-decoration: underline; } a:visited { color: rgb(0, 80, 160); } label { display: block; } input, button, select, textarea { font-family: inherit; font-size: inherit; -webkit-padding: 0.4em 0; padding: 0.4em; margin: 0 0 0.5em 0; box-sizing: border-box; border: 1px solid #ccc; border-radius: 2px; } input:disabled { color: #ccc; } button { color: #333; background-color: #f4f4f4; outline: none; } button:disabled { color: #999; } button:not(:disabled):active { background-color: #ddd; } button:focus { border-color: #666; } ================================================ FILE: examples/mega-form/src/useAtomSlice.ts ================================================ import { useMemo } from 'react' import { useAtom } from 'jotai' import type { PrimitiveAtom } from 'jotai' import { splitAtom } from 'jotai/utils' const useAtomSlice = (arrAtom: PrimitiveAtom) => { const [atoms, dispatch] = useAtom( useMemo(() => splitAtom(arrAtom), [arrAtom]), ) return useMemo( () => atoms.map( (itemAtom) => [ itemAtom, () => dispatch({ type: 'remove', atom: itemAtom }), ] as const, ), [atoms, dispatch], ) } export default useAtomSlice ================================================ FILE: examples/mega-form/src/vite-env.d.ts ================================================ /// ================================================ FILE: examples/mega-form/tsconfig.json ================================================ { "compilerOptions": { "target": "es2019", "strict": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "moduleResolution": "node", "lib": ["dom", "dom.iterable", "esnext"], "jsx": "react-jsx", "allowJs": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, "module": "esnext", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true }, "include": ["./src/**/*"], "exclude": ["node_modules"] } ================================================ FILE: examples/mega-form/vite.config.ts ================================================ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], }) ================================================ FILE: examples/starter/README.md ================================================ # Starter [![Open in StackBlitz](https://img.shields.io/badge/Open%20in-StackBlitz-blue?style=flat-square&logo=stackblitz)](https://stackblitz.com/github/pmndrs/jotai/tree/main/examples/starter) ## Set up locally ```bash git clone https://github.com/pmndrs/jotai # install project dependencies & build the library cd jotai && pnpm install # move to the examples folder & install dependencies cd examples/starter && pnpm install # start the dev server pnpm dev ``` ## Set up on `StackBlitz` Link: https://stackblitz.com/github/pmndrs/jotai/tree/main/examples/starter ================================================ FILE: examples/starter/index.html ================================================ Jotai Examples | Starter
================================================ FILE: examples/starter/package.json ================================================ { "name": "starter", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "tsc && vite build", "preview": "vite preview" }, "dependencies": { "jotai": "^2.10.4", "react": "^18.3.1", "react-dom": "^18.3.1" }, "devDependencies": { "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", "@vitejs/plugin-react": "^4.3.4", "typescript": "^5.0.0", "vite": "^6.0.5" } } ================================================ FILE: examples/starter/src/index.css ================================================ html, body, #root { height: 100%; } #root { display: flex; place-items: center; justify-content: center; color: #fff; background-color: hsl(0, 0%, 4%); } ================================================ FILE: examples/starter/src/index.tsx ================================================ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import { atom, useAtom } from 'jotai' import mascot from './assets/jotai-mascot.png' import './index.css' const countAtom = atom(0) const Counter = () => { const [count, setCount] = useAtom(countAtom) const inc = () => setCount((c) => c + 1) return ( <> {count} ) } function App() { return (
Jotai mascot

Jotai Starter

) } createRoot(document.getElementById('root')!).render( , ) ================================================ FILE: examples/starter/src/vite-env.d.ts ================================================ /// ================================================ FILE: examples/starter/tsconfig.json ================================================ { "compilerOptions": { "target": "es2019", "strict": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "moduleResolution": "node", "lib": ["dom", "dom.iterable", "esnext"], "jsx": "react-jsx", "allowJs": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, "module": "esnext", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true }, "include": ["vite.config.ts", "./src/**/*"], "exclude": ["node_modules"] } ================================================ FILE: examples/starter/vite.config.ts ================================================ import react from '@vitejs/plugin-react' import { defineConfig } from 'vite' // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], }) ================================================ FILE: examples/text_length/README.md ================================================ # Text Length [![Open in StackBlitz](https://img.shields.io/badge/Open%20in-StackBlitz-blue?style=flat-square&logo=stackblitz)](https://stackblitz.com/github/pmndrs/jotai/tree/main/examples/text_length) ## Description Count the length and show the uppercase of any text. ## Set up locally ```bash git clone https://github.com/pmndrs/jotai # install project dependencies & build the library cd jotai && pnpm install # move to the examples folder & install dependencies cd examples/text_length && pnpm install # start the dev server pnpm dev ``` ## Set up on `StackBlitz` Link: https://stackblitz.com/github/pmndrs/jotai/tree/main/examples/text_length ================================================ FILE: examples/text_length/index.html ================================================ Jotai Examples | Text Length
REACT SPRING
Code
Castle
================================================ FILE: examples/text_length/package.json ================================================ { "name": "text_length", "version": "2.0.0", "description": "Count the length and show the uppercase of any text", "type": "module", "scripts": { "dev": "vite", "build": "tsc && vite build", "serve": "vite preview" }, "dependencies": { "jotai": "^2.10.4", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", "@vitejs/plugin-react": "^4.3.4", "typescript": "^5.0.0", "vite": "^6.0.5" } } ================================================ FILE: examples/text_length/public/index.html ================================================ Jotai Examples | Text Length
REACT SPRING
Code
Castle
================================================ FILE: examples/text_length/src/App.tsx ================================================ import { Provider, atom, useAtom } from 'jotai' const textAtom = atom('hello') const textLenAtom = atom((get) => get(textAtom).length) const uppercaseAtom = atom((get) => get(textAtom).toUpperCase()) const Input = () => { const [text, setText] = useAtom(textAtom) return setText(e.target.value)} /> } const CharCount = () => { const [len] = useAtom(textLenAtom) return
Length: {len}
} const Uppercase = () => { const [uppercase] = useAtom(uppercaseAtom) return
Uppercase: {uppercase}
} const App = () => ( ) export default App ================================================ FILE: examples/text_length/src/index.tsx ================================================ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import App from './App' const rootElement = document.getElementById('root') createRoot(rootElement!).render( , ) ================================================ FILE: examples/text_length/src/react-app-env.d.ts ================================================ /// ================================================ FILE: examples/text_length/tsconfig.json ================================================ { "compilerOptions": { "target": "es2019", "strict": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "moduleResolution": "node", "lib": ["dom", "dom.iterable", "esnext"], "jsx": "react-jsx", "allowJs": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, "module": "esnext", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true }, "include": ["./src/**/*"], "exclude": ["node_modules"] } ================================================ FILE: examples/text_length/vite.config.ts ================================================ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], }) ================================================ FILE: examples/todos/README.md ================================================ # Todos [![Open in StackBlitz](https://img.shields.io/badge/Open%20in-StackBlitz-blue?style=flat-square&logo=stackblitz)](https://stackblitz.com/github/pmndrs/jotai/tree/main/examples/todos) ## Description Record your todo list by typing them into this app, check them if you have completed the task, and switch between `Completed` and `Incompleted` to see the status of your task. ## Set up locally ```bash git clone https://github.com/pmndrs/jotai # install project dependencies & build the library cd jotai && pnpm install # move to the examples folder & install dependencies cd examples/todos && pnpm install # start the dev server pnpm dev ``` ## Set up on `StackBlitz` Link: https://stackblitz.com/github/pmndrs/jotai/tree/main/examples/todos ================================================ FILE: examples/todos/index.html ================================================ Jotai Examples | Todos
================================================ FILE: examples/todos/package.json ================================================ { "name": "todos", "version": "2.0.0", "description": "Record your todo list by typing them into this app", "type": "module", "scripts": { "dev": "vite", "build": "tsc && vite build", "serve": "vite preview" }, "dependencies": { "@ant-design/icons": "^5.5.2", "@react-spring/web": "^9.2.3", "antd": "^4.16.2", "jotai": "^2.10.4", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", "@vitejs/plugin-react": "^4.3.4", "typescript": "^5.0.0", "vite": "^6.0.5" } } ================================================ FILE: examples/todos/public/index.html ================================================ Jotai Examples | Todos
================================================ FILE: examples/todos/src/App.tsx ================================================ import type { FormEvent } from 'react' import { CloseOutlined } from '@ant-design/icons' import { a, useTransition } from '@react-spring/web' import { Radio } from 'antd' import { Provider, atom, useAtom, useSetAtom } from 'jotai' import type { PrimitiveAtom } from 'jotai' type Todo = { title: string completed: boolean } const filterAtom = atom('all') const todosAtom = atom[]>([]) const filteredAtom = atom[]>((get) => { const filter = get(filterAtom) const todos = get(todosAtom) if (filter === 'all') return todos else if (filter === 'completed') return todos.filter((atom) => get(atom).completed) else return todos.filter((atom) => !get(atom).completed) }) type RemoveFn = (item: PrimitiveAtom) => void type TodoItemProps = { atom: PrimitiveAtom remove: RemoveFn } const TodoItem = ({ atom, remove }: TodoItemProps) => { const [item, setItem] = useAtom(atom) const toggleCompleted = () => setItem((props) => ({ ...props, completed: !props.completed })) return ( <> {item.title} remove(atom)} /> ) } const Filter = () => { const [filter, set] = useAtom(filterAtom) return ( set(e.target.value)} value={filter}> All Completed Incompleted ) } type FilteredType = { remove: RemoveFn } const Filtered = (props: FilteredType) => { const [todos] = useAtom(filteredAtom) const transitions = useTransition(todos, { keys: (todo) => todo.toString(), from: { opacity: 0, height: 0 }, enter: { opacity: 1, height: 40 }, leave: { opacity: 0, height: 0 }, }) return transitions((style, atom) => ( )) } const TodoList = () => { // Use `useSetAtom` to avoid re-render // const [, setTodos] = useAtom(todosAtom) const setTodos = useSetAtom(todosAtom) const remove: RemoveFn = (todo) => setTodos((prev) => prev.filter((item) => item !== todo)) const add = (e: FormEvent) => { e.preventDefault() const title = e.currentTarget.inputTitle.value e.currentTarget.inputTitle.value = '' setTodos((prev) => [...prev, atom({ title, completed: false })]) } return ( ) } export default function App() { return (

Jōtai

) } ================================================ FILE: examples/todos/src/index.tsx ================================================ import { createRoot } from 'react-dom/client' import 'antd/dist/antd.css' import './styles.css' import App from './App' const rootElement = document.getElementById('root') createRoot(rootElement!).render() ================================================ FILE: examples/todos/src/styles.css ================================================ @import url('https://rsms.me/inter/inter.css'); * { box-sizing: border-box; } html, body { width: 100%; height: 100%; } body { margin-top: 5em; display: flex; align-items: flex-start; justify-content: center; background: #fdfdfd; font-family: 'Inter', sans-serif !important; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; filter: saturate(0); } #root { width: 50ch; display: flex; flex-direction: column; gap: 1em; } input:not([type='checkbox']) { width: 100%; border: none; box-shadow: 0px 15px 30px rgba(0, 0, 0, 0.05); padding: 10px 20px; margin-top: 2em; margin-bottom: 4em; background: white; } input:focus { outline: none; } .anticon-close { width: 32px !important; cursor: pointer; color: #c0c0c0; } .anticon-close:hover { color: #272730; } .item { position: relative; display: flex; width: 100%; align-items: center; justify-content: space-between; gap: 20px; overflow: hidden; } .item > span { display: inline-block; width: 100%; } h1 { font-size: 10em; font-weight: 800; margin: 0; padding: 0; letter-spacing: -5px; color: black; white-space: nowrap; } ================================================ FILE: examples/todos/src/vite-env.d.ts ================================================ /// ================================================ FILE: examples/todos/tsconfig.json ================================================ { "compilerOptions": { "target": "es2019", "strict": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "moduleResolution": "node", "lib": ["dom", "dom.iterable", "esnext"], "jsx": "react-jsx" }, "include": ["./src/**/*"], "exclude": ["node_modules"] } ================================================ FILE: examples/todos/vite.config.ts ================================================ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], }) ================================================ FILE: examples/todos_with_atomFamily/README.md ================================================ # Todos with atomFamily [![Open in StackBlitz](https://img.shields.io/badge/Open%20in-StackBlitz-blue?style=flat-square&logo=stackblitz)](https://stackblitz.com/github/pmndrs/jotai/tree/main/examples/todos_with_atomFamily) > **⚠️ Note:** `atomFamily` from `jotai/utils` is deprecated and will be removed in v3. For new projects, please use the [`jotai-family`](https://github.com/jotaijs/jotai-family) package instead. ## Description Implement a todo list using atomFamily and localStorage, you can store your todo list to localStorage by click `Save to localStorage`, then remove your todo list and restore them by click `Load from localStorage`. ## Set up locally ```bash git clone https://github.com/pmndrs/jotai # install project dependencies & build the library cd jotai && pnpm install # move to the examples folder & install dependencies cd examples/todos_with_atomFamily && pnpm install # start the dev server pnpm dev ``` ## Set up on `StackBlitz` Link: https://stackblitz.com/github/pmndrs/jotai/tree/main/examples/todos_with_atomFamily ================================================ FILE: examples/todos_with_atomFamily/index.html ================================================ Jotai Examples | Todos with atomFamily
================================================ FILE: examples/todos_with_atomFamily/package.json ================================================ { "name": "todos_with_atomFamily", "version": "2.0.0", "description": "Implement a todo list using atomFamily and localStorage", "type": "module", "scripts": { "dev": "vite", "build": "tsc && vite build", "serve": "vite preview" }, "dependencies": { "@ant-design/icons": "^5.5.2", "@react-spring/web": "^9.2.3", "antd": "^4.16.2", "jotai": "^2.10.4", "nanoid": "^3.1.23", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", "@vitejs/plugin-react": "^4.3.4", "typescript": "^5.0.0", "vite": "^6.0.5" } } ================================================ FILE: examples/todos_with_atomFamily/public/index.html ================================================ Jotai Examples | Todos with atomFamily
================================================ FILE: examples/todos_with_atomFamily/src/App.tsx ================================================ import type { FormEvent } from 'react' import { CloseOutlined } from '@ant-design/icons' import { a, useTransition } from '@react-spring/web' import { Radio } from 'antd' import { Provider, atom, useAtom, useSetAtom } from 'jotai' import { atomFamily } from 'jotai/utils' import { nanoid } from 'nanoid' type Param = { id: string; title?: string } const todoAtomFamily = atomFamily( (param: Param) => atom({ title: param.title || 'No title', completed: false }), (a: Param, b: Param) => a.id === b.id, ) const filterAtom = atom('all') const todosAtom = atom([]) const filteredAtom = atom((get) => { const filter = get(filterAtom) const todos = get(todosAtom) if (filter === 'all') return todos else if (filter === 'completed') return todos.filter((id) => get(todoAtomFamily({ id })).completed) else return todos.filter((id) => !get(todoAtomFamily({ id })).completed) }) const TodoItem = ({ id, remove, }: { id: string remove: (id: string) => void }) => { const [item, setItem] = useAtom(todoAtomFamily({ id })) const toggleCompleted = () => setItem({ ...item, completed: !item.completed }) return ( <> {item.title} remove(id)} /> ) } const Filter = () => { const [filter, set] = useAtom(filterAtom) return ( set(e.target.value)} value={filter}> All Completed Incompleted ) } const Filtered = ({ remove }: { remove: (id: string) => void }) => { const [todos] = useAtom(filteredAtom) const transitions = useTransition(todos, { keys: (id: string) => id, from: { opacity: 0, height: 0 }, enter: { opacity: 1, height: 40 }, leave: { opacity: 0, height: 0 }, }) return transitions((style, id) => ( )) } const TodoList = () => { // Use `useSetAtom` to avoid re-render // const [, setTodos] = useAtom(todosAtom) const setTodos = useSetAtom(todosAtom) const remove = (id: string) => { setTodos((prev) => prev.filter((item) => item !== id)) todoAtomFamily.remove({ id }) } const add = (e: FormEvent) => { e.preventDefault() const title = e.currentTarget.inputTitle.value e.currentTarget.inputTitle.value = '' const id = nanoid() todoAtomFamily({ id, title }) setTodos((prev) => [...prev, id]) } return (
) } type Action = | { type: 'serialize'; callback: (value: string) => void } | { type: 'deserialize'; value: string } const serializeAtom = atom(null, (get, set, action: Action) => { if (action.type === 'serialize') { const todos = get(todosAtom) const todoMap: Record = {} todos.forEach((id) => { todoMap[id] = get(todoAtomFamily({ id })) }) const obj = { todos, todoMap, filter: get(filterAtom), } action.callback(JSON.stringify(obj)) } else if (action.type === 'deserialize') { const obj = JSON.parse(action.value) // needs error handling and type checking set(filterAtom, obj.filter) obj.todos.forEach((id: string) => { const todo = obj.todoMap[id] set(todoAtomFamily({ id, ...todo }), todo) }) set(todosAtom, obj.todos) } }) const Persist = () => { const [, dispatch] = useAtom(serializeAtom) const save = () => { dispatch({ type: 'serialize', callback: (value) => { localStorage.setItem('serializedTodos', value) }, }) } const load = () => { const value = localStorage.getItem('serializedTodos') if (value) { dispatch({ type: 'deserialize', value }) } } return (
) } export default function App() { return (

Jōtai

Persist

) } ================================================ FILE: examples/todos_with_atomFamily/src/index.tsx ================================================ import { createRoot } from 'react-dom/client' import 'antd/dist/antd.css' import './styles.css' import App from './App' const rootElement = document.getElementById('root') createRoot(rootElement!).render() ================================================ FILE: examples/todos_with_atomFamily/src/styles.css ================================================ @import url('https://rsms.me/inter/inter.css'); * { box-sizing: border-box; } html, body { width: 100%; height: 100%; } body { margin-top: 5em; display: flex; align-items: flex-start; justify-content: center; background: #fdfdfd; font-family: 'Inter', sans-serif !important; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; filter: saturate(0); } #root { width: 50ch; display: flex; flex-direction: column; gap: 1em; } input:not([type='checkbox']) { width: 100%; border: none; box-shadow: 0px 15px 30px rgba(0, 0, 0, 0.05); padding: 10px 20px; margin-top: 2em; margin-bottom: 4em; background: white; } input:focus { outline: none; } .anticon-close { width: 32px !important; cursor: pointer; color: #c0c0c0; } .anticon-close:hover { color: #272730; } .item { position: relative; display: flex; width: 100%; align-items: center; justify-content: space-between; gap: 20px; overflow: hidden; } .item > span { display: inline-block; width: 100%; } h1 { font-size: 10em; font-weight: 800; margin: 0; padding: 0; letter-spacing: -5px; color: black; white-space: nowrap; } ================================================ FILE: examples/todos_with_atomFamily/src/vite-env.d.ts ================================================ /// ================================================ FILE: examples/todos_with_atomFamily/tsconfig.json ================================================ { "compilerOptions": { "target": "es2019", "strict": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "moduleResolution": "node", "lib": ["dom", "dom.iterable", "esnext"], "jsx": "react-jsx", "allowJs": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, "module": "esnext", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true }, "include": ["./src/**/*"], "exclude": ["node_modules"] } ================================================ FILE: examples/todos_with_atomFamily/vite.config.ts ================================================ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], }) ================================================ FILE: package.json ================================================ { "name": "jotai", "description": "👻 Primitive and flexible state management for React", "private": true, "type": "commonjs", "version": "2.18.1", "main": "./index.js", "types": "./index.d.ts", "typesVersions": { ">=4.8": { "esm/*": [ "esm/*" ], "*": [ "*" ] }, ">=3.8": { "esm/*": [ "ts3.8/*" ], "*": [ "ts3.8/*" ] }, "*": { "esm/*": [ "ts_version_3.8_and_above_is_required.d.ts" ], "*": [ "ts_version_3.8_and_above_is_required.d.ts" ] } }, "exports": { "./package.json": "./package.json", ".": { "react-native": { "types": "./index.d.ts", "default": "./index.js" }, "import": { "types": "./esm/index.d.mts", "default": "./esm/index.mjs" }, "default": { "types": "./index.d.ts", "default": "./index.js" } }, "./*": { "react-native": { "types": "./*.d.ts", "default": "./*.js" }, "import": { "types": "./esm/*.d.mts", "default": "./esm/*.mjs" }, "default": { "types": "./*.d.ts", "default": "./*.js" } } }, "files": [ "**" ], "sideEffects": false, "scripts": { "prebuild": "shx rm -rf dist", "build": "pnpm run prebuild && pnpm run \"/^build:.*/\" && pnpm run postbuild", "build-watch": "pnpm run \"/^build:.*/\" --watch", "build:base": "rollup -c", "build:utils": "rollup -c --config-utils", "build:babel:plugin-debug-label": "rollup -c --config-babel_plugin-debug-label", "build:babel:plugin-react-refresh": "rollup -c --config-babel_plugin-react-refresh", "build:babel:preset": "rollup -c --config-babel_preset", "build:vanilla": "rollup -c --config-vanilla", "build:vanilla:utils": "rollup -c --config-vanilla_utils", "build:vanilla:internals": "rollup -c --config-vanilla_internals", "build:react": "rollup -c --config-react --client-only", "build:react:utils": "rollup -c --config-react_utils --client-only", "postbuild": "pnpm run patch-d-ts && pnpm run copy && pnpm run patch-ts3.8 && pnpm run patch-old-ts && pnpm run patch-esm-ts && pnpm run patch-readme", "fix": "pnpm run fix:lint && pnpm run fix:format", "fix:format": "prettier . --write", "fix:lint": "eslint . --fix", "test": "pnpm run \"/^test:.*/\"", "test:format": "prettier . --list-different", "test:types": "tsc --noEmit", "test:lint": "eslint .", "test:spec": "vitest run", "patch-d-ts": "node --input-type=module -e \"import { entries } from './rollup.config.mjs'; import shelljs from 'shelljs'; const { find, sed } = shelljs; find('dist/**/*.d.ts').forEach(f => { entries.forEach(({ find, replacement }) => { sed('-i', new RegExp(' from \\'' + find.source.slice(0, -1) + '\\';$'), ' from \\'' + replacement + '\\';', f); }); sed('-i', / from '(\\.[^']+)\\.ts';$/, ' from \\'\\$1\\';', f); });\"", "copy": "shx cp -r dist/src/* dist/esm && shx cp -r dist/src/* dist && shx rm -rf dist/src && shx rm -rf dist/{src,tests} && downlevel-dts dist dist/ts3.8 --to=3.8 && shx cp package.json README.md LICENSE dist && json -I -f dist/package.json -e \"this.private=false; this.devDependencies=undefined; this.optionalDependencies=undefined; this.scripts=undefined; this.prettier=undefined;\"", "patch-ts3.8": "node -e \"require('shelljs').find('dist/ts3.8/**/*.d.ts').forEach(f=>require('fs').appendFileSync(f,'declare type Awaited = T extends Promise ? V : T;'))\"", "patch-old-ts": "shx touch dist/ts_version_3.8_and_above_is_required.d.ts", "patch-esm-ts": "node -e \"require('shelljs').find('dist/esm/**/*.d.ts').forEach(f=>{var f2=f.replace(/\\.ts$/,'.mts');require('fs').renameSync(f,f2);require('shelljs').sed('-i',/ from '(\\.[^']+)';$/,' from \\'\\$1.mjs\\';',f2);require('shelljs').sed('-i',/^declare module '(\\.[^']+)'/,'declare module \\'\\$1.mjs\\'',f2)})\"", "patch-readme": "shx sed -i 's/.*Jotai \\(dark mode\\).*//' dist/README.md" }, "engines": { "node": ">=12.20.0" }, "prettier": { "semi": false, "singleQuote": true }, "repository": { "type": "git", "url": "git+https://github.com/pmndrs/jotai.git" }, "keywords": [ "react", "state", "manager", "management", "recoil", "store" ], "author": "Daishi Kato", "contributors": [], "license": "MIT", "bugs": { "url": "https://github.com/pmndrs/jotai/issues" }, "homepage": "https://github.com/pmndrs/jotai", "packageManager": "pnpm@10.18.3", "devDependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx": "^7.28.6", "@babel/plugin-transform-typescript": "^7.28.6", "@babel/preset-env": "^7.29.0", "@babel/template": "^7.28.6", "@eslint/js": "^9.39.2", "@rollup/plugin-alias": "^6.0.0", "@rollup/plugin-babel": "^7.0.0", "@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-replace": "^6.0.3", "@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-typescript": "^12.3.0", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@types/babel__core": "^7.20.5", "@types/babel__template": "^7.4.4", "@types/node": "^25.3.5", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.4", "@vitest/coverage-v8": "^4.0.18", "@vitest/eslint-plugin": "^1.6.9", "@vitest/ui": "^4.0.18", "benny": "^3.7.1", "downlevel-dts": "^0.11.0", "esbuild": "^0.27.3", "eslint": "^9.39.2", "eslint-import-resolver-typescript": "^4.4.4", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jest-dom": "^5.5.0", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-testing-library": "^7.16.0", "jest-leak-detector": "30.2.0", "jsdom": "^28.1.0", "json": "^11.0.0", "prettier": "^3.8.1", "react": "^19.2.4", "react-dom": "^19.2.4", "rollup": "^4.59.0", "rollup-plugin-banner2": "^1.3.1", "rollup-plugin-esbuild": "^6.2.1", "rxjs": "^7.8.2", "shelljs": "^0.10.0", "shx": "^0.4.0", "typescript": "^5.9.3", "typescript-eslint": "^8.56.1", "vitest": "^4.0.18", "wonka": "^6.3.5" }, "peerDependencies": { "@babel/core": ">=7.0.0", "@babel/template": ">=7.0.0", "@types/react": ">=17.0.0", "react": ">=17.0.0" }, "peerDependenciesMeta": { "@babel/core": { "optional": true }, "@babel/template": { "optional": true }, "@types/react": { "optional": true }, "react": { "optional": true } } } ================================================ FILE: pnpm-workspace.yaml ================================================ packages: - . minimumReleaseAge: 1440 minimumReleaseAgeExclude: - 'react' - 'react-dom' - 'scheduler' ================================================ FILE: rollup.config.mjs ================================================ /*global process*/ import path from 'path' import alias from '@rollup/plugin-alias' import babelPlugin from '@rollup/plugin-babel' import resolve from '@rollup/plugin-node-resolve' import replace from '@rollup/plugin-replace' import terser from '@rollup/plugin-terser' import typescript from '@rollup/plugin-typescript' import banner2 from 'rollup-plugin-banner2' import esbuild from 'rollup-plugin-esbuild' import createBabelConfig from './babel.config.mjs' const extensions = ['.js', '.ts', '.tsx'] const { root } = path.parse(process.cwd()) export const entries = [ { find: /.*\/vanilla\/utils\.ts$/, replacement: 'jotai/vanilla/utils' }, { find: /.*\/internals\.ts$/, replacement: 'jotai/vanilla/internals' }, { find: /.*\/react\/utils\.ts$/, replacement: 'jotai/react/utils' }, { find: /.*\/vanilla\.ts$/, replacement: 'jotai/vanilla' }, { find: /.*\/react\.ts$/, replacement: 'jotai/react' }, ] function external(id) { return !id.startsWith('.') && !id.startsWith(root) } const cscComment = `'use client';\n` function getBabelOptions(targets) { return { ...createBabelConfig({ env: (env) => env === 'build' }, targets), extensions, comments: false, babelHelpers: 'bundled', } } function getEsbuild(env = 'development') { return esbuild({ minify: env === 'production', target: 'es2018', supported: { 'import-meta': true }, tsconfig: path.resolve('./tsconfig.json'), }) } function createDeclarationConfig(input, output) { return { input, output: { dir: output, }, external, plugins: [ typescript({ declaration: true, emitDeclarationOnly: true, outDir: output, }), ], } } function createESMConfig(input, output, clientOnly) { return { input, output: { file: output, format: 'esm' }, external, plugins: [ alias({ entries: entries.filter((entry) => !entry.find.test(input)) }), resolve({ extensions }), replace({ ...(output.endsWith('.js') ? { 'import.meta.env?.MODE': 'process.env.NODE_ENV', } : { 'import.meta.env?.MODE': '(import.meta.env ? import.meta.env.MODE : undefined)', }), delimiters: ['\\b', '\\b(?!(\\.|/))'], preventAssignment: true, }), getEsbuild(), banner2(() => clientOnly && cscComment), ], } } function createCommonJSConfig(input, output, clientOnly) { return { input, output: { file: `${output}.js`, format: 'cjs' }, external, plugins: [ alias({ entries: entries.filter((entry) => !entry.find.test(input)) }), resolve({ extensions }), replace({ 'import.meta.env?.MODE': 'process.env.NODE_ENV', delimiters: ['\\b', '\\b(?!(\\.|/))'], preventAssignment: true, }), babelPlugin(getBabelOptions({ ie: 11 })), banner2(() => clientOnly && cscComment), ], } } function createUMDConfig(input, output, env, clientOnly) { let name = 'jotai' const fileName = output.slice('dist/umd/'.length) const capitalize = (str) => str.slice(0, 1).toUpperCase() + str.slice(1) if (fileName !== 'index') { name += fileName.replace(/(\w+)\W*/g, (_, p) => capitalize(p)) } return { input, output: { file: `${output}.${env}.js`, format: 'umd', name, globals: { react: 'React', 'jotai/vanilla': 'jotaiVanilla', 'jotai/utils': 'jotaiUtils', 'jotai/react': 'jotaiReact', 'jotai/vanilla/utils': 'jotaiVanillaUtils', 'jotai/vanilla/internals': 'jotaiVanillaInternals', 'jotai/react/utils': 'jotaiReactUtils', }, }, external, plugins: [ alias({ entries: entries.filter((entry) => !entry.find.test(input)) }), resolve({ extensions }), replace({ 'import.meta.env?.MODE': JSON.stringify(env), delimiters: ['\\b', '\\b(?!(\\.|/))'], preventAssignment: true, }), babelPlugin(getBabelOptions({ ie: 11 })), banner2(() => clientOnly && cscComment), ...(env === 'production' ? [terser()] : []), ], } } function createSystemConfig(input, output, env, clientOnly) { return { input, output: { file: `${output}.${env}.js`, format: 'system', }, external, plugins: [ alias({ entries: entries.filter((entry) => !entry.find.test(input)) }), resolve({ extensions }), replace({ 'import.meta.env?.MODE': JSON.stringify(env), delimiters: ['\\b', '\\b(?!(\\.|/))'], preventAssignment: true, }), getEsbuild(env), banner2(() => clientOnly && cscComment), ], } } export default function (args) { let c = Object.keys(args).find((key) => key.startsWith('config-')) const clientOnly = Object.keys(args).some((key) => key === 'client-only') if (c) { c = c.slice('config-'.length).replace(/_/g, '/') } else { c = 'index' } return [ ...(c === 'index' ? [createDeclarationConfig(`src/${c}.ts`, 'dist')] : []), createCommonJSConfig(`src/${c}.ts`, `dist/${c}`, clientOnly), createESMConfig(`src/${c}.ts`, `dist/esm/${c}.mjs`, clientOnly), createUMDConfig(`src/${c}.ts`, `dist/umd/${c}`, 'development', clientOnly), createUMDConfig(`src/${c}.ts`, `dist/umd/${c}`, 'production', clientOnly), createSystemConfig( `src/${c}.ts`, `dist/system/${c}`, 'development', clientOnly, ), createSystemConfig( `src/${c}.ts`, `dist/system/${c}`, 'production', clientOnly, ), ] } ================================================ FILE: src/babel/plugin-debug-label.ts ================================================ import babel from '@babel/core' import type { PluginObj } from '@babel/core' import _templateBuilder from '@babel/template' import { isAtom } from './utils.ts' import type { PluginOptions } from './utils.ts' const templateBuilder = (_templateBuilder as any).default || _templateBuilder /** @deprecated Use `jotai-babel/plugin-debug-label` instead. */ export default function debugLabelPlugin( { types: t }: typeof babel, options?: PluginOptions, ): PluginObj { console.warn( '[DEPRECATED] jotai/babel/plugin-debug-label is deprecated and will be removed in v3.\n' + 'Please use the `jotai-babel` package instead: https://github.com/jotaijs/jotai-babel', ) return { visitor: { ExportDefaultDeclaration(nodePath, state) { const { node } = nodePath if ( t.isCallExpression(node.declaration) && isAtom(t, node.declaration.callee, options?.customAtomNames) ) { const filename = (state.filename || 'unknown').replace(/\.\w+$/, '') let displayName = filename.split('/').pop()! // ./{module name}/index.js if (displayName === 'index') { displayName = filename.slice(0, -'/index'.length).split('/').pop() || 'unknown' } // Relies on visiting the variable declaration to add the debugLabel const buildExport = templateBuilder(` const %%atomIdentifier%% = %%atom%%; export default %%atomIdentifier%% `) const ast = buildExport({ atomIdentifier: t.identifier(displayName), atom: node.declaration, }) nodePath.replaceWithMultiple(ast as babel.Node[]) } }, VariableDeclarator(path) { if ( t.isIdentifier(path.node.id) && t.isCallExpression(path.node.init) && isAtom(t, path.node.init.callee, options?.customAtomNames) ) { path.parentPath.insertAfter( t.expressionStatement( t.assignmentExpression( '=', t.memberExpression( t.identifier(path.node.id.name), t.identifier('debugLabel'), ), t.stringLiteral(path.node.id.name), ), ), ) } }, }, } } ================================================ FILE: src/babel/plugin-react-refresh.ts ================================================ import babel from '@babel/core' import type { PluginObj } from '@babel/core' import _templateBuilder from '@babel/template' import { isAtom } from './utils.ts' import type { PluginOptions } from './utils.ts' const templateBuilder = (_templateBuilder as any).default || _templateBuilder /** @deprecated Use `jotai-babel/plugin-react-refresh` instead. */ export default function reactRefreshPlugin( { types: t }: typeof babel, options?: PluginOptions, ): PluginObj { console.warn( '[DEPRECATED] jotai/babel/plugin-react-refresh is deprecated and will be removed in v3.\n' + 'Please use the `jotai-babel` package instead: https://github.com/jotaijs/jotai-babel', ) return { pre({ opts }) { if (!opts.filename) { throw new Error('Filename must be available') } }, visitor: { Program: { exit(path) { const jotaiAtomCache = templateBuilder(` globalThis.jotaiAtomCache = globalThis.jotaiAtomCache || { cache: new Map(), get(name, inst) { if (this.cache.has(name)) { return this.cache.get(name) } this.cache.set(name, inst) return inst }, }`)() path.unshiftContainer('body', jotaiAtomCache) }, }, ExportDefaultDeclaration(nodePath, state) { const { node } = nodePath if ( t.isCallExpression(node.declaration) && isAtom(t, node.declaration.callee, options?.customAtomNames) ) { const filename = state.filename || 'unknown' const atomKey = `${filename}/defaultExport` const buildExport = templateBuilder( `export default globalThis.jotaiAtomCache.get(%%atomKey%%, %%atom%%)`, ) const ast = buildExport({ atomKey: t.stringLiteral(atomKey), atom: node.declaration, }) nodePath.replaceWith(ast as babel.Node) } }, VariableDeclarator(nodePath, state) { if ( t.isIdentifier(nodePath.node.id) && t.isCallExpression(nodePath.node.init) && isAtom(t, nodePath.node.init.callee, options?.customAtomNames) && // Make sure atom declaration is in module scope (nodePath.parentPath.parentPath?.isProgram() || nodePath.parentPath.parentPath?.isExportNamedDeclaration()) ) { const filename = state.filename || 'unknown' const atomKey = `${filename}/${nodePath.node.id.name}` const buildAtomDeclaration = templateBuilder( `const %%atomIdentifier%% = globalThis.jotaiAtomCache.get(%%atomKey%%, %%atom%%)`, ) const ast = buildAtomDeclaration({ atomIdentifier: t.identifier(nodePath.node.id.name), atomKey: t.stringLiteral(atomKey), atom: nodePath.node.init, }) nodePath.parentPath.replaceWith(ast as babel.Node) } }, }, } } ================================================ FILE: src/babel/preset.ts ================================================ import babel from '@babel/core' import pluginDebugLabel from './plugin-debug-label.ts' import pluginReactRefresh from './plugin-react-refresh.ts' import type { PluginOptions } from './utils.ts' /** @deprecated Use `jotai-babel/preset` instead. */ export default function jotaiPreset( _: typeof babel, options?: PluginOptions, ): { plugins: babel.PluginItem[] } { console.warn( '[DEPRECATED] jotai/babel/preset is deprecated and will be removed in v3.\n' + 'Please use the `jotai-babel` package instead: https://github.com/jotaijs/jotai-babel', ) return { plugins: [ [pluginDebugLabel, options], [pluginReactRefresh, options], ], } } ================================================ FILE: src/babel/utils.ts ================================================ import { types } from '@babel/core' export interface PluginOptions { customAtomNames?: string[] } export function isAtom( t: typeof types, callee: babel.types.Expression | babel.types.V8IntrinsicIdentifier, customAtomNames: PluginOptions['customAtomNames'] = [], ): boolean { const atomNames = [...atomFunctionNames, ...customAtomNames] if (t.isIdentifier(callee) && atomNames.includes(callee.name)) { return true } if (t.isMemberExpression(callee)) { const { property } = callee if (t.isIdentifier(property) && atomNames.includes(property.name)) { return true } } return false } const atomFunctionNames = [ // Core 'atom', 'atomFamily', 'atomWithDefault', 'atomWithObservable', 'atomWithReducer', 'atomWithReset', 'atomWithStorage', 'freezeAtom', 'loadable', 'selectAtom', 'splitAtom', 'unwrap', // jotai-xstate 'atomWithMachine', // jotai-immer 'atomWithImmer', // jotai-valtio 'atomWithProxy', // jotai-trpc + jotai-relay 'atomWithQuery', 'atomWithMutation', 'atomWithSubscription', // jotai-redux + jotai-zustand 'atomWithStore', // jotai-location 'atomWithHash', 'atomWithLocation', // jotai-optics 'focusAtom', // jotai-form 'atomWithValidate', 'validateAtoms', // jotai-cache 'atomWithCache', // jotai-recoil 'atomWithRecoilValue', ] ================================================ FILE: src/index.ts ================================================ export * from './vanilla.ts' export * from './react.ts' ================================================ FILE: src/react/Provider.ts ================================================ import { createContext, createElement, useContext, useRef } from 'react' import type { FunctionComponent, ReactElement, ReactNode } from 'react' import { createStore, getDefaultStore } from '../vanilla.ts' type Store = ReturnType type StoreContextType = ReturnType> const StoreContext: StoreContextType = createContext( undefined, ) type Options = { store?: Store } export function useStore(options?: Options): Store { const store = useContext(StoreContext) return options?.store || store || getDefaultStore() } export function Provider({ children, store, }: { children?: ReactNode store?: Store }): ReactElement< { value: Store | undefined }, FunctionComponent<{ value: Store }> > { const storeRef = useRef(null) if (store) { return createElement(StoreContext.Provider, { value: store }, children) } if (storeRef.current === null) { storeRef.current = createStore() } return createElement( StoreContext.Provider, { // TODO: If this is not a false positive, consider using useState instead of useRef like https://github.com/pmndrs/jotai/pull/2771 // eslint-disable-next-line react-hooks/refs value: storeRef.current, }, children, ) } ================================================ FILE: src/react/useAtom.ts ================================================ import type { Atom, ExtractAtomArgs, ExtractAtomResult, ExtractAtomValue, PrimitiveAtom, SetStateAction, WritableAtom, } from '../vanilla.ts' import { useAtomValue } from './useAtomValue.ts' import { useSetAtom } from './useSetAtom.ts' type SetAtom = (...args: Args) => Result type Options = Parameters[1] export function useAtom( atom: WritableAtom, options?: Options, ): [Awaited, SetAtom] export function useAtom( atom: PrimitiveAtom, options?: Options, ): [Awaited, SetAtom<[SetStateAction], void>] export function useAtom( atom: Atom, options?: Options, ): [Awaited, never] export function useAtom< AtomType extends WritableAtom, >( atom: AtomType, options?: Options, ): [ Awaited>, SetAtom, ExtractAtomResult>, ] export function useAtom>( atom: AtomType, options?: Options, ): [Awaited>, never] export function useAtom( atom: Atom | WritableAtom, options?: Options, ) { return [ useAtomValue(atom, options), // We do wrong type assertion here, which results in throwing an error. useSetAtom(atom as WritableAtom, options), ] } ================================================ FILE: src/react/useAtomValue.ts ================================================ import React, { useDebugValue, useEffect, useReducer } from 'react' import { INTERNAL_getBuildingBlocksRev2 as INTERNAL_getBuildingBlocks } from '../vanilla/internals.ts' import type { Atom, ExtractAtomValue } from '../vanilla.ts' import { useStore } from './Provider.ts' type Store = ReturnType const isPromiseLike = (x: unknown): x is PromiseLike => typeof (x as any)?.then === 'function' const attachPromiseStatus = ( promise: PromiseLike & { status?: 'pending' | 'fulfilled' | 'rejected' value?: T reason?: unknown }, ) => { if (!promise.status) { promise.status = 'pending' promise.then( (v) => { promise.status = 'fulfilled' promise.value = v }, (e) => { promise.status = 'rejected' promise.reason = e }, ) } } const use = React.use || // A shim for older React versions (( promise: PromiseLike & { status?: 'pending' | 'fulfilled' | 'rejected' value?: T reason?: unknown }, ): T => { if (promise.status === 'pending') { throw promise } else if (promise.status === 'fulfilled') { return promise.value as T } else if (promise.status === 'rejected') { throw promise.reason } else { attachPromiseStatus(promise) throw promise } }) const continuablePromiseMap = new WeakMap< PromiseLike, Promise >() const createContinuablePromise = ( store: Store, promise: PromiseLike, getValue: () => PromiseLike | T, ) => { const buildingBlocks = INTERNAL_getBuildingBlocks(store) const registerAbortHandler = buildingBlocks[26] let continuablePromise = continuablePromiseMap.get(promise) if (!continuablePromise) { continuablePromise = new Promise((resolve, reject) => { let curr = promise const onFulfilled = (me: PromiseLike) => (v: T) => { if (curr === me) { resolve(v) } } const onRejected = (me: PromiseLike) => (e: unknown) => { if (curr === me) { reject(e) } } const onAbort = () => { try { const nextValue = getValue() if (isPromiseLike(nextValue)) { continuablePromiseMap.set(nextValue, continuablePromise!) curr = nextValue nextValue.then(onFulfilled(nextValue), onRejected(nextValue)) registerAbortHandler(store, nextValue, onAbort) } else { resolve(nextValue) } } catch (e) { reject(e) } } promise.then(onFulfilled(promise), onRejected(promise)) registerAbortHandler(store, promise, onAbort) }) continuablePromiseMap.set(promise, continuablePromise) } return continuablePromise } type Options = Parameters[0] & { delay?: number unstable_promiseStatus?: boolean } export function useAtomValue( atom: Atom, options?: Options, ): Awaited export function useAtomValue>( atom: AtomType, options?: Options, ): Awaited> export function useAtomValue(atom: Atom, options?: Options) { const { delay, unstable_promiseStatus: promiseStatus = !React.use } = options || {} const store = useStore(options) const [[valueFromReducer, storeFromReducer, atomFromReducer], rerender] = useReducer( (prev) => { const nextValue = store.get(atom) if ( Object.is(prev[0], nextValue) && prev[1] === store && prev[2] === atom ) { return prev } return [nextValue, store, atom] }, undefined, () => [store.get(atom), store, atom], ) let value = valueFromReducer if (storeFromReducer !== store || atomFromReducer !== atom) { rerender() value = store.get(atom) } useEffect(() => { const unsub = store.sub(atom, () => { if (promiseStatus) { try { const value = store.get(atom) if (isPromiseLike(value)) { attachPromiseStatus( createContinuablePromise(store, value, () => store.get(atom)), ) } } catch { // ignore } } if (typeof delay === 'number') { // delay rerendering to wait a promise possibly to resolve setTimeout(rerender, delay) return } rerender() }) rerender() return unsub }, [store, atom, delay, promiseStatus]) useDebugValue(value) if (isPromiseLike(value)) { const promise = createContinuablePromise(store, value, () => store.get(atom), ) if (promiseStatus) { attachPromiseStatus(promise) } return use(promise) } return value as Awaited } ================================================ FILE: src/react/useSetAtom.ts ================================================ import { useCallback } from 'react' import type { ExtractAtomArgs, ExtractAtomResult, WritableAtom, } from '../vanilla.ts' import { useStore } from './Provider.ts' type SetAtom = (...args: Args) => Result type Options = Parameters[0] export function useSetAtom( atom: WritableAtom, options?: Options, ): SetAtom export function useSetAtom< AtomType extends WritableAtom, >( atom: AtomType, options?: Options, ): SetAtom, ExtractAtomResult> export function useSetAtom( atom: WritableAtom, options?: Options, ) { const store = useStore(options) const setAtom = useCallback( (...args: Args) => { if (import.meta.env?.MODE !== 'production' && !('write' in atom)) { // useAtom can pass non writable atom with wrong type assertion, // so we should check here. throw new Error('not writable atom') } return store.set(atom, ...args) }, [store, atom], ) return setAtom } ================================================ FILE: src/react/utils/useAtomCallback.ts ================================================ import { useMemo } from 'react' import { useSetAtom } from '../../react.ts' import { atom } from '../../vanilla.ts' import type { Getter, Setter } from '../../vanilla.ts' type Options = Parameters[1] export function useAtomCallback( callback: (get: Getter, set: Setter, ...arg: Args) => Result, options?: Options, ): (...args: Args) => Result { const anAtom = useMemo( () => atom(null, (get, set, ...args: Args) => callback(get, set, ...args)), [callback], ) return useSetAtom(anAtom, options) } ================================================ FILE: src/react/utils/useHydrateAtoms.ts ================================================ import { useStore } from '../../react.ts' import { type WritableAtom } from '../../vanilla.ts' type Store = ReturnType type Options = Parameters[0] & { dangerouslyForceHydrate?: boolean } type AnyWritableAtom = WritableAtom type InferAtomTuples = { [K in keyof T]: T[K] extends readonly [infer A, ...infer Rest] ? A extends WritableAtom ? Rest extends Args ? readonly [A, ...Rest] : never : T[K] : never } // For internal use only // This can be changed without notice. export type INTERNAL_InferAtomTuples = InferAtomTuples const hydratedMap: WeakMap> = new WeakMap() export function useHydrateAtoms< T extends (readonly [AnyWritableAtom, ...unknown[]])[], >(values: InferAtomTuples, options?: Options): void export function useHydrateAtoms>( values: T, options?: Options, ): void export function useHydrateAtoms< T extends Iterable, >(values: InferAtomTuples, options?: Options): void export function useHydrateAtoms< T extends Iterable, >(values: T, options?: Options) { const store = useStore(options) const hydratedSet = getHydratedSet(store) for (const [atom, ...args] of values) { if (!hydratedSet.has(atom) || options?.dangerouslyForceHydrate) { hydratedSet.add(atom) store.set(atom, ...args) } } } const getHydratedSet = (store: Store) => { let hydratedSet = hydratedMap.get(store) if (!hydratedSet) { hydratedSet = new WeakSet() hydratedMap.set(store, hydratedSet) } return hydratedSet } ================================================ FILE: src/react/utils/useReducerAtom.ts ================================================ import { useCallback } from 'react' import { useAtom } from '../../react.ts' import type { PrimitiveAtom } from '../../vanilla.ts' type Options = Parameters[1] /** * @deprecated please use a recipe instead * https://github.com/pmndrs/jotai/pull/2467 */ export function useReducerAtom( anAtom: PrimitiveAtom, reducer: (v: Value, a?: Action) => Value, options?: Options, ): [Value, (action?: Action) => void] /** * @deprecated please use a recipe instead * https://github.com/pmndrs/jotai/pull/2467 */ export function useReducerAtom( anAtom: PrimitiveAtom, reducer: (v: Value, a: Action) => Value, options?: Options, ): [Value, (action: Action) => void] export function useReducerAtom( anAtom: PrimitiveAtom, reducer: (v: Value, a: Action) => Value, options?: Options, ) { if (import.meta.env?.MODE !== 'production') { console.warn( '[DEPRECATED] useReducerAtom is deprecated and will be removed in the future. Please create your own version using the recipe. https://github.com/pmndrs/jotai/pull/2467', ) } const [state, setState] = useAtom(anAtom, options) const dispatch = useCallback( (action: Action) => { setState((prev) => reducer(prev, action)) }, [setState, reducer], ) return [state, dispatch] } ================================================ FILE: src/react/utils/useResetAtom.ts ================================================ import { useCallback } from 'react' import { useSetAtom } from '../../react.ts' import { RESET } from '../../vanilla/utils.ts' import type { WritableAtom } from '../../vanilla.ts' type Options = Parameters[1] export function useResetAtom( anAtom: WritableAtom, options?: Options, ): () => T { const setAtom = useSetAtom(anAtom, options) const resetAtom = useCallback(() => setAtom(RESET), [setAtom]) return resetAtom } ================================================ FILE: src/react/utils.ts ================================================ export { useResetAtom } from './utils/useResetAtom.ts' export { useReducerAtom } from './utils/useReducerAtom.ts' export { useAtomCallback } from './utils/useAtomCallback.ts' export { useHydrateAtoms } from './utils/useHydrateAtoms.ts' ================================================ FILE: src/react.ts ================================================ export { Provider, useStore } from './react/Provider.ts' export { useAtomValue } from './react/useAtomValue.ts' export { useSetAtom } from './react/useSetAtom.ts' export { useAtom } from './react/useAtom.ts' ================================================ FILE: src/types.d.ts ================================================ declare interface ImportMeta { env?: { MODE: string } } ================================================ FILE: src/utils.ts ================================================ export * from './vanilla/utils.ts' export * from './react/utils.ts' ================================================ FILE: src/vanilla/atom.ts ================================================ import type { Store } from './store' type Getter = (atom: Atom) => Value type Setter = ( atom: WritableAtom, ...args: Args ) => Result type SetAtom = ( ...args: A ) => Result /** * setSelf is for internal use only and subject to change without notice. */ type Read = ( get: Getter, options: { readonly signal: AbortSignal; readonly setSelf: SetSelf }, ) => Value type Write = ( get: Getter, set: Setter, ...args: Args ) => Result // This is an internal type and not part of public API. // Do not depend on it as it can change without notice. type WithInitialValue = { init: Value } type OnUnmount = () => void type OnMount = < S extends SetAtom, >( setAtom: S, ) => OnUnmount | void export interface Atom { toString: () => string read: Read debugLabel?: string /** * To ONLY be used by Jotai libraries to mark atoms as private. Subject to change. * @private */ debugPrivate?: boolean /** * Fires after atom is referenced by the store for the first time * This is an internal API and subject to change without notice. */ INTERNAL_onInit?: (store: Store) => void } export interface WritableAtom< Value, Args extends unknown[], Result, > extends Atom { read: Read> write: Write onMount?: OnMount } type SetStateAction = Value | ((prev: Value) => Value) export type PrimitiveAtom = WritableAtom< Value, [SetStateAction], void > let keyCount = 0 // global key count for all atoms // writable derived atom export function atom( read: Read>, write: Write, ): WritableAtom // read-only derived atom export function atom(read: Read): Atom // write-only derived atom export function atom( initialValue: Value, write: Write, ): WritableAtom & WithInitialValue // primitive atom without initial value export function atom(): PrimitiveAtom & WithInitialValue // primitive atom export function atom( initialValue: Value, ): PrimitiveAtom & WithInitialValue export function atom( read?: Value | Read>, write?: Write, ) { const key = `atom${++keyCount}` const config = { toString() { return import.meta.env?.MODE !== 'production' && this.debugLabel ? key + ':' + this.debugLabel : key }, } as WritableAtom & { init?: Value | undefined } if (typeof read === 'function') { config.read = read as Read> } else { config.init = read config.read = defaultRead config.write = defaultWrite as unknown as Write } if (write) { config.write = write } return config } function defaultRead(this: Atom, get: Getter) { return get(this) } function defaultWrite( this: PrimitiveAtom, get: Getter, set: Setter, arg: SetStateAction, ) { return set( this, typeof arg === 'function' ? (arg as (prev: Value) => Value)(get(this)) : arg, ) } ================================================ FILE: src/vanilla/internals.ts ================================================ // Internal functions (subject to change without notice) // In case you rely on them, be sure to pin the version import type { Atom, WritableAtom } from './atom.ts' type AnyValue = unknown type AnyError = unknown type AnyAtom = Atom type AnyWritableAtom = WritableAtom type OnUnmount = () => void type Getter = Parameters[0] type Setter = Parameters[1] type EpochNumber = number /** * Mutable atom state, * tracked for both mounted and unmounted atoms in a store. * * This should be garbage collectable. * We can mutate it during atom read. (except for fields with TODO) */ type AtomState = { /** * Map of atoms that the atom depends on. * The map value is the epoch number of the dependency. */ readonly d: Map /** * Set of atoms with pending promise that depend on the atom. * * This may cause memory leaks, but it's for the capability to continue promises * TODO(daishi): revisit how to handle this */ readonly p: Set /** The epoch number of the atom. */ n: EpochNumber /** Atom value */ v?: Value /** Atom error */ e?: AnyError } /** * State tracked for mounted atoms. An atom is considered "mounted" if it has a * subscriber, or is a transitive dependency of another atom that has a * subscriber. * The mounted state of an atom is freed once it is no longer mounted. */ type Mounted = { /** Set of listeners to notify when the atom value changes. */ readonly l: Set<() => void> /** Set of mounted atoms that the atom depends on. */ readonly d: Set /** Set of mounted atoms that depends on the atom. */ readonly t: Set /** Function to run when the atom is unmounted. */ u?: () => void } type WeakMapLike = { get(key: K): V | undefined set(key: K, value: V): void has(key: K): boolean delete(key: K): boolean } type SetLike = { readonly size: number add(value: T): void has(value: T): boolean delete(value: T): boolean clear(): void forEach(callback: (value: T) => void): void [Symbol.iterator](): IterableIterator } type AtomStateMap = WeakMapLike type MountedMap = WeakMapLike type InvalidatedAtoms = WeakMapLike type ChangedAtoms = SetLike type Callbacks = SetLike<() => void> type AtomRead = ( store: Store, atom: Atom, ...params: Parameters['read']> ) => Value type AtomWrite = ( store: Store, atom: WritableAtom, ...params: Parameters['write']> ) => Result type AtomOnInit = (store: Store, atom: Atom) => void type AtomOnMount = ( store: Store, atom: WritableAtom, setAtom: (...args: Args) => Result, ) => OnUnmount | void type EnsureAtomState = ( store: Store, atom: Atom, ) => AtomState type FlushCallbacks = (store: Store) => void type RecomputeInvalidatedAtoms = (store: Store) => void type ReadAtomState = ( store: Store, atom: Atom, ) => AtomState type InvalidateDependents = (store: Store, atom: AnyAtom) => void type WriteAtomState = ( store: Store, atom: WritableAtom, ...args: Args ) => Result type MountDependencies = (store: Store, atom: AnyAtom) => void type MountAtom = (store: Store, atom: Atom) => Mounted type UnmountAtom = ( store: Store, atom: Atom, ) => Mounted | undefined type SetAtomStateValueOrPromise = ( store: Store, atom: Atom, valueOrPromise: Value, ) => void type StoreGet = (store: Store, atom: Atom) => Value type StoreSet = ( store: Store, atom: WritableAtom, ...args: Args ) => Result type StoreSub = ( store: Store, atom: AnyAtom, listener: () => void, ) => () => void type EnhanceBuildingBlocks = ( buildingBlocks: Readonly, ) => Readonly type AbortHandlersMap = WeakMapLike, Set<() => void>> type RegisterAbortHandler = ( store: Store, promise: PromiseLike, abortHandler: () => void, ) => void type AbortPromise = (store: Store, promise: PromiseLike) => void type Store = { get: (atom: Atom) => Value set: ( atom: WritableAtom, ...args: Args ) => Result sub: (atom: AnyAtom, listener: () => void) => () => void } type BuildingBlocks = [ // store state atomStateMap: AtomStateMap, // 0 mountedMap: MountedMap, // 1 invalidatedAtoms: InvalidatedAtoms, // 2 changedAtoms: ChangedAtoms, // 3 mountCallbacks: Callbacks, // 4 unmountCallbacks: Callbacks, // 5 storeHooks: StoreHooks, // 6 // atom interceptors atomRead: AtomRead, // 7 atomWrite: AtomWrite, // 8 atomOnInit: AtomOnInit, // 9 atomOnMount: AtomOnMount, // 10 // building-block functions ensureAtomState: EnsureAtomState, // 11 flushCallbacks: FlushCallbacks, // 12 recomputeInvalidatedAtoms: RecomputeInvalidatedAtoms, // 13 readAtomState: ReadAtomState, // 14 invalidateDependents: InvalidateDependents, // 15 writeAtomState: WriteAtomState, // 16 mountDependencies: MountDependencies, // 17 mountAtom: MountAtom, // 18 unmountAtom: UnmountAtom, // 19 setAtomStateValueOrPromise: SetAtomStateValueOrPromise, // 20 // store api storeGet: StoreGet, // 21 storeSet: StoreSet, // 22 storeSub: StoreSub, // 23 enhanceBuildingBlocks: EnhanceBuildingBlocks | undefined, // 24 // abortable promise support abortHandlersMap: AbortHandlersMap, // 25 registerAbortHandler: RegisterAbortHandler, // 26 abortPromise: AbortPromise, // 27 ] export type { AtomState as INTERNAL_AtomState, Mounted as INTERNAL_Mounted, AtomStateMap as INTERNAL_AtomStateMap, MountedMap as INTERNAL_MountedMap, InvalidatedAtoms as INTERNAL_InvalidatedAtoms, ChangedAtoms as INTERNAL_ChangedAtoms, Callbacks as INTERNAL_Callbacks, AtomRead as INTERNAL_AtomRead, AtomWrite as INTERNAL_AtomWrite, AtomOnInit as INTERNAL_AtomOnInit, AtomOnMount as INTERNAL_AtomOnMount, EnsureAtomState as INTERNAL_EnsureAtomState, FlushCallbacks as INTERNAL_FlushCallbacks, RecomputeInvalidatedAtoms as INTERNAL_RecomputeInvalidatedAtoms, ReadAtomState as INTERNAL_ReadAtomState, InvalidateDependents as INTERNAL_InvalidateDependents, WriteAtomState as INTERNAL_WriteAtomState, MountDependencies as INTERNAL_MountDependencies, MountAtom as INTERNAL_MountAtom, UnmountAtom as INTERNAL_UnmountAtom, Store as INTERNAL_Store, BuildingBlocks as INTERNAL_BuildingBlocks, StoreHooks as INTERNAL_StoreHooks, } // // Some util functions // function hasInitialValue>( atom: T, ): atom is T & (T extends Atom ? { init: Value } : never) { return 'init' in atom } function isActuallyWritableAtom(atom: AnyAtom): atom is AnyWritableAtom { return !!(atom as AnyWritableAtom).write } function isAtomStateInitialized(atomState: AtomState): boolean { return 'v' in atomState || 'e' in atomState } function returnAtomValue(atomState: AtomState): Value { if ('e' in atomState) { throw atomState.e } if (import.meta.env?.MODE !== 'production' && !('v' in atomState)) { throw new Error('[Bug] atom state is not initialized') } return atomState.v! } function isPromiseLike(p: unknown): p is PromiseLike { return typeof (p as any)?.then === 'function' } function addPendingPromiseToDependency( atom: AnyAtom, promise: PromiseLike, dependencyAtomState: AtomState, ): void { if (!dependencyAtomState.p.has(atom)) { dependencyAtomState.p.add(atom) const cleanup = () => dependencyAtomState.p.delete(atom) promise.then(cleanup, cleanup) } } function getMountedOrPendingDependents( atom: AnyAtom, atomState: AtomState, mountedMap: MountedMap, ): Iterable { const dependents = new Set() for (const a of mountedMap.get(atom)?.t || []) { dependents.add(a) } for (const atomWithPendingPromise of atomState.p) { dependents.add(atomWithPendingPromise) } return dependents } // // Store hooks // type StoreHook = { (): void add(callback: () => void): () => void } type StoreHookForAtoms = { (atom: AnyAtom): void add(atom: AnyAtom, callback: () => void): () => void add(atom: undefined, callback: (atom: AnyAtom) => void): () => void } /** StoreHooks are an experimental API. */ type StoreHooks = { /** Listener to notify when the atom state is created. */ readonly i?: StoreHookForAtoms /** Listener to notify when the atom is read. */ readonly r?: StoreHookForAtoms /** Listener to notify when the atom value is changed. */ readonly c?: StoreHookForAtoms /** Listener to notify when the atom is mounted. */ readonly m?: StoreHookForAtoms /** Listener to notify when the atom is unmounted. */ readonly u?: StoreHookForAtoms /** Listener to notify when callbacks are being flushed. */ readonly f?: StoreHook } const createStoreHook = (): StoreHook => { const callbacks = new Set<() => void>() const notify = () => callbacks.forEach((fn) => fn()) notify.add = (fn: () => void) => { callbacks.add(fn) return () => callbacks.delete(fn) } return notify } const createStoreHookForAtoms = (): StoreHookForAtoms => { const all: object = {} const callbacks = new WeakMap< AnyAtom | typeof all, Set<(atom?: AnyAtom) => void> >() const notify = (atom: AnyAtom) => { callbacks.get(all)?.forEach((fn) => fn(atom)) callbacks.get(atom)?.forEach((fn) => fn()) } notify.add = (atom: AnyAtom | undefined, fn: (atom?: AnyAtom) => void) => { const key = atom || all let fns = callbacks.get(key) if (!fns) { fns = new Set() callbacks.set(key, fns) } fns.add(fn) return () => { fns!.delete(fn) if (!fns!.size) { callbacks.delete(key) } } } return notify as StoreHookForAtoms } function initializeStoreHooks(storeHooks: StoreHooks): Required { type SH = { -readonly [P in keyof StoreHooks]: StoreHooks[P] } ;(storeHooks as SH).i ||= createStoreHookForAtoms() ;(storeHooks as SH).r ||= createStoreHookForAtoms() ;(storeHooks as SH).c ||= createStoreHookForAtoms() ;(storeHooks as SH).m ||= createStoreHookForAtoms() ;(storeHooks as SH).u ||= createStoreHookForAtoms() ;(storeHooks as SH).f ||= createStoreHook() return storeHooks as Required } // // Main functions // const BUILDING_BLOCK_atomRead: AtomRead = (_store, atom, ...params) => atom.read(...params) const BUILDING_BLOCK_atomWrite: AtomWrite = (_store, atom, ...params) => atom.write(...params) const BUILDING_BLOCK_atomOnInit: AtomOnInit = (store, atom) => atom.INTERNAL_onInit?.(store) const BUILDING_BLOCK_atomOnMount: AtomOnMount = (_store, atom, setAtom) => atom.onMount?.(setAtom) const BUILDING_BLOCK_ensureAtomState: EnsureAtomState = (store, atom) => { const buildingBlocks = getInternalBuildingBlocks(store) const atomStateMap = buildingBlocks[0] const storeHooks = buildingBlocks[6] const atomOnInit = buildingBlocks[9] if (import.meta.env?.MODE !== 'production' && !atom) { throw new Error('Atom is undefined or null') } let atomState = atomStateMap.get(atom) if (!atomState) { atomState = { d: new Map(), p: new Set(), n: 0 } atomStateMap.set(atom, atomState) storeHooks.i?.(atom) atomOnInit?.(store, atom) } return atomState as never } const BUILDING_BLOCK_flushCallbacks: FlushCallbacks = (store) => { const buildingBlocks = getInternalBuildingBlocks(store) const mountedMap = buildingBlocks[1] const changedAtoms = buildingBlocks[3] const mountCallbacks = buildingBlocks[4] const unmountCallbacks = buildingBlocks[5] const storeHooks = buildingBlocks[6] const recomputeInvalidatedAtoms = buildingBlocks[13] const errors: unknown[] = [] const call = (fn: () => void) => { try { fn() } catch (e) { errors.push(e) } } do { if (storeHooks.f) { call(storeHooks.f) } const callbacks = new Set<() => void>() const add = callbacks.add.bind(callbacks) changedAtoms.forEach((atom) => mountedMap.get(atom)?.l.forEach(add)) changedAtoms.clear() unmountCallbacks.forEach(add) unmountCallbacks.clear() mountCallbacks.forEach(add) mountCallbacks.clear() callbacks.forEach(call) if (changedAtoms.size) { recomputeInvalidatedAtoms(store) } } while (changedAtoms.size || unmountCallbacks.size || mountCallbacks.size) if (errors.length) { throw new AggregateError(errors) } } const BUILDING_BLOCK_recomputeInvalidatedAtoms: RecomputeInvalidatedAtoms = ( store, ) => { const buildingBlocks = getInternalBuildingBlocks(store) const mountedMap = buildingBlocks[1] const invalidatedAtoms = buildingBlocks[2] const changedAtoms = buildingBlocks[3] const ensureAtomState = buildingBlocks[11] const readAtomState = buildingBlocks[14] const mountDependencies = buildingBlocks[17] // Step 1: traverse the dependency graph to build the topologically sorted atom list // We don't bother to check for cycles, which simplifies the algorithm. // This is a topological sort via depth-first search, slightly modified from // what's described here for simplicity and performance reasons: // https://en.wikipedia.org/wiki/Topological_sorting#Depth-first_search const topSortedReversed: [atom: AnyAtom, atomState: AtomState][] = [] const visiting = new WeakSet() const visited = new WeakSet() // Visit the root atom. This is the only atom in the dependency graph // without incoming edges, which is one reason we can simplify the algorithm const stack: AnyAtom[] = Array.from(changedAtoms) while (stack.length) { const a = stack[stack.length - 1]! const aState = ensureAtomState(store, a) if (visited.has(a)) { // All dependents have been processed, now process this atom stack.pop() continue } if (visiting.has(a)) { // The algorithm calls for pushing onto the front of the list. For // performance, we will simply push onto the end, and then will iterate in // reverse order later. if (invalidatedAtoms.get(a) === aState.n) { topSortedReversed.push([a, aState]) } else if ( import.meta.env?.MODE !== 'production' && invalidatedAtoms.has(a) ) { throw new Error('[Bug] invalidated atom exists') } // Atom has been visited but not yet processed visited.add(a) stack.pop() continue } visiting.add(a) // Push unvisited dependents onto the stack for (const d of getMountedOrPendingDependents(a, aState, mountedMap)) { if (!visiting.has(d)) { stack.push(d) } } } // Step 2: use the topSortedReversed atom list to recompute all affected atoms // Track what's changed, so that we can short circuit when possible for (let i = topSortedReversed.length - 1; i >= 0; --i) { const [a, aState] = topSortedReversed[i]! let hasChangedDeps = false for (const dep of aState.d.keys()) { if (dep !== a && changedAtoms.has(dep)) { hasChangedDeps = true break } } if (hasChangedDeps) { invalidatedAtoms.set(a, aState.n) readAtomState(store, a) mountDependencies(store, a) } invalidatedAtoms.delete(a) } } // Dev only const storeMutationSet = new WeakSet() const BUILDING_BLOCK_readAtomState: ReadAtomState = (store, atom) => { const buildingBlocks = getInternalBuildingBlocks(store) const mountedMap = buildingBlocks[1] const invalidatedAtoms = buildingBlocks[2] const changedAtoms = buildingBlocks[3] const storeHooks = buildingBlocks[6] const atomRead = buildingBlocks[7] const ensureAtomState = buildingBlocks[11] const flushCallbacks = buildingBlocks[12] const recomputeInvalidatedAtoms = buildingBlocks[13] const readAtomState = buildingBlocks[14] const writeAtomState = buildingBlocks[16] const mountDependencies = buildingBlocks[17] const setAtomStateValueOrPromise = buildingBlocks[20] const registerAbortHandler = buildingBlocks[26] const atomState = ensureAtomState(store, atom) // See if we can skip recomputing this atom. if (isAtomStateInitialized(atomState)) { // If the atom is mounted, we can use cached atom state. // because it should have been updated by dependencies. // We can't use the cache if the atom is invalidated. if (mountedMap.has(atom) && invalidatedAtoms.get(atom) !== atomState.n) { return atomState } // Otherwise, check if the dependencies have changed. // If all dependencies haven't changed, we can use the cache. let hasChangedDeps = false for (const [a, n] of atomState.d) { if (readAtomState(store, a).n !== n) { hasChangedDeps = true break } } if (!hasChangedDeps) { return atomState } } // Compute a new state for this atom. let isSync = true const prevDeps = new Set(atomState.d.keys()) const nextDeps = new Map() const pruneDependencies = () => { for (const a of prevDeps) { if (!nextDeps.has(a)) { atomState.d.delete(a) } } } const mountDependenciesIfAsync = () => { if (mountedMap.has(atom)) { // If changedAtoms is already populated, an outer recompute cycle will handle it const shouldRecompute = !changedAtoms.size mountDependencies(store, atom) if (shouldRecompute) { recomputeInvalidatedAtoms(store) flushCallbacks(store) } } } const getter = (a: Atom) => { if (a === (atom as AnyAtom)) { const aState = ensureAtomState(store, a) if (!isAtomStateInitialized(aState)) { if (hasInitialValue(a)) { setAtomStateValueOrPromise(store, a, a.init) } else { // NOTE invalid derived atoms can reach here throw new Error('no atom init') } } return returnAtomValue(aState) } // a !== atom const aState = readAtomState(store, a) try { return returnAtomValue(aState) } finally { nextDeps.set(a, aState.n) atomState.d.set(a, aState.n) if (isPromiseLike(atomState.v)) { addPendingPromiseToDependency(atom, atomState.v, aState) } if (mountedMap.has(atom)) { mountedMap.get(a)?.t.add(atom) } if (!isSync) { mountDependenciesIfAsync() } } } let controller: AbortController | undefined let setSelf: ((...args: unknown[]) => unknown) | undefined const options = { get signal() { if (!controller) { controller = new AbortController() } return controller.signal }, get setSelf() { if (import.meta.env?.MODE !== 'production') { // This is shown even before calling. It's a strong warning. console.warn( '[DEPRECATED] setSelf is deprecated and will be removed in v3.', ) } if ( import.meta.env?.MODE !== 'production' && !isActuallyWritableAtom(atom) ) { console.warn('setSelf function cannot be used with read-only atom') } if (!setSelf && isActuallyWritableAtom(atom)) { setSelf = (...args) => { if (import.meta.env?.MODE !== 'production' && isSync) { console.warn('setSelf function cannot be called in sync') } if (!isSync) { try { return writeAtomState(store, atom, ...args) } finally { recomputeInvalidatedAtoms(store) flushCallbacks(store) } } } } return setSelf }, } const prevEpochNumber = atomState.n const prevInvalidated = invalidatedAtoms.get(atom) === prevEpochNumber try { if (import.meta.env?.MODE !== 'production') { storeMutationSet.delete(store) } const valueOrPromise = atomRead(store, atom, getter, options as never) if (import.meta.env?.MODE !== 'production' && storeMutationSet.has(store)) { console.warn( 'Detected store mutation during atom read. This is not supported.', ) } setAtomStateValueOrPromise(store, atom, valueOrPromise) if (isPromiseLike(valueOrPromise)) { registerAbortHandler(store, valueOrPromise, () => controller?.abort()) const settle = () => { pruneDependencies() mountDependenciesIfAsync() } valueOrPromise.then(settle, settle) } else { pruneDependencies() } storeHooks.r?.(atom) return atomState } catch (error) { delete atomState.v atomState.e = error ++atomState.n return atomState } finally { isSync = false if (atomState.n !== prevEpochNumber && prevInvalidated) { invalidatedAtoms.set(atom, atomState.n) changedAtoms.add(atom) storeHooks.c?.(atom) } } } const BUILDING_BLOCK_invalidateDependents: InvalidateDependents = ( store, atom, ) => { const buildingBlocks = getInternalBuildingBlocks(store) const mountedMap = buildingBlocks[1] const invalidatedAtoms = buildingBlocks[2] const ensureAtomState = buildingBlocks[11] const stack: AnyAtom[] = [atom] while (stack.length) { const a = stack.pop()! const aState = ensureAtomState(store, a) for (const d of getMountedOrPendingDependents(a, aState, mountedMap)) { const dState = ensureAtomState(store, d) if (invalidatedAtoms.get(d) !== dState.n) { invalidatedAtoms.set(d, dState.n) stack.push(d) } } } } const BUILDING_BLOCK_writeAtomState: WriteAtomState = ( store, atom, ...args ) => { const buildingBlocks = getInternalBuildingBlocks(store) const changedAtoms = buildingBlocks[3] const storeHooks = buildingBlocks[6] const atomWrite = buildingBlocks[8] const ensureAtomState = buildingBlocks[11] const flushCallbacks = buildingBlocks[12] const recomputeInvalidatedAtoms = buildingBlocks[13] const readAtomState = buildingBlocks[14] const invalidateDependents = buildingBlocks[15] const writeAtomState = buildingBlocks[16] const mountDependencies = buildingBlocks[17] const setAtomStateValueOrPromise = buildingBlocks[20] let isSync = true const getter: Getter = (a: Atom) => returnAtomValue(readAtomState(store, a)) const setter: Setter = ( a: WritableAtom, ...args: As ) => { const aState = ensureAtomState(store, a) try { if (a === (atom as AnyAtom)) { if (!hasInitialValue(a)) { // NOTE technically possible but restricted as it may cause bugs throw new Error('atom not writable') } if (import.meta.env?.MODE !== 'production') { storeMutationSet.add(store) } const prevEpochNumber = aState.n const v = args[0] as V setAtomStateValueOrPromise(store, a, v) mountDependencies(store, a) if (prevEpochNumber !== aState.n) { changedAtoms.add(a) invalidateDependents(store, a) storeHooks.c?.(a) } return undefined as R } else { return writeAtomState(store, a, ...args) } } finally { if (!isSync) { recomputeInvalidatedAtoms(store) flushCallbacks(store) } } } try { return atomWrite(store, atom, getter, setter, ...args) } finally { isSync = false } } const BUILDING_BLOCK_mountDependencies: MountDependencies = (store, atom) => { const buildingBlocks = getInternalBuildingBlocks(store) const mountedMap = buildingBlocks[1] const changedAtoms = buildingBlocks[3] const storeHooks = buildingBlocks[6] const ensureAtomState = buildingBlocks[11] const invalidateDependents = buildingBlocks[15] const mountAtom = buildingBlocks[18] const unmountAtom = buildingBlocks[19] const atomState = ensureAtomState(store, atom) const mounted = mountedMap.get(atom) if (mounted) { for (const [a, n] of atomState.d) { if (!mounted.d.has(a)) { const aState = ensureAtomState(store, a) const aMounted = mountAtom(store, a) aMounted.t.add(atom) mounted.d.add(a) if (n !== aState.n) { changedAtoms.add(a) invalidateDependents(store, a) storeHooks.c?.(a) } } } for (const a of mounted.d) { if (!atomState.d.has(a)) { mounted.d.delete(a) const aMounted = unmountAtom(store, a) aMounted?.t.delete(atom) } } } } const BUILDING_BLOCK_mountAtom: MountAtom = (store, atom) => { const buildingBlocks = getInternalBuildingBlocks(store) const mountedMap = buildingBlocks[1] const mountCallbacks = buildingBlocks[4] const storeHooks = buildingBlocks[6] const atomOnMount = buildingBlocks[10] const ensureAtomState = buildingBlocks[11] const flushCallbacks = buildingBlocks[12] const recomputeInvalidatedAtoms = buildingBlocks[13] const readAtomState = buildingBlocks[14] const writeAtomState = buildingBlocks[16] const mountAtom = buildingBlocks[18] const atomState = ensureAtomState(store, atom) let mounted = mountedMap.get(atom) if (!mounted) { // recompute atom state readAtomState(store, atom) // mount dependencies first for (const a of atomState.d.keys()) { const aMounted = mountAtom(store, a) aMounted.t.add(atom) } // mount self mounted = { l: new Set(), d: new Set(atomState.d.keys()), t: new Set(), } mountedMap.set(atom, mounted) if (isActuallyWritableAtom(atom)) { const processOnMount = () => { let isSync = true const setAtom = (...args: unknown[]) => { try { return writeAtomState(store, atom, ...args) } finally { if (!isSync) { recomputeInvalidatedAtoms(store) flushCallbacks(store) } } } try { const onUnmount = atomOnMount(store, atom, setAtom) if (onUnmount) { mounted!.u = () => { isSync = true try { onUnmount() } finally { isSync = false } } } } finally { isSync = false } } mountCallbacks.add(processOnMount) } storeHooks.m?.(atom) } return mounted } const BUILDING_BLOCK_unmountAtom: UnmountAtom = (store, atom) => { const buildingBlocks = getInternalBuildingBlocks(store) const mountedMap = buildingBlocks[1] const unmountCallbacks = buildingBlocks[5] const storeHooks = buildingBlocks[6] const ensureAtomState = buildingBlocks[11] const unmountAtom = buildingBlocks[19] const atomState = ensureAtomState(store, atom) let mounted = mountedMap.get(atom) if (!mounted || mounted.l.size) { return mounted } let isDependent = false for (const a of mounted.t) { if (mountedMap.get(a)?.d.has(atom)) { isDependent = true break } } if (!isDependent) { // unmount self if (mounted.u) { unmountCallbacks.add(mounted.u) } mounted = undefined mountedMap.delete(atom) // unmount dependencies for (const a of atomState.d.keys()) { const aMounted = unmountAtom(store, a) aMounted?.t.delete(atom) } storeHooks.u?.(atom) return undefined } return mounted } const BUILDING_BLOCK_setAtomStateValueOrPromise: SetAtomStateValueOrPromise = ( store, atom, valueOrPromise, ) => { const buildingBlocks = getInternalBuildingBlocks(store) const ensureAtomState = buildingBlocks[11] const abortPromise = buildingBlocks[27] const atomState = ensureAtomState(store, atom) const hasPrevValue = 'v' in atomState const prevValue = atomState.v if (isPromiseLike(valueOrPromise)) { for (const a of atomState.d.keys()) { addPendingPromiseToDependency( atom, valueOrPromise, ensureAtomState(store, a), ) } } atomState.v = valueOrPromise delete atomState.e if (!hasPrevValue || !Object.is(prevValue, atomState.v)) { ++atomState.n if (isPromiseLike(prevValue)) { abortPromise(store, prevValue) } } } const BUILDING_BLOCK_storeGet: StoreGet = (store, atom) => { const readAtomState = getInternalBuildingBlocks(store)[14] return returnAtomValue(readAtomState(store, atom)) as any } const BUILDING_BLOCK_storeSet: StoreSet = (store, atom, ...args) => { const buildingBlocks = getInternalBuildingBlocks(store) const changedAtoms = buildingBlocks[3] const flushCallbacks = buildingBlocks[12] const recomputeInvalidatedAtoms = buildingBlocks[13] const writeAtomState = buildingBlocks[16] const prevChangedAtomsSize = changedAtoms.size try { return writeAtomState(store, atom, ...args) as any } finally { if (changedAtoms.size !== prevChangedAtomsSize) { recomputeInvalidatedAtoms(store) flushCallbacks(store) } } } const BUILDING_BLOCK_storeSub: StoreSub = (store, atom, listener) => { const buildingBlocks = getInternalBuildingBlocks(store) const flushCallbacks = buildingBlocks[12] const mountAtom = buildingBlocks[18] const unmountAtom = buildingBlocks[19] const mounted = mountAtom(store, atom) const listeners = mounted.l listeners.add(listener) flushCallbacks(store) return () => { listeners.delete(listener) unmountAtom(store, atom) flushCallbacks(store) } } const BUILDING_BLOCK_registerAbortHandler: RegisterAbortHandler = ( store, promise, abortHandler, ) => { const buildingBlocks = getInternalBuildingBlocks(store) const abortHandlersMap = buildingBlocks[25] let abortHandlers = abortHandlersMap.get(promise) if (!abortHandlers) { abortHandlers = new Set() abortHandlersMap.set(promise, abortHandlers) const cleanup = () => abortHandlersMap.delete(promise) promise.then(cleanup, cleanup) } abortHandlers.add(abortHandler) } const BUILDING_BLOCK_abortPromise: AbortPromise = (store, promise) => { const buildingBlocks = getInternalBuildingBlocks(store) const abortHandlersMap = buildingBlocks[25] const abortHandlers = abortHandlersMap.get(promise) abortHandlers?.forEach((fn) => fn()) } const buildingBlockMap = new WeakMap>() const getInternalBuildingBlocks = (store: Store): Readonly => { const buildingBlocks = buildingBlockMap.get(store)! if (import.meta.env?.MODE !== 'production' && !buildingBlocks) { throw new Error( 'Store must be created by buildStore to read its building blocks', ) } return buildingBlocks } function getBuildingBlocks(store: Store): Readonly { const buildingBlocks = getInternalBuildingBlocks(store) const enhanceBuildingBlocks = buildingBlocks[24] if (enhanceBuildingBlocks) { return enhanceBuildingBlocks(buildingBlocks) } return buildingBlocks } function buildStore(...buildArgs: Partial): Store { const store = { get(atom) { const storeGet = getInternalBuildingBlocks(store)[21] return storeGet(store, atom) }, set(atom, ...args) { const storeSet = getInternalBuildingBlocks(store)[22] return storeSet(store, atom, ...args) }, sub(atom, listener) { const storeSub = getInternalBuildingBlocks(store)[23] return storeSub(store, atom, listener) }, } as Store const buildingBlocks = ( [ // store state new WeakMap(), // atomStateMap new WeakMap(), // mountedMap new WeakMap(), // invalidatedAtoms new Set(), // changedAtoms new Set(), // mountCallbacks new Set(), // unmountCallbacks {}, // storeHooks // atom interceptors BUILDING_BLOCK_atomRead, BUILDING_BLOCK_atomWrite, BUILDING_BLOCK_atomOnInit, BUILDING_BLOCK_atomOnMount, // building-block functions BUILDING_BLOCK_ensureAtomState, BUILDING_BLOCK_flushCallbacks, BUILDING_BLOCK_recomputeInvalidatedAtoms, BUILDING_BLOCK_readAtomState, BUILDING_BLOCK_invalidateDependents, BUILDING_BLOCK_writeAtomState, BUILDING_BLOCK_mountDependencies, BUILDING_BLOCK_mountAtom, BUILDING_BLOCK_unmountAtom, BUILDING_BLOCK_setAtomStateValueOrPromise, BUILDING_BLOCK_storeGet, BUILDING_BLOCK_storeSet, BUILDING_BLOCK_storeSub, undefined, // abortable promise support new WeakMap(), // abortHandlersMap BUILDING_BLOCK_registerAbortHandler, BUILDING_BLOCK_abortPromise, ] satisfies BuildingBlocks ).map((fn, i) => buildArgs[i] || fn) as BuildingBlocks buildingBlockMap.set(store, Object.freeze(buildingBlocks)) return store } export { // // Export internal functions // buildStore as INTERNAL_buildStoreRev2, getBuildingBlocks as INTERNAL_getBuildingBlocksRev2, initializeStoreHooks as INTERNAL_initializeStoreHooksRev2, // // Still experimental and some of them will be gone soon // hasInitialValue as INTERNAL_hasInitialValue, isActuallyWritableAtom as INTERNAL_isActuallyWritableAtom, isAtomStateInitialized as INTERNAL_isAtomStateInitialized, returnAtomValue as INTERNAL_returnAtomValue, isPromiseLike as INTERNAL_isPromiseLike, addPendingPromiseToDependency as INTERNAL_addPendingPromiseToDependency, getMountedOrPendingDependents as INTERNAL_getMountedOrPendingDependents, } ================================================ FILE: src/vanilla/store.ts ================================================ import { INTERNAL_buildStoreRev2 as INTERNAL_buildStore } from './internals.ts' import type { INTERNAL_Store } from './internals.ts' export type Store = INTERNAL_Store let overriddenCreateStore: typeof createStore | undefined export function INTERNAL_overrideCreateStore( fn: (prev: typeof createStore | undefined) => typeof createStore, ): void { overriddenCreateStore = fn(overriddenCreateStore) } export function createStore(): Store { if (overriddenCreateStore) { return overriddenCreateStore() } return INTERNAL_buildStore() } let defaultStore: Store | undefined export function getDefaultStore(): Store { if (!defaultStore) { defaultStore = createStore() if (import.meta.env?.MODE !== 'production') { ;(globalThis as any).__JOTAI_DEFAULT_STORE__ ||= defaultStore if ((globalThis as any).__JOTAI_DEFAULT_STORE__ !== defaultStore) { console.warn( 'Detected multiple Jotai instances. It may cause unexpected behavior with the default store. https://github.com/pmndrs/jotai/discussions/2044', ) } } } return defaultStore } ================================================ FILE: src/vanilla/typeUtils.ts ================================================ import type { Atom, PrimitiveAtom, WritableAtom } from './atom.ts' export type Getter = Parameters['read']>[0] export type Setter = Parameters< WritableAtom['write'] >[1] export type ExtractAtomValue = AtomType extends Atom ? Value : never export type ExtractAtomArgs = AtomType extends WritableAtom ? Args : never export type ExtractAtomResult = AtomType extends WritableAtom ? Result : never export type SetStateAction = ExtractAtomArgs>[0] ================================================ FILE: src/vanilla/utils/atomFamily.ts ================================================ import { type Atom } from '../../vanilla.ts' /** * in milliseconds */ type CreatedAt = number type ShouldRemove = (createdAt: CreatedAt, param: Param) => boolean type Cleanup = () => void type Callback = (event: { type: 'CREATE' | 'REMOVE' param: Param atom: AtomType }) => void /** * @deprecated atomFamily is deprecated and will be removed in v3. * Please use the `jotai-family` package instead: https://github.com/jotaijs/jotai-family * * Install: `npm install jotai-family` * * Migration: * ```ts * // Before * import { atomFamily } from 'jotai/utils' * * // After * import { atomFamily } from 'jotai-family' * ``` */ export interface AtomFamily { (param: Param): AtomType getParams(): Iterable remove(param: Param): void setShouldRemove(shouldRemove: ShouldRemove | null): void /** * fires when an atom is created or removed * This API is for advanced use cases, and can change without notice. */ unstable_listen(callback: Callback): Cleanup } let didWarnDeprecation = false /** * @deprecated atomFamily is deprecated and will be removed in v3. * Please use the `jotai-family` package instead: https://github.com/jotaijs/jotai-family * * Install: `npm install jotai-family` * * Migration: * ```ts * // Before * import { atomFamily } from 'jotai/utils' * * // After * import { atomFamily } from 'jotai-family' * ``` */ export function atomFamily>( initializeAtom: (param: Param) => AtomType, areEqual?: (a: Param, b: Param) => boolean, ): AtomFamily export function atomFamily>( initializeAtom: (param: Param) => AtomType, areEqual?: (a: Param, b: Param) => boolean, ) { if (import.meta.env?.MODE !== 'production' && !didWarnDeprecation) { console.warn( '[DEPRECATED] atomFamily is deprecated and will be removed in v3. ' + 'Please use the `jotai-family` package instead: https://github.com/jotaijs/jotai-family', ) didWarnDeprecation = true } let shouldRemove: ShouldRemove | null = null const atoms: Map = new Map() const listeners = new Set>() const createAtom = (param: Param) => { let item: [AtomType, CreatedAt] | undefined if (areEqual === undefined) { item = atoms.get(param) } else { // Custom comparator, iterate over all elements for (const [key, value] of atoms) { if (areEqual(key, param)) { item = value break } } } if (item !== undefined) { if (shouldRemove?.(item[1], param)) { createAtom.remove(param) } else { return item[0] } } const newAtom = initializeAtom(param) atoms.set(param, [newAtom, Date.now()]) notifyListeners('CREATE', param, newAtom) return newAtom } const notifyListeners = ( type: 'CREATE' | 'REMOVE', param: Param, atom: AtomType, ) => { for (const listener of listeners) { listener({ type, param, atom }) } } createAtom.unstable_listen = (callback: Callback) => { listeners.add(callback) return () => { listeners.delete(callback) } } createAtom.getParams = () => atoms.keys() createAtom.remove = (param: Param) => { if (areEqual === undefined) { if (!atoms.has(param)) return const [atom] = atoms.get(param)! atoms.delete(param) notifyListeners('REMOVE', param, atom) } else { for (const [key, [atom]] of atoms) { if (areEqual(key, param)) { atoms.delete(key) notifyListeners('REMOVE', key, atom) break } } } } createAtom.setShouldRemove = (fn: ShouldRemove | null) => { shouldRemove = fn if (!shouldRemove) return for (const [key, [atom, createdAt]] of atoms) { if (shouldRemove(createdAt, key)) { atoms.delete(key) notifyListeners('REMOVE', key, atom) } } } return createAtom } ================================================ FILE: src/vanilla/utils/atomWithDefault.ts ================================================ import { atom } from '../../vanilla.ts' import type { WritableAtom } from '../../vanilla.ts' import { RESET } from './constants.ts' type Read = WritableAtom< Value, Args, Result >['read'] type DefaultSetStateAction = | Value | typeof RESET | ((prev: Value) => Value | typeof RESET) export function atomWithDefault( getDefault: Read], void>, ): WritableAtom], void> { const EMPTY = Symbol() const overwrittenAtom = atom(EMPTY) if (import.meta.env?.MODE !== 'production') { overwrittenAtom.debugPrivate = true } const anAtom: WritableAtom], void> = atom( (get, options) => { const overwritten = get(overwrittenAtom) if (overwritten !== EMPTY) { return overwritten } return getDefault(get, options) }, (get, set, update) => { const newValue = typeof update === 'function' ? (update as (prev: Value) => Value)(get(anAtom)) : update set(overwrittenAtom, newValue === RESET ? EMPTY : newValue) }, ) return anAtom } ================================================ FILE: src/vanilla/utils/atomWithLazy.ts ================================================ import { atom } from '../../vanilla.ts' import type { PrimitiveAtom } from '../../vanilla.ts' export function atomWithLazy( makeInitial: () => Value, ): PrimitiveAtom { const a = atom(undefined as unknown as Value) delete (a as { init?: Value }).init Object.defineProperty(a, 'init', { get() { return makeInitial() }, }) return a } ================================================ FILE: src/vanilla/utils/atomWithObservable.ts ================================================ import { atom } from '../../vanilla.ts' import type { Atom, Getter, WritableAtom } from '../../vanilla.ts' const isPromiseLike = (x: unknown): x is PromiseLike => typeof (x as any)?.then === 'function' type Timeout = ReturnType type AnyError = unknown type Subscription = { unsubscribe: () => void } type Observer = { next: (value: T) => void error: (error: AnyError) => void complete: () => void } type SubscribableObservable = | { subscribe(observer: Observer): Subscription } | { subscribe(observer: Partial>): Subscription } | { subscribe(observer: Partial>): Subscription // Overload function to make typing happy subscribe(next: (value: T) => void): Subscription } type SymbolObservable = { [Symbol.observable]: () => SubscribableObservable } type ObservableLike = SubscribableObservable | SymbolObservable type SubjectLike = ObservableLike & Observer type Options = { initialValue?: Data | (() => Data) unstable_timeout?: number } type OptionsWithInitialValue = { initialValue: Data | (() => Data) unstable_timeout?: number } export function atomWithObservable( getObservable: (get: Getter) => SubjectLike, options: OptionsWithInitialValue, ): WritableAtom export function atomWithObservable( getObservable: (get: Getter) => SubjectLike, options?: Options, ): WritableAtom, [Data], void> export function atomWithObservable( getObservable: (get: Getter) => ObservableLike, options: OptionsWithInitialValue, ): Atom export function atomWithObservable( getObservable: (get: Getter) => ObservableLike, options?: Options, ): Atom> export function atomWithObservable( getObservable: (get: Getter) => ObservableLike | SubjectLike, options?: Options, ) { type Result = { d: Data } | { e: AnyError } const returnResultData = (result: Result) => { if ('e' in result) { throw result.e } return result.d } const observableResultAtom = atom((get) => { const observable = getObservable(get) const subscribable = (observable as Partial>)[Symbol.observable]?.() || (observable as SubscribableObservable) let resolve: ((result: Result) => void) | undefined const makePending = () => new Promise((r) => { resolve = r }) const initialResult: Result | Promise = options && 'initialValue' in options ? { d: typeof options.initialValue === 'function' ? (options.initialValue as () => Data)() : (options.initialValue as Data), } : makePending() let setResult: ((result: Result) => void) | undefined let lastResult: Result | undefined const listener = (result: Result) => { lastResult = result resolve?.(result) setResult?.(result) } let subscription: Subscription | undefined let timer: Timeout | undefined const isNotMounted = () => !setResult const unsubscribe = () => { if (subscription) { subscription.unsubscribe() subscription = undefined } } const start = () => { if (subscription) { clearTimeout(timer) subscription.unsubscribe() } subscription = subscribable.subscribe({ next: (d) => listener({ d }), error: (e) => listener({ e }), complete: () => {}, }) if (isNotMounted() && options?.unstable_timeout) { timer = setTimeout(unsubscribe, options.unstable_timeout) } } start() const resultAtom = atom(lastResult || initialResult) if (import.meta.env?.MODE !== 'production') { resultAtom.debugPrivate = true } resultAtom.onMount = (update) => { setResult = update if (lastResult) { update(lastResult) } if (subscription) { clearTimeout(timer) } else { start() } return () => { setResult = undefined if (options?.unstable_timeout) { timer = setTimeout(unsubscribe, options.unstable_timeout) } else { unsubscribe() } } } return [resultAtom, observable, makePending, start, isNotMounted] as const }) if (import.meta.env?.MODE !== 'production') { observableResultAtom.debugPrivate = true } const observableAtom = atom( (get) => { const [resultAtom] = get(observableResultAtom) const result = get(resultAtom) if (isPromiseLike(result)) { return result.then(returnResultData) } return returnResultData(result) }, (get, set, data: Data) => { const [resultAtom, observable, makePending, start, isNotMounted] = get(observableResultAtom) if ('next' in observable) { if (isNotMounted()) { set(resultAtom, makePending()) start() } observable.next(data) } else { throw new Error('observable is not subject') } }, ) return observableAtom } ================================================ FILE: src/vanilla/utils/atomWithReducer.ts ================================================ import { atom } from '../../vanilla.ts' import type { WritableAtom } from '../../vanilla.ts' export function atomWithReducer( initialValue: Value, reducer: (value: Value, action?: Action) => Value, ): WritableAtom export function atomWithReducer( initialValue: Value, reducer: (value: Value, action: Action) => Value, ): WritableAtom export function atomWithReducer( initialValue: Value, reducer: (value: Value, action: Action) => Value, ) { return atom(initialValue, function (this: never, get, set, action: Action) { set(this, reducer(get(this), action)) }) } ================================================ FILE: src/vanilla/utils/atomWithRefresh.ts ================================================ import { atom } from '../../vanilla.ts' import type { WritableAtom } from '../../vanilla.ts' type Read = WritableAtom< Value, Args, Result >['read'] type Write = WritableAtom< Value, Args, Result >['write'] export function atomWithRefresh( read: Read, write: Write, ): WritableAtom export function atomWithRefresh( read: Read, ): WritableAtom export function atomWithRefresh( read: Read, write?: Write, ) { const refreshAtom = atom(0) if (import.meta.env?.MODE !== 'production') { refreshAtom.debugPrivate = true } return atom( (get, options) => { get(refreshAtom) return read(get, options as never) }, (get, set, ...args: Args) => { if (args.length === 0) { set(refreshAtom, (c) => c + 1) } else if (write) { return write(get, set, ...args) } else if (import.meta.env?.MODE !== 'production') { throw new Error('refresh must be called without arguments') } }, ) } ================================================ FILE: src/vanilla/utils/atomWithReset.ts ================================================ import { atom } from '../../vanilla.ts' import type { WritableAtom } from '../../vanilla.ts' import { RESET } from './constants.ts' type SetStateActionWithReset = | Value | typeof RESET | ((prev: Value) => Value | typeof RESET) // This is an internal type and not part of public API. // Do not depend on it as it can change without notice. type WithInitialValue = { init: Value } export function atomWithReset( initialValue: Value, ): WritableAtom], void> & WithInitialValue { type Update = SetStateActionWithReset const anAtom = atom( initialValue, (get, set, update) => { const nextValue = typeof update === 'function' ? (update as (prev: Value) => Value | typeof RESET)(get(anAtom)) : update set(anAtom, nextValue === RESET ? initialValue : nextValue) }, ) return anAtom as WritableAtom & WithInitialValue } ================================================ FILE: src/vanilla/utils/atomWithStorage.ts ================================================ import { atom } from '../../vanilla.ts' import type { WritableAtom } from '../../vanilla.ts' import { RESET } from './constants.ts' const isPromiseLike = (x: unknown): x is PromiseLike => typeof (x as any)?.then === 'function' type Unsubscribe = () => void type Subscribe = ( key: string, callback: (value: Value) => void, initialValue: Value, ) => Unsubscribe | undefined type StringSubscribe = ( key: string, callback: (value: string | null) => void, ) => Unsubscribe | undefined type SetStateActionWithReset = | Value | typeof RESET | ((prev: Value) => Value | typeof RESET) export interface AsyncStorage { getItem: (key: string, initialValue: Value) => PromiseLike setItem: (key: string, newValue: Value) => PromiseLike removeItem: (key: string) => PromiseLike subscribe?: Subscribe } export interface SyncStorage { getItem: (key: string, initialValue: Value) => Value setItem: (key: string, newValue: Value) => void removeItem: (key: string) => void subscribe?: Subscribe } export interface AsyncStringStorage { getItem: (key: string) => PromiseLike setItem: (key: string, newValue: string) => PromiseLike removeItem: (key: string) => PromiseLike subscribe?: StringSubscribe } export interface SyncStringStorage { getItem: (key: string) => string | null setItem: (key: string, newValue: string) => void removeItem: (key: string) => void subscribe?: StringSubscribe } export function withStorageValidator( validator: (value: unknown) => value is Value, ): { (storage: AsyncStorage): AsyncStorage (storage: SyncStorage): SyncStorage } export function withStorageValidator( validator: (value: unknown) => value is Value, ) { return (unknownStorage: AsyncStorage | SyncStorage) => { const storage = { ...unknownStorage, getItem: (key: string, initialValue: Value) => { const validate = (value: unknown) => { if (!validator(value)) { return initialValue } return value } const value = unknownStorage.getItem(key, initialValue) if (isPromiseLike(value)) { return value.then(validate) } return validate(value) }, } return storage } } type JsonStorageOptions = { reviver?: (key: string, value: unknown) => unknown replacer?: (key: string, value: unknown) => unknown } export function createJSONStorage(): SyncStorage export function createJSONStorage( getStringStorage: () => AsyncStringStorage, options?: JsonStorageOptions, ): AsyncStorage export function createJSONStorage( getStringStorage: () => SyncStringStorage, options?: JsonStorageOptions, ): SyncStorage export function createJSONStorage( getStringStorage: () => | AsyncStringStorage | SyncStringStorage | undefined = () => { try { return window.localStorage } catch (e) { if (import.meta.env?.MODE !== 'production') { if (typeof window !== 'undefined') { console.warn(e) } } return undefined } }, options?: JsonStorageOptions, ): AsyncStorage | SyncStorage { let lastStr: string | undefined let lastValue: Value const storage: AsyncStorage | SyncStorage = { getItem: (key, initialValue) => { const parse = (str: string | null) => { str = str || '' if (lastStr !== str) { try { lastValue = JSON.parse(str, options?.reviver) } catch { return initialValue } lastStr = str } return lastValue } const str = getStringStorage()?.getItem(key) ?? null if (isPromiseLike(str)) { return str.then(parse) as never } return parse(str) as never }, setItem: (key, newValue) => getStringStorage()?.setItem( key, JSON.stringify(newValue, options?.replacer), ), removeItem: (key) => getStringStorage()?.removeItem(key), } const createHandleSubscribe = (subscriber: StringSubscribe): Subscribe => (key, callback, initialValue) => subscriber(key, (v) => { let newValue: Value try { newValue = JSON.parse(v || '') } catch { newValue = initialValue } callback(newValue) }) let subscriber: StringSubscribe | undefined try { subscriber = getStringStorage()?.subscribe } catch { // ignore } if ( !subscriber && typeof window !== 'undefined' && typeof window.addEventListener === 'function' && window.Storage ) { subscriber = (key, callback) => { if (!(getStringStorage() instanceof window.Storage)) { return () => {} } const storageEventCallback = (e: StorageEvent) => { if (e.storageArea === getStringStorage() && e.key === key) { callback(e.newValue) } } window.addEventListener('storage', storageEventCallback) return () => { window.removeEventListener('storage', storageEventCallback) } } } if (subscriber) { storage.subscribe = createHandleSubscribe(subscriber) } return storage } const defaultStorage = createJSONStorage() export function atomWithStorage( key: string, initialValue: Value, storage: AsyncStorage, options?: { getOnInit?: boolean }, ): WritableAtom< Value | Promise, [SetStateActionWithReset>], Promise > export function atomWithStorage( key: string, initialValue: Value, storage?: SyncStorage, options?: { getOnInit?: boolean }, ): WritableAtom], void> export function atomWithStorage( key: string, initialValue: Value, storage: | SyncStorage | AsyncStorage = defaultStorage as SyncStorage, options?: { getOnInit?: boolean }, ) { const getOnInit = options?.getOnInit const baseAtom = atom( getOnInit ? (storage.getItem(key, initialValue) as Value | Promise) : initialValue, ) if (import.meta.env?.MODE !== 'production') { baseAtom.debugPrivate = true } baseAtom.onMount = (setAtom) => { setAtom(storage.getItem(key, initialValue) as Value | Promise) return storage.subscribe?.(key, setAtom, initialValue) } const anAtom = atom( (get) => get(baseAtom), (get, set, update: SetStateActionWithReset>) => { const nextValue = typeof update === 'function' ? ( update as ( prev: Value | Promise, ) => Value | Promise | typeof RESET )(get(baseAtom)) : update if (nextValue === RESET) { set(baseAtom, initialValue) return storage.removeItem(key) } if (isPromiseLike(nextValue)) { return nextValue.then((resolvedValue) => { set(baseAtom, resolvedValue) return storage.setItem(key, resolvedValue) }) } set(baseAtom, nextValue) return storage.setItem(key, nextValue) }, ) return anAtom as never } ================================================ FILE: src/vanilla/utils/constants.ts ================================================ export const RESET: unique symbol = Symbol( import.meta.env?.MODE !== 'production' ? 'RESET' : '', ) ================================================ FILE: src/vanilla/utils/freezeAtom.ts ================================================ import type { Atom, WritableAtom } from '../../vanilla.ts' const frozenAtoms = new WeakSet>() const deepFreeze = (value: T): T => { if (typeof value !== 'object' || value === null) { return value } Object.freeze(value) const propNames = Object.getOwnPropertyNames(value) for (const name of propNames) { deepFreeze((value as never)[name]) } return value } export function freezeAtom>( anAtom: AtomType, ): AtomType export function freezeAtom( anAtom: WritableAtom, ): WritableAtom { if (frozenAtoms.has(anAtom)) { return anAtom } frozenAtoms.add(anAtom) const origRead = anAtom.read anAtom.read = function (get, options) { return deepFreeze(origRead.call(this, get, options)) } if ('write' in anAtom) { const origWrite = anAtom.write anAtom.write = function (get, set, ...args) { return origWrite.call( this, get, (...setArgs) => { if (setArgs[0] === anAtom) { setArgs[1] = deepFreeze(setArgs[1]) } return set(...setArgs) }, ...args, ) } } return anAtom } /** * @deprecated Define it on users end */ export function freezeAtomCreator< CreateAtom extends (...args: unknown[]) => Atom, >(createAtom: CreateAtom): CreateAtom { if (import.meta.env?.MODE !== 'production') { console.warn( '[DEPRECATED] freezeAtomCreator is deprecated, define it on users end', ) } return ((...args: unknown[]) => freezeAtom(createAtom(...args))) as never } ================================================ FILE: src/vanilla/utils/loadable.ts ================================================ import { atom } from '../../vanilla.ts' import type { Atom } from '../../vanilla.ts' import { unwrap } from './unwrap.ts' const cache1 = new WeakMap() const memo1 = (create: () => T, dep1: object): T => (cache1.has(dep1) ? cache1 : cache1.set(dep1, create())).get(dep1) export type Loadable = | { state: 'loading' } | { state: 'hasError'; error: unknown } | { state: 'hasData'; data: Awaited } let didWarnDeprecation = false /** * @deprecated `loadable` is deprecated infavor of `unwrap`. * * Userland implementation of loadable: * ```js * function loadable(anAtom) { * const LOADING = { state: 'loading' } * const unwrappedAtom = unwrap(anAtom, () => LOADING) * return atom((get) => { * try { * const data = get(unwrappedAtom) * if (data === LOADING) { * return LOADING * } * return { state: 'hasData', data } * } catch (error) { * return { state: 'hasError', error } * } * }) * } * ``` */ export function loadable(anAtom: Atom): Atom> { if (import.meta.env?.MODE !== 'production' && !didWarnDeprecation) { console.warn( '[DEPRECATED] loadable is deprecated and will be removed in v3. ' + 'Please use a userland util with the `unwrap` util: https://github.com/pmndrs/jotai/pull/3217', ) didWarnDeprecation = true } return memo1(() => { const LOADING: Loadable = { state: 'loading' } const unwrappedAtom = unwrap(anAtom, () => LOADING) return atom((get) => { try { const data = get(unwrappedAtom) if (data === LOADING) { return LOADING } return { state: 'hasData', data } as Loadable } catch (error) { return { state: 'hasError', error } } }) }, anAtom) } ================================================ FILE: src/vanilla/utils/selectAtom.ts ================================================ import { atom } from '../../vanilla.ts' import type { Atom } from '../../vanilla.ts' const getCached = (c: () => T, m: WeakMap, k: object): T => (m.has(k) ? m : m.set(k, c())).get(k) as T const cache1 = new WeakMap() const memo3 = ( create: () => T, dep1: object, dep2: object, dep3: object, ): T => { const cache2 = getCached(() => new WeakMap(), cache1, dep1) const cache3 = getCached(() => new WeakMap(), cache2, dep2) return getCached(create, cache3, dep3) } export function selectAtom( anAtom: Atom, selector: (v: Value, prevSlice?: Slice) => Slice, equalityFn?: (a: Slice, b: Slice) => boolean, ): Atom export function selectAtom( anAtom: Atom, selector: (v: Value, prevSlice?: Slice) => Slice, equalityFn: (prevSlice: Slice, slice: Slice) => boolean = Object.is, ) { return memo3( () => { const EMPTY = Symbol() const selectValue = ([value, prevSlice]: readonly [ Value, Slice | typeof EMPTY, ]) => { if (prevSlice === EMPTY) { return selector(value) } const slice = selector(value, prevSlice) return equalityFn(prevSlice, slice) ? prevSlice : slice } const derivedAtom: Atom & { init?: typeof EMPTY } = atom((get) => { const prev = get(derivedAtom) const value = get(anAtom) return selectValue([value, prev] as const) }) // HACK to read derived atom before initialization derivedAtom.init = EMPTY return derivedAtom }, anAtom, selector, equalityFn, ) } ================================================ FILE: src/vanilla/utils/splitAtom.ts ================================================ import { atom } from '../../vanilla.ts' import type { Atom, Getter, PrimitiveAtom, SetStateAction, Setter, WritableAtom, } from '../../vanilla.ts' const getCached = (c: () => T, m: WeakMap, k: object): T => (m.has(k) ? m : m.set(k, c())).get(k) as T const cache1 = new WeakMap() const memo2 = (create: () => T, dep1: object, dep2: object): T => { const cache2 = getCached(() => new WeakMap(), cache1, dep1) return getCached(create, cache2, dep2) } const cacheKeyForEmptyKeyExtractor = {} const isWritable = ( atom: Atom | WritableAtom, ): atom is WritableAtom => !!(atom as WritableAtom).write const isFunction = (x: T): x is T & ((...args: never[]) => unknown) => typeof x === 'function' type SplitAtomAction = | { type: 'remove'; atom: PrimitiveAtom } | { type: 'insert' value: Item before?: PrimitiveAtom } | { type: 'move' atom: PrimitiveAtom before?: PrimitiveAtom } export function splitAtom( arrAtom: WritableAtom, keyExtractor?: (item: Item) => Key, ): WritableAtom[], [SplitAtomAction], void> export function splitAtom( arrAtom: Atom, keyExtractor?: (item: Item) => Key, ): Atom[]> export function splitAtom( arrAtom: WritableAtom | Atom, keyExtractor?: (item: Item) => Key, ) { return memo2( () => { type ItemAtom = PrimitiveAtom | Atom type Mapping = { arr: Item[] atomList: ItemAtom[] keyList: Key[] } const mappingCache = new WeakMap() const getMapping = (arr: Item[], prev?: Item[]) => { let mapping = mappingCache.get(arr) if (mapping) { return mapping } const prevMapping = prev && mappingCache.get(prev) const atomList: Atom[] = [] const keyList: Key[] = [] arr.forEach((item, index) => { const key = keyExtractor ? keyExtractor(item) : (index as unknown as Key) keyList[index] = key const cachedAtom = prevMapping && prevMapping.atomList[prevMapping.keyList.indexOf(key)] if (cachedAtom) { atomList[index] = cachedAtom return } const read = (get: Getter) => { const prev = get(mappingAtom) as Mapping | undefined const currArr = get(arrAtom) const mapping = getMapping(currArr, prev?.arr) const index = mapping.keyList.indexOf(key) if (index < 0 || index >= currArr.length) { // returning a stale value to avoid errors for use cases such as react-spring const prevItem = arr[getMapping(arr).keyList.indexOf(key)] if (prevItem) { return prevItem } throw new Error('splitAtom: index out of bounds for read') } return currArr[index]! } const write = ( get: Getter, set: Setter, update: SetStateAction, ) => { const prev = get(mappingAtom) as Mapping | undefined const arr = get(arrAtom) const mapping = getMapping(arr, prev?.arr) const index = mapping.keyList.indexOf(key) if (index < 0 || index >= arr.length) { throw new Error('splitAtom: index out of bounds for write') } const nextItem = isFunction(update) ? (update as (prev: Item) => Item)(arr[index]!) : update if (!Object.is(arr[index], nextItem)) { set(arrAtom as WritableAtom, [ ...arr.slice(0, index), nextItem, ...arr.slice(index + 1), ]) } } atomList[index] = isWritable(arrAtom) ? atom(read, write) : atom(read) }) if ( prevMapping && prevMapping.keyList.length === keyList.length && prevMapping.keyList.every((x, i) => x === keyList[i]) ) { // not changed mapping = prevMapping } else { mapping = { arr, atomList, keyList } } mappingCache.set(arr, mapping) return mapping } const mappingAtom: Atom & { init?: undefined } = atom((get) => { const prev = get(mappingAtom) as Mapping | undefined const arr = get(arrAtom) const mapping = getMapping(arr, prev?.arr) return mapping }) if (import.meta.env?.MODE !== 'production') { mappingAtom.debugPrivate = true } // HACK to read mapping atom before initialization mappingAtom.init = undefined const splittedAtom = isWritable(arrAtom) ? atom( (get) => get(mappingAtom).atomList, (get, set, action: SplitAtomAction) => { switch (action.type) { case 'remove': { const index = get(splittedAtom).indexOf(action.atom) if (index >= 0) { const arr = get(arrAtom) set(arrAtom as WritableAtom, [ ...arr.slice(0, index), ...arr.slice(index + 1), ]) } break } case 'insert': { const index = action.before ? get(splittedAtom).indexOf(action.before) : get(splittedAtom).length if (index >= 0) { const arr = get(arrAtom) set(arrAtom as WritableAtom, [ ...arr.slice(0, index), action.value, ...arr.slice(index), ]) } break } case 'move': { const index1 = get(splittedAtom).indexOf(action.atom) const index2 = action.before ? get(splittedAtom).indexOf(action.before) : get(splittedAtom).length if (index1 >= 0 && index2 >= 0) { const arr = get(arrAtom) if (index1 < index2) { set(arrAtom as WritableAtom, [ ...arr.slice(0, index1), ...arr.slice(index1 + 1, index2), arr[index1]!, ...arr.slice(index2), ]) } else { set(arrAtom as WritableAtom, [ ...arr.slice(0, index2), arr[index1]!, ...arr.slice(index2, index1), ...arr.slice(index1 + 1), ]) } } break } } }, ) : atom((get) => get(mappingAtom).atomList) // read-only atom return splittedAtom }, arrAtom, keyExtractor || cacheKeyForEmptyKeyExtractor, ) } ================================================ FILE: src/vanilla/utils/unwrap.ts ================================================ import { atom } from '../../vanilla.ts' import type { Atom, WritableAtom } from '../../vanilla.ts' const getCached = (c: () => T, m: WeakMap, k: object): T => (m.has(k) ? m : m.set(k, c())).get(k) as T const cache1 = new WeakMap() const memo2 = (create: () => T, dep1: object, dep2: object): T => { const cache2 = getCached(() => new WeakMap(), cache1, dep1) return getCached(create, cache2, dep2) } const isPromiseLike = (p: unknown): p is PromiseLike => typeof (p as any)?.then === 'function' const defaultFallback = () => undefined export function unwrap( anAtom: WritableAtom, ): WritableAtom | undefined, Args, Result> export function unwrap( anAtom: WritableAtom, fallback: (prev?: Awaited) => PendingValue, ): WritableAtom | PendingValue, Args, Result> export function unwrap( anAtom: Atom, ): Atom | undefined> export function unwrap( anAtom: Atom, fallback: (prev?: Awaited) => PendingValue, ): Atom | PendingValue> export function unwrap( anAtom: WritableAtom | Atom, fallback: (prev?: Awaited) => PendingValue = defaultFallback as never, ) { return memo2( () => { type PromiseAndValue = { readonly p?: PromiseLike } & ( | { readonly v: Awaited } | { readonly f: PendingValue; readonly v?: Awaited } ) const promiseErrorCache = new WeakMap, unknown>() const promiseResultCache = new WeakMap< PromiseLike, Awaited >() const refreshAtom = atom(0) const triggerRefreshAtom = atom([] as [triggerRefresh?: () => void]) triggerRefreshAtom.INTERNAL_onInit = (store) => { store.set(triggerRefreshAtom, [ () => store.set(refreshAtom, (c) => c + 1), ]) } if (import.meta.env?.MODE !== 'production') { refreshAtom.debugPrivate = true triggerRefreshAtom.debugPrivate = true } const promiseAndValueAtom: Atom & { init?: undefined } = atom((get) => { get(refreshAtom) let prev: PromiseAndValue | undefined try { prev = get(promiseAndValueAtom) as PromiseAndValue | undefined } catch { // ignore previous errors to avoid getting stuck in error state } const promise = get(anAtom) if (!isPromiseLike(promise)) { return { v: promise as Awaited } } if (promise !== prev?.p) { promise.then( (v) => { promiseResultCache.set(promise, v as Awaited) const [triggerRefresh] = get(triggerRefreshAtom) triggerRefresh!() }, (e) => { promiseErrorCache.set(promise, e) const [triggerRefresh] = get(triggerRefreshAtom) triggerRefresh!() }, ) } if (promiseErrorCache.has(promise)) { throw promiseErrorCache.get(promise) } if (promiseResultCache.has(promise)) { return { p: promise, v: promiseResultCache.get(promise) as Awaited, } } if (prev && 'v' in prev) { return { p: promise, f: fallback(prev.v), v: prev.v } } return { p: promise, f: fallback() } }) // HACK to read PromiseAndValue atom before initialization promiseAndValueAtom.init = undefined if (import.meta.env?.MODE !== 'production') { promiseAndValueAtom.debugPrivate = true } return atom( (get) => { const state = get(promiseAndValueAtom) if ('f' in state) { // is pending return state.f } return state.v }, (_get, set, ...args) => set(anAtom as WritableAtom, ...args), ) }, anAtom, fallback, ) } ================================================ FILE: src/vanilla/utils.ts ================================================ export { RESET } from './utils/constants.ts' export { atomWithReset } from './utils/atomWithReset.ts' export { atomWithReducer } from './utils/atomWithReducer.ts' export { atomFamily } from './utils/atomFamily.ts' export { selectAtom } from './utils/selectAtom.ts' export { freezeAtom, freezeAtomCreator } from './utils/freezeAtom.ts' export { splitAtom } from './utils/splitAtom.ts' export { atomWithDefault } from './utils/atomWithDefault.ts' export { atomWithStorage, createJSONStorage, withStorageValidator as unstable_withStorageValidator, } from './utils/atomWithStorage.ts' export { atomWithObservable } from './utils/atomWithObservable.ts' export { loadable } from './utils/loadable.ts' export { unwrap } from './utils/unwrap.ts' export { atomWithRefresh } from './utils/atomWithRefresh.ts' export { atomWithLazy } from './utils/atomWithLazy.ts' ================================================ FILE: src/vanilla.ts ================================================ export { atom } from './vanilla/atom.ts' export type { Atom, WritableAtom, PrimitiveAtom } from './vanilla/atom.ts' export { createStore, getDefaultStore, INTERNAL_overrideCreateStore, } from './vanilla/store.ts' export type { Getter, Setter, ExtractAtomValue, ExtractAtomArgs, ExtractAtomResult, SetStateAction, } from './vanilla/typeUtils.ts' ================================================ FILE: tests/babel/plugin-debug-label.test.ts ================================================ import { transformSync } from '@babel/core' import { expect, it } from 'vitest' import plugin from 'jotai/babel/plugin-debug-label' const transform = ( code: string, filename?: string, customAtomNames?: string[], ) => transformSync(code, { babelrc: false, configFile: false, filename, plugins: [[plugin, { customAtomNames }]], })?.code it('Should add a debugLabel to an atom', () => { expect(transform(`const countAtom = atom(0);`)).toMatchInlineSnapshot(` "const countAtom = atom(0); countAtom.debugLabel = "countAtom";" `) }) it('Should handle a atom from a default export', () => { expect(transform(`const countAtom = jotai.atom(0);`)).toMatchInlineSnapshot(` "const countAtom = jotai.atom(0); countAtom.debugLabel = "countAtom";" `) }) it('Should handle a atom being exported', () => { expect(transform(`export const countAtom = atom(0);`)).toMatchInlineSnapshot(` "export const countAtom = atom(0); countAtom.debugLabel = "countAtom";" `) }) it('Should handle a default exported atom', () => { expect(transform(`export default atom(0);`, 'countAtom.ts')) .toMatchInlineSnapshot(` "const countAtom = atom(0); countAtom.debugLabel = "countAtom"; export default countAtom;" `) }) it('Should handle a default exported atom in a barrel file', () => { expect(transform(`export default atom(0);`, 'atoms/index.ts')) .toMatchInlineSnapshot(` "const atoms = atom(0); atoms.debugLabel = "atoms"; export default atoms;" `) }) it('Should handle all types of exports', () => { expect( transform( ` export const countAtom = atom(0); export default atom(0); `, 'atoms/index.ts', ), ).toMatchInlineSnapshot(` "export const countAtom = atom(0); countAtom.debugLabel = "countAtom"; const atoms = atom(0); atoms.debugLabel = "atoms"; export default atoms;" `) }) it('Should handle all atom types', () => { expect( transform( ` export const countAtom = atom(0); const myFamily = atomFamily((param) => atom(param)); const countAtomWithDefault = atomWithDefault((get) => get(countAtom) * 2); const observableAtom = atomWithObservable(() => {}); const reducerAtom = atomWithReducer(0, () => {}); const resetAtom = atomWithReset(0); const storageAtom = atomWithStorage('count', 1); const freezedAtom = freezeAtom(atom({ count: 0 })); const loadedAtom = loadable(countAtom); const selectedValueAtom = selectAtom(atom({ a: 0, b: 'othervalue' }), (v) => v.a); const splittedAtom = splitAtom(atom([])); const unwrappedAtom = unwrap(asyncArrayAtom, () => []); const someatomWithSubscription = atomWithSubscription(() => {}); const someAtomWithStore = atomWithStore(() => {}); const someAtomWithHash = atomWithHash('', ''); const someAtomWithLocation = atomWithLocation(); const someFocusAtom = focusAtom(someAtom, () => {}); const someAtomWithValidate = atomWithValidate('', {}); const someValidateAtoms = validateAtoms({}, () => {}); const someAtomWithCache = atomWithCache(async () => {}); const someAtomWithRecoilValue = atomWithRecoilValue({}); `, 'atoms/index.ts', ), ).toMatchInlineSnapshot(` "export const countAtom = atom(0); countAtom.debugLabel = "countAtom"; const myFamily = atomFamily(param => atom(param)); myFamily.debugLabel = "myFamily"; const countAtomWithDefault = atomWithDefault(get => get(countAtom) * 2); countAtomWithDefault.debugLabel = "countAtomWithDefault"; const observableAtom = atomWithObservable(() => {}); observableAtom.debugLabel = "observableAtom"; const reducerAtom = atomWithReducer(0, () => {}); reducerAtom.debugLabel = "reducerAtom"; const resetAtom = atomWithReset(0); resetAtom.debugLabel = "resetAtom"; const storageAtom = atomWithStorage('count', 1); storageAtom.debugLabel = "storageAtom"; const freezedAtom = freezeAtom(atom({ count: 0 })); freezedAtom.debugLabel = "freezedAtom"; const loadedAtom = loadable(countAtom); loadedAtom.debugLabel = "loadedAtom"; const selectedValueAtom = selectAtom(atom({ a: 0, b: 'othervalue' }), v => v.a); selectedValueAtom.debugLabel = "selectedValueAtom"; const splittedAtom = splitAtom(atom([])); splittedAtom.debugLabel = "splittedAtom"; const unwrappedAtom = unwrap(asyncArrayAtom, () => []); unwrappedAtom.debugLabel = "unwrappedAtom"; const someatomWithSubscription = atomWithSubscription(() => {}); someatomWithSubscription.debugLabel = "someatomWithSubscription"; const someAtomWithStore = atomWithStore(() => {}); someAtomWithStore.debugLabel = "someAtomWithStore"; const someAtomWithHash = atomWithHash('', ''); someAtomWithHash.debugLabel = "someAtomWithHash"; const someAtomWithLocation = atomWithLocation(); someAtomWithLocation.debugLabel = "someAtomWithLocation"; const someFocusAtom = focusAtom(someAtom, () => {}); someFocusAtom.debugLabel = "someFocusAtom"; const someAtomWithValidate = atomWithValidate('', {}); someAtomWithValidate.debugLabel = "someAtomWithValidate"; const someValidateAtoms = validateAtoms({}, () => {}); someValidateAtoms.debugLabel = "someValidateAtoms"; const someAtomWithCache = atomWithCache(async () => {}); someAtomWithCache.debugLabel = "someAtomWithCache"; const someAtomWithRecoilValue = atomWithRecoilValue({}); someAtomWithRecoilValue.debugLabel = "someAtomWithRecoilValue";" `) }) it('Handles custom atom names a debugLabel to an atom', () => { expect( transform(`const mySpecialThing = myCustomAtom(0);`, undefined, [ 'myCustomAtom', ]), ).toMatchInlineSnapshot(` "const mySpecialThing = myCustomAtom(0); mySpecialThing.debugLabel = "mySpecialThing";" `) }) ================================================ FILE: tests/babel/plugin-react-refresh.test.ts ================================================ import { transformSync } from '@babel/core' import { expect, it } from 'vitest' import plugin from 'jotai/babel/plugin-react-refresh' const transform = ( code: string, filename?: string, customAtomNames?: string[], ) => transformSync(code, { babelrc: false, configFile: false, filename, root: '.', plugins: [[plugin, { customAtomNames }]], })?.code it('Should add a cache for a single atom', () => { expect(transform(`const countAtom = atom(0);`, '/src/atoms/index.ts')) .toMatchInlineSnapshot(` "globalThis.jotaiAtomCache = globalThis.jotaiAtomCache || { cache: new Map(), get(name, inst) { if (this.cache.has(name)) { return this.cache.get(name); } this.cache.set(name, inst); return inst; } }; const countAtom = globalThis.jotaiAtomCache.get("/src/atoms/index.ts/countAtom", atom(0));" `) }) it('Should add a cache for multiple atoms', () => { expect( transform( ` const countAtom = atom(0); const doubleAtom = atom((get) => get(countAtom) * 2); `, '/src/atoms/index.ts', ), ).toMatchInlineSnapshot(` "globalThis.jotaiAtomCache = globalThis.jotaiAtomCache || { cache: new Map(), get(name, inst) { if (this.cache.has(name)) { return this.cache.get(name); } this.cache.set(name, inst); return inst; } }; const countAtom = globalThis.jotaiAtomCache.get("/src/atoms/index.ts/countAtom", atom(0)); const doubleAtom = globalThis.jotaiAtomCache.get("/src/atoms/index.ts/doubleAtom", atom(get => get(countAtom) * 2));" `) }) it('Should add a cache for multiple exported atoms', () => { expect( transform( ` export const countAtom = atom(0); export const doubleAtom = atom((get) => get(countAtom) * 2); `, '/src/atoms/index.ts', ), ).toMatchInlineSnapshot(` "globalThis.jotaiAtomCache = globalThis.jotaiAtomCache || { cache: new Map(), get(name, inst) { if (this.cache.has(name)) { return this.cache.get(name); } this.cache.set(name, inst); return inst; } }; export const countAtom = globalThis.jotaiAtomCache.get("/src/atoms/index.ts/countAtom", atom(0)); export const doubleAtom = globalThis.jotaiAtomCache.get("/src/atoms/index.ts/doubleAtom", atom(get => get(countAtom) * 2));" `) }) it('Should add a cache for a default exported atom', () => { expect(transform(`export default atom(0);`, '/src/atoms/index.ts')) .toMatchInlineSnapshot(` "globalThis.jotaiAtomCache = globalThis.jotaiAtomCache || { cache: new Map(), get(name, inst) { if (this.cache.has(name)) { return this.cache.get(name); } this.cache.set(name, inst); return inst; } }; export default globalThis.jotaiAtomCache.get("/src/atoms/index.ts/defaultExport", atom(0));" `) }) it('Should add a cache for mixed exports of atoms', () => { expect( transform( ` export const countAtom = atom(0); export default atom((get) => get(countAtom) * 2); `, '/src/atoms/index.ts', ), ).toMatchInlineSnapshot(` "globalThis.jotaiAtomCache = globalThis.jotaiAtomCache || { cache: new Map(), get(name, inst) { if (this.cache.has(name)) { return this.cache.get(name); } this.cache.set(name, inst); return inst; } }; export const countAtom = globalThis.jotaiAtomCache.get("/src/atoms/index.ts/countAtom", atom(0)); export default globalThis.jotaiAtomCache.get("/src/atoms/index.ts/defaultExport", atom(get => get(countAtom) * 2));" `) }) it('Should fail if no filename is available', () => { expect(() => transform(`const countAtom = atom(0);`)).toThrow( 'Filename must be available', ) }) it('Should handle atoms returned from functions (#891)', () => { expect( transform( `function createAtom(label) { const anAtom = atom(0); anAtom.debugLabel = label; return anAtom; } const countAtom = atom(0); const countAtom2 = createAtom("countAtom2"); const countAtom3 = createAtom("countAtom3");`, '/src/atoms/index.ts', ), ).toMatchInlineSnapshot(` "globalThis.jotaiAtomCache = globalThis.jotaiAtomCache || { cache: new Map(), get(name, inst) { if (this.cache.has(name)) { return this.cache.get(name); } this.cache.set(name, inst); return inst; } }; function createAtom(label) { const anAtom = atom(0); anAtom.debugLabel = label; return anAtom; } const countAtom = globalThis.jotaiAtomCache.get("/src/atoms/index.ts/countAtom", atom(0)); const countAtom2 = createAtom("countAtom2"); const countAtom3 = createAtom("countAtom3");" `) }) it('Should handle custom atom names', () => { expect( transform( `const mySpecialThing = myCustomAtom(0);`, '/src/atoms/index.ts', ['myCustomAtom'], ), ).toMatchInlineSnapshot(` "globalThis.jotaiAtomCache = globalThis.jotaiAtomCache || { cache: new Map(), get(name, inst) { if (this.cache.has(name)) { return this.cache.get(name); } this.cache.set(name, inst); return inst; } }; const mySpecialThing = globalThis.jotaiAtomCache.get("/src/atoms/index.ts/mySpecialThing", myCustomAtom(0));" `) }) ================================================ FILE: tests/babel/preset.test.ts ================================================ import { transformSync } from '@babel/core' import { expect, it } from 'vitest' import preset from 'jotai/babel/preset' const transform = ( code: string, filename?: string, customAtomNames?: string[], ) => transformSync(code, { babelrc: false, configFile: false, filename, presets: [[preset, { customAtomNames }]], })?.code it('Should add a debugLabel and cache to an atom', () => { expect(transform(`const countAtom = atom(0);`, '/src/atoms.ts')) .toMatchInlineSnapshot(` "globalThis.jotaiAtomCache = globalThis.jotaiAtomCache || { cache: new Map(), get(name, inst) { if (this.cache.has(name)) { return this.cache.get(name); } this.cache.set(name, inst); return inst; } }; const countAtom = globalThis.jotaiAtomCache.get("/src/atoms.ts/countAtom", atom(0)); countAtom.debugLabel = "countAtom";" `) }) it('Should add a debugLabel and cache to multiple atoms', () => { expect( transform( ` const countAtom = atom(0); const doubleAtom = atom((get) => get(countAtom) * 2);`, '/src/atoms.ts', ), ).toMatchInlineSnapshot(` "globalThis.jotaiAtomCache = globalThis.jotaiAtomCache || { cache: new Map(), get(name, inst) { if (this.cache.has(name)) { return this.cache.get(name); } this.cache.set(name, inst); return inst; } }; const countAtom = globalThis.jotaiAtomCache.get("/src/atoms.ts/countAtom", atom(0)); countAtom.debugLabel = "countAtom"; const doubleAtom = globalThis.jotaiAtomCache.get("/src/atoms.ts/doubleAtom", atom(get => get(countAtom) * 2)); doubleAtom.debugLabel = "doubleAtom";" `) }) it('Should add a cache and debugLabel for multiple exported atoms', () => { expect( transform( ` export const countAtom = atom(0); export const doubleAtom = atom((get) => get(countAtom) * 2); `, '/src/atoms/index.ts', ), ).toMatchInlineSnapshot(` "globalThis.jotaiAtomCache = globalThis.jotaiAtomCache || { cache: new Map(), get(name, inst) { if (this.cache.has(name)) { return this.cache.get(name); } this.cache.set(name, inst); return inst; } }; export const countAtom = globalThis.jotaiAtomCache.get("/src/atoms/index.ts/countAtom", atom(0)); countAtom.debugLabel = "countAtom"; export const doubleAtom = globalThis.jotaiAtomCache.get("/src/atoms/index.ts/doubleAtom", atom(get => get(countAtom) * 2)); doubleAtom.debugLabel = "doubleAtom";" `) }) it('Should add a cache and debugLabel for a default exported atom', () => { expect(transform(`export default atom(0);`, '/src/atoms/index.ts')) .toMatchInlineSnapshot(` "globalThis.jotaiAtomCache = globalThis.jotaiAtomCache || { cache: new Map(), get(name, inst) { if (this.cache.has(name)) { return this.cache.get(name); } this.cache.set(name, inst); return inst; } }; const atoms = globalThis.jotaiAtomCache.get("/src/atoms/index.ts/atoms", atom(0)); atoms.debugLabel = "atoms"; export default atoms;" `) }) it('Should add a cache and debugLabel for mixed exports of atoms', () => { expect( transform( ` export const countAtom = atom(0); export default atom((get) => get(countAtom) * 2); `, '/src/atoms/index.ts', ), ).toMatchInlineSnapshot(` "globalThis.jotaiAtomCache = globalThis.jotaiAtomCache || { cache: new Map(), get(name, inst) { if (this.cache.has(name)) { return this.cache.get(name); } this.cache.set(name, inst); return inst; } }; export const countAtom = globalThis.jotaiAtomCache.get("/src/atoms/index.ts/countAtom", atom(0)); countAtom.debugLabel = "countAtom"; const atoms = globalThis.jotaiAtomCache.get("/src/atoms/index.ts/atoms", atom(get => get(countAtom) * 2)); atoms.debugLabel = "atoms"; export default atoms;" `) }) it('Should fail if no filename is available', () => { expect(() => transform(`const countAtom = atom(0);`)).toThrow( 'Filename must be available', ) }) it('Should handle custom atom names', () => { expect( transform(`const mySpecialThing = myCustomAtom(0);`, '/src/atoms.ts', [ 'myCustomAtom', ]), ).toMatchInlineSnapshot(` "globalThis.jotaiAtomCache = globalThis.jotaiAtomCache || { cache: new Map(), get(name, inst) { if (this.cache.has(name)) { return this.cache.get(name); } this.cache.set(name, inst); return inst; } }; const mySpecialThing = globalThis.jotaiAtomCache.get("/src/atoms.ts/mySpecialThing", myCustomAtom(0)); mySpecialThing.debugLabel = "mySpecialThing";" `) }) ================================================ FILE: tests/react/abortable.test.tsx ================================================ import { StrictMode, Suspense, useState } from 'react' import { act, fireEvent, render, screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { useAtomValue, useSetAtom } from 'jotai/react' import { atom } from 'jotai/vanilla' import { sleep } from '../test-utils' beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) describe('abortable atom test', () => { it('can abort with signal.aborted', async () => { const countAtom = atom(0) let abortedCount = 0 const derivedAtom = atom(async (get, { signal }) => { const count = get(countAtom) await sleep(100) if (signal.aborted) { ++abortedCount } return count }) const Component = () => { const count = useAtomValue(derivedAtom) return
count: {count}
} const Controls = () => { const setCount = useSetAtom(countAtom) return ( <> ) } await act(() => render( , ), ) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count: 0')).toBeInTheDocument() expect(abortedCount).toBe(0) await act(() => fireEvent.click(screen.getByText('button'))) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('button'))) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count: 2')).toBeInTheDocument() expect(abortedCount).toBe(1) await act(() => fireEvent.click(screen.getByText('button'))) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count: 3')).toBeInTheDocument() expect(abortedCount).toBe(1) }) it('can abort with event listener', async () => { const countAtom = atom(0) let abortedCount = 0 const derivedAtom = atom(async (get, { signal }) => { const count = get(countAtom) const callback = () => { ++abortedCount } signal.addEventListener('abort', callback) await sleep(100) signal.removeEventListener('abort', callback) return count }) const Component = () => { const count = useAtomValue(derivedAtom) return
count: {count}
} const Controls = () => { const setCount = useSetAtom(countAtom) return ( <> ) } await act(() => render( , ), ) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count: 0')).toBeInTheDocument() expect(abortedCount).toBe(0) await act(() => fireEvent.click(screen.getByText('button'))) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('button'))) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count: 2')).toBeInTheDocument() expect(abortedCount).toBe(1) await act(() => fireEvent.click(screen.getByText('button'))) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count: 3')).toBeInTheDocument() expect(abortedCount).toBe(1) }) it('does not abort on unmount', async () => { const countAtom = atom(0) let abortedCount = 0 const derivedAtom = atom(async (get, { signal }) => { const count = get(countAtom) await sleep(100) if (signal.aborted) { ++abortedCount } return count }) const Component = () => { const count = useAtomValue(derivedAtom) return
count: {count}
} const Parent = () => { const setCount = useSetAtom(countAtom) const [show, setShow] = useState(true) return ( <> {show ? : 'hidden'} ) } await act(() => render( , ), ) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count: 0')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('button'))) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('toggle'))) expect(screen.getByText('hidden')).toBeInTheDocument() expect(abortedCount).toBe(0) }) it('throws aborted error (like fetch)', async () => { const countAtom = atom(0) const derivedAtom = atom(async (get, { signal }) => { const count = get(countAtom) await sleep(100) if (signal.aborted) { throw new Error('aborted') } return count }) const Component = () => { const count = useAtomValue(derivedAtom) return
count: {count}
} const Controls = () => { const setCount = useSetAtom(countAtom) return ( <> ) } await act(() => render( , ), ) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count: 0')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('button'))) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('button'))) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count: 2')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('button'))) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count: 3')).toBeInTheDocument() }) }) ================================================ FILE: tests/react/async.test.tsx ================================================ import { StrictMode, Suspense, useEffect } from 'react' import { act, fireEvent, render, screen } from '@testing-library/react' import { afterEach, beforeEach, expect, it, vi } from 'vitest' import { useAtom } from 'jotai/react' import { atom } from 'jotai/vanilla' import type { Atom } from 'jotai/vanilla' import { sleep, useCommitCount } from '../test-utils' beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) it('does not show async stale result', async () => { const countAtom = atom(0) const asyncCountAtom = atom(async (get) => { await sleep(100) return get(countAtom) }) const committed: number[] = [] const Counter = () => { const [count, setCount] = useAtom(countAtom) const onClick = async () => { setCount((c) => c + 1) await sleep(100) setCount((c) => c + 1) } return ( <>
count: {count}
) } const DelayedCounter = () => { const [delayedCount] = useAtom(asyncCountAtom) useEffect(() => { committed.push(delayedCount) }) return
delayedCount: {delayedCount}
} await act(() => render( <> , ), ) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count: 0')).toBeInTheDocument() expect(screen.getByText('delayedCount: 0')).toBeInTheDocument() expect(committed).toEqual([0]) await act(() => fireEvent.click(screen.getByText('button'))) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count: 2')).toBeInTheDocument() expect(screen.getByText('delayedCount: 2')).toBeInTheDocument() // React 18+ uses automatic batching, so committed is [0, 2] // React 16-17 doesn't batch async updates, so committed is [0, 1, 2] // Different build types (cjs, umd, esm) may also affect batching behavior expect(committed.length).toBeGreaterThanOrEqual(2) expect(committed[0]).toBe(0) expect(committed[committed.length - 1]).toBe(2) }) it('does not show async stale result on derived atom', async () => { const countAtom = atom(0) const asyncAlwaysNullAtom = atom(async (get) => { get(countAtom) await sleep(100) return null }) const derivedAtom = atom((get) => get(asyncAlwaysNullAtom)) const DisplayAsyncValue = () => { const [asyncValue] = useAtom(asyncAlwaysNullAtom) return
async value: {JSON.stringify(asyncValue)}
} const DisplayDerivedValue = () => { const [derivedValue] = useAtom(derivedAtom) return
derived value: {JSON.stringify(derivedValue)}
} const Test = () => { const [count, setCount] = useAtom(countAtom) return (
count: {count}
loading async value
}> loading derived value}> ) } await act(() => render( , ), ) expect(screen.getByText('count: 0')).toBeInTheDocument() expect(screen.getByText('loading async value')).toBeInTheDocument() expect(screen.getByText('loading derived value')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('async value: null')).toBeInTheDocument() expect(screen.getByText('derived value: null')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('button'))) expect(screen.getByText('count: 1')).toBeInTheDocument() expect(screen.getByText('loading async value')).toBeInTheDocument() expect(screen.getByText('loading derived value')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('async value: null')).toBeInTheDocument() expect(screen.getByText('derived value: null')).toBeInTheDocument() }) it('works with async get with extra deps', async () => { const countAtom = atom(0) const anotherAtom = atom(-1) const asyncCountAtom = atom(async (get) => { get(anotherAtom) await sleep(100) return get(countAtom) }) const Counter = () => { const [count, setCount] = useAtom(countAtom) return ( <>
count: {count}
) } const DelayedCounter = () => { const [delayedCount] = useAtom(asyncCountAtom) return
delayedCount: {delayedCount}
} await act(() => render( , ), ) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count: 0')).toBeInTheDocument() expect(screen.getByText('delayedCount: 0')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('button'))) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count: 1')).toBeInTheDocument() expect(screen.getByText('delayedCount: 1')).toBeInTheDocument() }) it('reuses promises on initial read', async () => { let invokeCount = 0 const asyncAtom = atom(async () => { invokeCount += 1 await sleep(100) return 'ready' }) const Child = () => { const [str] = useAtom(asyncAtom) return
{str}
} await act(() => render( , ), ) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) const elements = screen.getAllByText('ready') elements.forEach((element) => { expect(element).toBeInTheDocument() }) expect(invokeCount).toBe(1) }) it('uses multiple async atoms at once', async () => { const someAtom = atom(async () => { await sleep(100) return 'ready' }) const someAtom2 = atom(async () => { await sleep(50) return 'ready2' }) const Component = () => { const [some] = useAtom(someAtom) const [some2] = useAtom(someAtom2) return ( <>
{some} {some2}
) } await act(() => render( , ), ) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) await act(() => vi.advanceTimersByTimeAsync(50)) expect(screen.getByText('ready ready2')).toBeInTheDocument() }) it('uses async atom in the middle of dependency chain', async () => { const countAtom = atom(0) const asyncCountAtom = atom(async (get) => { await sleep(100) return get(countAtom) }) const delayedCountAtom = atom((get) => get(asyncCountAtom)) const Counter = () => { const [count, setCount] = useAtom(countAtom) const [delayedCount] = useAtom(delayedCountAtom) return ( <>
count: {count}, delayed: {delayedCount}
) } await act(() => render( , ), ) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count: 0, delayed: 0')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('button'))) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count: 1, delayed: 1')).toBeInTheDocument() }) it('updates an async atom in child useEffect on remount without setTimeout', async () => { const toggleAtom = atom(true) const countAtom = atom(0) const asyncCountAtom = atom( async (get) => get(countAtom), async (get, set) => set(countAtom, get(countAtom) + 1), ) const Counter = () => { const [count, incCount] = useAtom(asyncCountAtom) useEffect(() => { incCount() }, [incCount]) return
count: {count}
} const Parent = () => { const [toggle, setToggle] = useAtom(toggleAtom) return ( <> {toggle ? :
no child
} ) } await act(() => render( <> , ), ) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('count: 1')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('button'))) expect(screen.getByText('no child')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('button'))) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('count: 2')).toBeInTheDocument() }) it('updates an async atom in child useEffect on remount', async () => { const toggleAtom = atom(true) const countAtom = atom(0) const asyncCountAtom = atom( async (get) => { await sleep(100) return get(countAtom) }, async (get, set) => { await sleep(100) set(countAtom, get(countAtom) + 1) }, ) const Counter = () => { const [count, incCount] = useAtom(asyncCountAtom) useEffect(() => { incCount() }, [incCount]) return
count: {count}
} const Parent = () => { const [toggle, setToggle] = useAtom(toggleAtom) return ( <> {toggle ? :
no child
} ) } await act(() => render( <> , ), ) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count: 0')).toBeInTheDocument() // NOTE: 1000ms to wait for useEffect's write operation with React scheduling overhead await act(() => vi.advanceTimersByTimeAsync(1000)) expect(screen.getByText('count: 1')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect(screen.getByText('no child')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) // NOTE: 1000ms to wait for useEffect's write operation with React scheduling overhead await act(() => vi.advanceTimersByTimeAsync(1000)) expect(screen.getByText('count: 2')).toBeInTheDocument() }) it('async get and useEffect on parent', async () => { const countAtom = atom(0) const asyncAtom = atom(async (get) => { const count = get(countAtom) if (!count) return 'none' return 'resolved' }) const AsyncComponent = () => { const [text] = useAtom(asyncAtom) return
text: {text}
} const Parent = () => { const [count, setCount] = useAtom(countAtom) useEffect(() => { setCount((c) => c + 1) }, [setCount]) return ( <>
count: {count}
) } await act(() => render( <> , ), ) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('count: 1')).toBeInTheDocument() expect(screen.getByText('text: resolved')).toBeInTheDocument() }) it('async get with another dep and useEffect on parent', async () => { const countAtom = atom(0) const derivedAtom = atom((get) => get(countAtom)) const asyncAtom = atom(async (get) => { const count = get(derivedAtom) if (!count) return 'none' return count }) const AsyncComponent = () => { const [count] = useAtom(asyncAtom) return
async: {count}
} const Parent = () => { const [count, setCount] = useAtom(countAtom) useEffect(() => { setCount((c) => c + 1) }, [setCount]) return ( <>
count: {count}
) } await act(() => render( <> , ), ) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('count: 1')).toBeInTheDocument() expect(screen.getByText('async: 1')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('button'))) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('count: 2')).toBeInTheDocument() expect(screen.getByText('async: 2')).toBeInTheDocument() }) it('set promise atom value on write (#304)', async () => { const countAtom = atom(Promise.resolve(0)) const asyncAtom = atom(null, (get, set, _arg) => { set( countAtom, Promise.resolve(get(countAtom)).then(async (c) => { await sleep(100) return c + 1 }), ) }) const Counter = () => { const [count] = useAtom(countAtom) return
count: {count * 1}
} const Parent = () => { const [, dispatch] = useAtom(asyncAtom) return ( <> ) } await act(() => render( , ), ) // FIXME this is not working //await screen.findByText('loading') await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('count: 0')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('button'))) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count: 1')).toBeInTheDocument() }) it('uses async atom double chain (#306)', async () => { const countAtom = atom(0) const asyncCountAtom = atom(async (get) => { await sleep(100) return get(countAtom) }) const delayedCountAtom = atom(async (get) => { return get(asyncCountAtom) }) const Counter = () => { const [count, setCount] = useAtom(countAtom) const [delayedCount] = useAtom(delayedCountAtom) return ( <>
count: {count}, delayed: {delayedCount}
) } await act(() => render( , ), ) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count: 0, delayed: 0')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('button'))) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count: 1, delayed: 1')).toBeInTheDocument() }) it('uses an async atom that depends on another async atom', async () => { const asyncAtom = atom(async (get) => { await sleep(100) get(anotherAsyncAtom) return 1 }) const anotherAsyncAtom = atom(async () => { return 2 }) const Counter = () => { const [num] = useAtom(asyncAtom) return
num: {num}
} await act(() => render( , ), ) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('num: 1')).toBeInTheDocument() }) it('a derived atom from a newly created async atom (#351)', async () => { const countAtom = atom(1) const atomCache = new Map>>() const getAsyncAtom = (n: number) => { if (!atomCache.has(n)) { atomCache.set( n, atom(async () => { return n + 10 }), ) } return atomCache.get(n) as Atom> } const derivedAtom = atom((get) => get(getAsyncAtom(get(countAtom)))) const Counter = () => { const [, setCount] = useAtom(countAtom) const [derived] = useAtom(derivedAtom) return ( <>
derived: {derived}, commits: {useCommitCount()}
) } await act(() => render( <> , ), ) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('derived: 11, commits: 1')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('button'))) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('derived: 12, commits: 2')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('button'))) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('derived: 13, commits: 3')).toBeInTheDocument() }) it('Handles synchronously invoked async set (#375)', async () => { const loadingAtom = atom(false) const documentAtom = atom(undefined) const loadDocumentAtom = atom(null, (_get, set) => { const fetch = async () => { set(loadingAtom, true) const response = await new Promise((resolve) => setTimeout(() => resolve('great document'), 100), ) set(documentAtom, response) set(loadingAtom, false) } fetch() }) const ListDocuments = () => { const [loading] = useAtom(loadingAtom) const [document] = useAtom(documentAtom) const [, loadDocument] = useAtom(loadDocumentAtom) useEffect(() => { loadDocument() }, [loadDocument]) return ( <> {loading &&
loading
} {!loading &&
{document}
} ) } render( , ) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('great document')).toBeInTheDocument() }) it('async write self atom', async () => { const countAtom = atom(0, async (get, set, _arg) => { set(countAtom, get(countAtom) + 1) await sleep(100) set(countAtom, -1) }) const Counter = () => { const [count, inc] = useAtom(countAtom) return ( <>
count: {count}
) } render( , ) expect(screen.getByText('count: 0')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count: -1')).toBeInTheDocument() }) it('non suspense async write self atom with setTimeout (#389)', async () => { const countAtom = atom(0, (get, set, _arg) => { set(countAtom, get(countAtom) + 1) setTimeout(() => set(countAtom, -1)) }) const Counter = () => { const [count, inc] = useAtom(countAtom) return ( <>
count: {count}
) } render( , ) expect(screen.getByText('count: 0')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 1')).toBeInTheDocument() await act(() => vi.advanceTimersByTime(0)) expect(screen.getByText('count: -1')).toBeInTheDocument() }) it('should override promise as atom value (#430)', async () => { const countAtom = atom(new Promise(() => {})) const setCountAtom = atom(null, (_get, set, arg: number) => { set(countAtom, Promise.resolve(arg)) }) const Counter = () => { const [count] = useAtom(countAtom) return
count: {count * 1}
} const Control = () => { const [, setCount] = useAtom(setCountAtom) return } await act(() => render( , ), ) expect(screen.getByText('loading')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('count: 1')).toBeInTheDocument() }) it('combine two promise atom values (#442)', async () => { const count1Atom = atom(new Promise(() => {})) const count2Atom = atom(new Promise(() => {})) const derivedAtom = atom( async (get) => (await get(count1Atom)) + (await get(count2Atom)), ) const initAtom = atom(null, (_get, set) => { setTimeout(() => set(count1Atom, Promise.resolve(1))) setTimeout(() => set(count2Atom, Promise.resolve(2))) }) initAtom.onMount = (init) => { init() } const Counter = () => { const [count] = useAtom(derivedAtom) return
count: {count}
} const Control = () => { useAtom(initAtom) return null } await act(() => render( , ), ) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('count: 3')).toBeInTheDocument() }) it('set two promise atoms at once', async () => { const count1Atom = atom(new Promise(() => {})) const count2Atom = atom(new Promise(() => {})) const derivedAtom = atom( async (get) => (await get(count1Atom)) + (await get(count2Atom)), ) const setCountsAtom = atom(null, (_get, set) => { set(count1Atom, Promise.resolve(1)) set(count2Atom, Promise.resolve(2)) }) const Counter = () => { const [count] = useAtom(derivedAtom) return
count: {count}
} const Control = () => { const [, setCounts] = useAtom(setCountsAtom) return } await act(() => render( , ), ) expect(screen.getByText('loading')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('count: 3')).toBeInTheDocument() }) it('async write chain', async () => { const countAtom = atom(0) const asyncWriteAtom = atom(null, async (_get, set, _arg) => { await sleep(100) set(countAtom, 2) }) const controlAtom = atom(null, async (_get, set, _arg) => { set(countAtom, 1) await set(asyncWriteAtom, null) await sleep(100) set(countAtom, 3) }) const Counter = () => { const [count] = useAtom(countAtom) return
count: {count}
} const Control = () => { const [, invoke] = useAtom(controlAtom) return } render( , ) expect(screen.getByText('count: 0')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 1')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count: 2')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count: 3')).toBeInTheDocument() }) it('async atom double chain without setTimeout (#751)', async () => { const enabledAtom = atom(false) const asyncAtom = atom(async (get) => { const enabled = get(enabledAtom) if (!enabled) { return 'init' } await sleep(100) return 'ready' }) const derivedAsyncAtom = atom(async (get) => get(asyncAtom)) const anotherAsyncAtom = atom(async (get) => get(derivedAsyncAtom)) const AsyncComponent = () => { const [text] = useAtom(anotherAsyncAtom) return
async: {text}
} const Parent = () => { // Use useAtom to reproduce the issue const [, setEnabled] = useAtom(enabledAtom) return ( <> ) } await act(() => render( , ), ) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('async: init')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('button'))) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('async: ready')).toBeInTheDocument() }) it('async atom double chain with setTimeout', async () => { const enabledAtom = atom(false) const asyncAtom = atom(async (get) => { const enabled = get(enabledAtom) if (!enabled) { return 'init' } await sleep(100) return 'ready' }) const derivedAsyncAtom = atom(async (get) => { await sleep(100) return get(asyncAtom) }) const anotherAsyncAtom = atom(async (get) => { await sleep(100) return get(derivedAsyncAtom) }) const AsyncComponent = () => { const [text] = useAtom(anotherAsyncAtom) return
async: {text}
} const Parent = () => { // Use useAtom to reproduce the issue const [, setEnabled] = useAtom(enabledAtom) return ( <> ) } await act(() => render( , ), ) await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('async: init')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('button'))) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('async: ready')).toBeInTheDocument() }) it('update unmounted async atom with intermediate atom', async () => { const enabledAtom = atom(true) const countAtom = atom(1) const intermediateAtom = atom((get) => { const count = get(countAtom) const enabled = get(enabledAtom) const tmpAtom = atom(async () => { if (!enabled) { return -1 } await sleep(100) return count * 2 }) return tmpAtom }) const derivedAtom = atom((get) => { const tmpAtom = get(intermediateAtom) return get(tmpAtom) }) const DerivedCounter = () => { const [derived] = useAtom(derivedAtom) return
derived: {derived}
} const Control = () => { const [, setEnabled] = useAtom(enabledAtom) const [, setCount] = useAtom(countAtom) return ( <> ) } await act(() => render( , ), ) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('derived: 2')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('toggle enabled'))) await act(() => vi.advanceTimersByTimeAsync(0)) await act(() => fireEvent.click(screen.getByText('increment count'))) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('derived: -1')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('toggle enabled'))) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('derived: 4')).toBeInTheDocument() }) it('multiple derived atoms with dependency chaining and async write (#813)', async () => { const responseBaseAtom = atom<{ name: string }[] | null>(null) const response1 = [{ name: 'alpha' }, { name: 'beta' }] const responseAtom = atom( (get) => get(responseBaseAtom), (_get, set) => { setTimeout(() => set(responseBaseAtom, response1)) }, ) responseAtom.onMount = (init) => { init() } const mapAtom = atom((get) => get(responseAtom)) const itemA = atom((get) => get(mapAtom)?.[0]) const itemB = atom((get) => get(mapAtom)?.[1]) const itemAName = atom((get) => get(itemA)?.name) const itemBName = atom((get) => get(itemB)?.name) const App = () => { const [aName] = useAtom(itemAName) const [bName] = useAtom(itemBName) return ( <>
aName: {aName}
bName: {bName}
) } render( , ) await act(() => vi.advanceTimersByTime(0)) expect(screen.getByText('aName: alpha')).toBeInTheDocument() expect(screen.getByText('bName: beta')).toBeInTheDocument() }) ================================================ FILE: tests/react/async2.test.tsx ================================================ import { StrictMode, Suspense } from 'react' import { act, fireEvent, render, screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { useAtom, useAtomValue, useSetAtom } from 'jotai/react' import { atom } from 'jotai/vanilla' import { sleep } from '../test-utils' beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) describe('useAtom delay option test', () => { it('suspend for Promise.resolve without delay option', async () => { const countAtom = atom(0) const asyncAtom = atom((get) => { const count = get(countAtom) if (count === 0) { return 0 } return Promise.resolve(count) }) const Component = () => { const count = useAtomValue(asyncAtom) return
count: {count}
} const Controls = () => { const setCount = useSetAtom(countAtom) return ( <> ) } render( , ) expect(screen.getByText('count: 0')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('button'))) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('count: 1')).toBeInTheDocument() }) it('do not suspend for Promise.resolve with delay option', async () => { const countAtom = atom(0) const asyncAtom = atom((get) => { const count = get(countAtom) if (count === 0) { return 0 } return Promise.resolve(count) }) const Component = () => { const count = useAtomValue(asyncAtom, { delay: 0 }) return
count: {count}
} const Controls = () => { const setCount = useSetAtom(countAtom) return ( <> ) } render( , ) expect(screen.getByText('count: 0')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('count: 1')).toBeInTheDocument() }) }) describe('atom read function setSelf option test', () => { it('do not suspend with promise resolving with setSelf', async () => { const countAtom = atom(0) const asyncAtom = atom(async () => { await sleep(100) return 'hello' }) const refreshAtom = atom(0) const promiseCache = new WeakMap() const derivedAtom = atom( (get, { setSelf }) => { get(refreshAtom) const count = get(countAtom) const promise = get(asyncAtom) if (promiseCache.has(promise)) { return (promiseCache.get(promise) as string) + count } promise.then((v) => { promiseCache.set(promise, v) setSelf() }) return 'pending' + count }, (_get, set) => { set(refreshAtom, (c) => c + 1) }, ) const Component = () => { const text = useAtomValue(derivedAtom) return
text: {text}
} const Controls = () => { const setCount = useSetAtom(countAtom) return ( <> ) } render( , ) expect(screen.getByText('text: pending0')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('text: hello0')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect(screen.getByText('text: hello1')).toBeInTheDocument() }) }) describe('timing issue with setSelf', () => { it('resolves dependencies reliably after a delay (#2192)', async () => { expect.assertions(6) const countAtom = atom(0) let result: number | null = null const asyncAtom = atom(async (get) => { const count = get(countAtom) await sleep(100) return count }) const derivedAtom = atom( async (get, { setSelf }) => { get(countAtom) await Promise.resolve() const resultCount = await get(asyncAtom) result = resultCount if (resultCount === 2) setSelf() // <-- necessary }, () => {}, ) const derivedSyncAtom = atom((get) => { get(derivedAtom) }) const increment = (c: number) => c + 1 function TestComponent() { useAtom(derivedSyncAtom) const [count, setCount] = useAtom(countAtom) const onClick = () => { setCount(increment) setCount(increment) } return ( <> count: {count} ) } render( , ) await vi.advanceTimersByTimeAsync(100) expect(screen.getByText('count: 0')).toBeInTheDocument() expect(result).toBe(0) fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 2')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(100) expect(result).toBe(2) fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 4')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(100) expect(result).toBe(4) }) }) describe('infinite pending', () => { it('odd counter', async () => { const countAtom = atom(0) const asyncAtom = atom((get) => { const count = get(countAtom) if (count % 2 === 0) { const infinitePending = new Promise(() => {}) return infinitePending } return count }) const Component = () => { const count = useAtomValue(asyncAtom) return
count: {count}
} const Controls = () => { const setCount = useSetAtom(countAtom) return ( <> ) } await act(() => render( , ), ) expect(screen.getByText('loading')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('count: 1')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('button'))) expect(screen.getByText('loading')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('count: 3')).toBeInTheDocument() }) }) describe('write to async atom twice', async () => { it('no wait', async () => { const asyncAtom = atom(Promise.resolve(2)) const writer = atom(null, async (get, set) => { set(asyncAtom, async (c) => (await c) + 1) set(asyncAtom, async (c) => (await c) + 1) return get(asyncAtom) }) const Component = () => { const count = useAtomValue(asyncAtom) const write = useSetAtom(writer) return ( <>
count: {count}
) } await act(() => render( , ), ) await vi.advanceTimersByTimeAsync(0) expect(screen.getByText('count: 2')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('button'))) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('count: 4')).toBeInTheDocument() }) it('wait Promise.resolve()', async () => { const asyncAtom = atom(Promise.resolve(2)) const writer = atom(null, async (get, set) => { set(asyncAtom, async (c) => (await c) + 1) await Promise.resolve() set(asyncAtom, async (c) => (await c) + 1) return get(asyncAtom) }) const Component = () => { const count = useAtomValue(asyncAtom) const write = useSetAtom(writer) return ( <>
count: {count}
) } await act(() => render( , ), ) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('count: 2')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('button'))) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('count: 4')).toBeInTheDocument() }) it('wait setTimeout()', async () => { const asyncAtom = atom(Promise.resolve(2)) const writer = atom(null, async (get, set) => { set(asyncAtom, async (c) => (await c) + 1) await sleep(100) set(asyncAtom, async (c) => (await c) + 1) return get(asyncAtom) }) const Component = () => { const count = useAtomValue(asyncAtom) const write = useSetAtom(writer) return ( <>
count: {count}
) } await act(() => render( , ), ) await vi.advanceTimersByTimeAsync(0) expect(screen.getByText('count: 2')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('button'))) await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count: 4')).toBeInTheDocument() }) }) describe('with onMount', () => { it('does not infinite loop with setting a promise (#2931)', async () => { const firstPromise = Promise.resolve(1) const secondPromise = Promise.resolve(2) const asyncAtom = atom(firstPromise) let onMountCallCount = 0 asyncAtom.onMount = (setCount) => { onMountCallCount++ setCount((prev) => (prev === firstPromise ? secondPromise : prev)) } const Component = () => { const [count, setCount] = useAtom(asyncAtom) return ( <>
count: {count}
) } await act(() => render( , ), ) await act(() => vi.advanceTimersByTimeAsync(0)) // onMount should be called a limited number of times (not infinitely) // In StrictMode, React may mount/unmount/remount, so allow up to a few calls const initialCallCount = onMountCallCount expect(initialCallCount).toBeGreaterThan(0) expect(initialCallCount).toBeLessThanOrEqual(4) expect(screen.getByText('count: 2')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('button'))) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('count: 3')).toBeInTheDocument() // onMount may be called a few more times due to StrictMode, but not infinitely expect(onMountCallCount).toBeLessThanOrEqual(initialCallCount + 2) expect(onMountCallCount).toBeLessThan(10) // If infinite loop, this would be much higher }) }) ================================================ FILE: tests/react/basic.test.tsx ================================================ import { StrictMode, Suspense, version as reactVersion, useEffect, useMemo, useState, } from 'react' import { act, fireEvent, render, screen } from '@testing-library/react' import { unstable_batchedUpdates } from 'react-dom' import { afterEach, beforeEach, expect, it, vi } from 'vitest' import { useAtom } from 'jotai/react' import { atom } from 'jotai/vanilla' import type { PrimitiveAtom } from 'jotai/vanilla' import { sleep, useCommitCount } from '../test-utils' beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) const IS_REACT18 = /^18\./.test(reactVersion) const batchedUpdates = (fn: () => void) => { if (IS_REACT18) { fn() } else { unstable_batchedUpdates(fn) } } it('uses a primitive atom', () => { const countAtom = atom(0) const Counter = () => { const [count, setCount] = useAtom(countAtom) return ( <>
count: {count}
) } render( , ) expect(screen.getByText('count: 0')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 1')).toBeInTheDocument() }) it('uses a read-only derived atom', () => { const countAtom = atom(0) const doubledCountAtom = atom((get) => get(countAtom) * 2) const Counter = () => { const [count, setCount] = useAtom(countAtom) const [doubledCount] = useAtom(doubledCountAtom) return ( <>
count: {count}
doubledCount: {doubledCount}
) } render( , ) expect(screen.getByText('count: 0')).toBeInTheDocument() expect(screen.getByText('doubledCount: 0')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 1')).toBeInTheDocument() expect(screen.getByText('doubledCount: 2')).toBeInTheDocument() }) it('uses a read-write derived atom', () => { const countAtom = atom(0) const doubledCountAtom = atom( (get) => get(countAtom) * 2, (get, set, update: number) => set(countAtom, get(countAtom) + update), ) const Counter = () => { const [count] = useAtom(countAtom) const [doubledCount, increaseCount] = useAtom(doubledCountAtom) return ( <>
count: {count}
doubledCount: {doubledCount}
) } render( , ) expect(screen.getByText('count: 0')).toBeInTheDocument() expect(screen.getByText('doubledCount: 0')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 2')).toBeInTheDocument() expect(screen.getByText('doubledCount: 4')).toBeInTheDocument() }) it('uses a write-only derived atom', () => { const countAtom = atom(0) const incrementCountAtom = atom(null, (get, set) => set(countAtom, get(countAtom) + 1), ) const Counter = () => { const [count] = useAtom(countAtom) return (
commits: {useCommitCount()}, count: {count}
) } const Control = () => { const [, increment] = useAtom(incrementCountAtom) return ( <>
button commits: {useCommitCount()}
) } render( <> , ) expect(screen.getByText('commits: 1, count: 0')).toBeInTheDocument() expect(screen.getByText('button commits: 1')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect(screen.getByText('commits: 2, count: 1')).toBeInTheDocument() expect(screen.getByText('button commits: 1')).toBeInTheDocument() }) it('only re-renders if value has changed', () => { const count1Atom = atom(0) const count2Atom = atom(0) const productAtom = atom((get) => get(count1Atom) * get(count2Atom)) type Props = { countAtom: typeof count1Atom; name: string } const Counter = ({ countAtom, name }: Props) => { const [count, setCount] = useAtom(countAtom) return ( <>
commits: {useCommitCount()}, {name}: {count}
) } const Product = () => { const [product] = useAtom(productAtom) return ( <>
commits: {useCommitCount()}, product: {product}
) } render( <> , ) expect(screen.getByText('commits: 1, count1: 0')).toBeInTheDocument() expect(screen.getByText('commits: 1, count2: 0')).toBeInTheDocument() expect(screen.getByText('commits: 1, product: 0')).toBeInTheDocument() fireEvent.click(screen.getByText('button-count1')) expect(screen.getByText('commits: 2, count1: 1')).toBeInTheDocument() expect(screen.getByText('commits: 1, count2: 0')).toBeInTheDocument() expect(screen.getByText('commits: 1, product: 0')).toBeInTheDocument() fireEvent.click(screen.getByText('button-count2')) expect(screen.getByText('commits: 2, count1: 1')).toBeInTheDocument() expect(screen.getByText('commits: 2, count2: 1')).toBeInTheDocument() expect(screen.getByText('commits: 2, product: 1')).toBeInTheDocument() }) it('re-renders a time delayed derived atom with the same initial value (#947)', async () => { const aAtom = atom(false) aAtom.onMount = (set) => { setTimeout(() => { set(true) }) } const bAtom = atom(1) bAtom.onMount = (set) => { set(2) } const cAtom = atom((get) => { if (get(aAtom)) { return get(bAtom) } return 1 }) const App = () => { const [value] = useAtom(cAtom) return <>{value} } render( , ) expect(screen.getByText('1')).toBeInTheDocument() // Wait for setTimeout to execute await act(() => vi.advanceTimersByTime(0)) expect(screen.getByText('2')).toBeInTheDocument() }) it('works with async get', async () => { const countAtom = atom(0) const asyncCountAtom = atom(async (get) => { await sleep(100) return get(countAtom) }) const Counter = () => { const [count, setCount] = useAtom(countAtom) const [delayedCount] = useAtom(asyncCountAtom) return ( <>
commits: {useCommitCount()}, count: {count}, delayedCount:{' '} {delayedCount}
) } await act(() => render( <> , ), ) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect( screen.getByText('commits: 1, count: 0, delayedCount: 0'), ).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('button'))) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect( screen.getByText('commits: 2, count: 1, delayedCount: 1'), ).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('button'))) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect( screen.getByText('commits: 3, count: 2, delayedCount: 2'), ).toBeInTheDocument() }) it('works with async get without setTimeout', async () => { const countAtom = atom(0) const asyncCountAtom = atom(async (get) => { return get(countAtom) }) const Counter = () => { const [count, setCount] = useAtom(countAtom) const [delayedCount] = useAtom(asyncCountAtom) return ( <>
count: {count}, delayedCount: {delayedCount}
) } await act(() => render( , ), ) // NOTE: loading doesn't appear because async atom resolves immediately (microtask only) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('count: 0, delayedCount: 0')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('button'))) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('count: 1, delayedCount: 1')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('button'))) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('count: 2, delayedCount: 2')).toBeInTheDocument() }) it('uses atoms with tree dependencies', async () => { const topAtom = atom(0) const leftAtom = atom((get) => get(topAtom)) const rightAtom = atom( (get) => get(topAtom), async (get, set, update: (prev: number) => number) => { await sleep(100) batchedUpdates(() => { set(topAtom, update(get(topAtom))) }) }, ) const Counter = () => { const [count] = useAtom(leftAtom) const [, setCount] = useAtom(rightAtom) return ( <>
commits: {useCommitCount()}, count: {count}
) } render( <> , ) expect(screen.getByText('commits: 1, count: 0')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('commits: 2, count: 1')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('commits: 3, count: 2')).toBeInTheDocument() }) it('runs update only once in StrictMode', () => { let updateCount = 0 const countAtom = atom(0) const derivedAtom = atom( (get) => get(countAtom), (_get, set, update: number) => { updateCount += 1 set(countAtom, update) }, ) const Counter = () => { const [count, setCount] = useAtom(derivedAtom) return ( <>
count: {count}
) } render( , ) expect(screen.getByText('count: 0')).toBeInTheDocument() expect(updateCount).toBe(0) fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 1')).toBeInTheDocument() expect(updateCount).toBe(1) }) it('uses an async write-only atom', async () => { const countAtom = atom(0) const asyncCountAtom = atom( null, async (get, set, update: (prev: number) => number) => { await sleep(100) set(countAtom, update(get(countAtom))) }, ) const Counter = () => { const [count] = useAtom(countAtom) const [, setCount] = useAtom(asyncCountAtom) return ( <>
commits: {useCommitCount()}, count: {count}
) } render( <> , ) expect(screen.getByText('commits: 1, count: 0')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('commits: 2, count: 1')).toBeInTheDocument() }) it('uses a writable atom without read function', async () => { const countAtom = atom(1, async (get, set, v: number) => { await sleep(100) set(countAtom, get(countAtom) + 10 * v) }) const Counter = () => { const [count, addCount10Times] = useAtom(countAtom) return ( <>
count: {count}
) } render( , ) expect(screen.getByText('count: 1')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count: 11')).toBeInTheDocument() }) it('can write an atom value on useEffect', async () => { const countAtom = atom(0) const Counter = () => { const [count, setCount] = useAtom(countAtom) useEffect(() => { setCount((c) => c + 1) }, [setCount]) return
count: {count}
} render( <> , ) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('count: 1')).toBeInTheDocument() }) it('can write an atom value on useEffect in children', async () => { const countAtom = atom(0) const Child = ({ setCount, }: { setCount: (f: (c: number) => number) => void }) => { useEffect(() => { setCount((c) => c + 1) }, [setCount]) return null } const Counter = () => { const [count, setCount] = useAtom(countAtom) return (
count: {count}
) } render( <> , ) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('count: 2')).toBeInTheDocument() }) it('only invoke read function on use atom', () => { const countAtom = atom(0) let readCount = 0 const doubledCountAtom = atom((get) => { readCount += 1 return get(countAtom) * 2 }) expect(readCount).toBe(0) // do not invoke on atom() const Counter = () => { const [count, setCount] = useAtom(countAtom) const [doubledCount] = useAtom(doubledCountAtom) return ( <>
commits: {useCommitCount()}, count: {count}, readCount: {readCount}, doubled: {doubledCount}
) } render( <> , ) expect( screen.getByText('commits: 1, count: 0, readCount: 1, doubled: 0'), ).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect( screen.getByText('commits: 2, count: 1, readCount: 2, doubled: 2'), ).toBeInTheDocument() }) it('uses a read-write derived atom with two primitive atoms', () => { const countAAtom = atom(0) const countBAtom = atom(0) const sumAtom = atom( (get) => get(countAAtom) + get(countBAtom), (_get, set) => { set(countAAtom, 0) set(countBAtom, 0) }, ) const incBothAtom = atom(null, (get, set) => { set(countAAtom, get(countAAtom) + 1) set(countBAtom, get(countBAtom) + 1) }) const Counter = () => { const [countA, setCountA] = useAtom(countAAtom) const [countB, setCountB] = useAtom(countBAtom) const [sum, reset] = useAtom(sumAtom) const [, incBoth] = useAtom(incBothAtom) return ( <>
countA: {countA}, countB: {countB}, sum: {sum}
) } render( , ) expect(screen.getByText('countA: 0, countB: 0, sum: 0')).toBeInTheDocument() fireEvent.click(screen.getByText('incA')) expect(screen.getByText('countA: 1, countB: 0, sum: 1')).toBeInTheDocument() fireEvent.click(screen.getByText('incB')) expect(screen.getByText('countA: 1, countB: 1, sum: 2')).toBeInTheDocument() fireEvent.click(screen.getByText('reset')) expect(screen.getByText('countA: 0, countB: 0, sum: 0')).toBeInTheDocument() fireEvent.click(screen.getByText('incBoth')) expect(screen.getByText('countA: 1, countB: 1, sum: 2')).toBeInTheDocument() }) it('updates a derived atom in useEffect with two primitive atoms', () => { const countAAtom = atom(0) const countBAtom = atom(1) const sumAtom = atom((get) => get(countAAtom) + get(countBAtom)) const Counter = () => { const [countA, setCountA] = useAtom(countAAtom) const [countB, setCountB] = useAtom(countBAtom) const [sum] = useAtom(sumAtom) useEffect(() => { setCountA((c) => c + 1) }, [setCountA, countB]) return ( <>
countA: {countA}, countB: {countB}, sum: {sum}
) } render( <> , ) expect(screen.getByText('countA: 1, countB: 1, sum: 2')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect(screen.getByText('countA: 2, countB: 2, sum: 4')).toBeInTheDocument() }) it('updates two atoms in child useEffect', () => { const countAAtom = atom(0) const countBAtom = atom(10) const Child = () => { const [countB, setCountB] = useAtom(countBAtom) useEffect(() => { setCountB((c) => c + 1) }, [setCountB]) return
countB: {countB}
} const Counter = () => { const [countA, setCountA] = useAtom(countAAtom) useEffect(() => { setCountA((c) => c + 1) }, [setCountA]) return ( <>
countA: {countA}
{countA > 0 && } ) } render( <> , ) expect(screen.getByText('countA: 1')).toBeInTheDocument() expect(screen.getByText('countB: 11')).toBeInTheDocument() }) it('set atom right after useEffect (#208)', async () => { const countAtom = atom(0) const effectFn = vi.fn() const Child = () => { const [count, setCount] = useAtom(countAtom) const [, setState] = useState(null) // rAF does not repro, so schedule update intentionally in render if (count === 1) { Promise.resolve().then(() => { setCount(2) }) } useEffect(() => { effectFn(count) // eslint-disable-next-line react-hooks/set-state-in-effect setState(null) // this is important to repro (set something stable) }, [count, setState]) return
count: {count}
} const Parent = () => { const [, setCount] = useAtom(countAtom) useEffect(() => { setCount(1) // requestAnimationFrame(() => setCount(2)) }, [setCount]) return } render( , ) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('count: 2')).toBeInTheDocument() expect(effectFn).toHaveBeenLastCalledWith(2) }) it('changes atom from parent (#273, #275)', () => { const atomA = atom({ id: 'a' }) const atomB = atom({ id: 'b' }) const Item = ({ id }: { id: string }) => { const a = useMemo(() => (id === 'a' ? atomA : atomB), [id]) const [atomValue] = useAtom(a) return (
commits: {useCommitCount()}, id: {atomValue.id}
) } const App = () => { const [id, setId] = useState('a') return (
) } render( <> , ) expect(screen.getByText('commits: 1, id: a')).toBeInTheDocument() fireEvent.click(screen.getByText('atom a')) expect(screen.getByText('commits: 1, id: a')).toBeInTheDocument() fireEvent.click(screen.getByText('atom b')) expect(screen.getByText('commits: 2, id: b')).toBeInTheDocument() fireEvent.click(screen.getByText('atom a')) expect(screen.getByText('commits: 3, id: a')).toBeInTheDocument() }) it('should be able to use a double derived atom twice and useEffect (#373)', () => { const countAtom = atom(0) const doubleAtom = atom((get) => get(countAtom) * 2) const fourfoldAtom = atom((get) => get(doubleAtom) * 2) const App = () => { const [count, setCount] = useAtom(countAtom) const [fourfold] = useAtom(fourfoldAtom) const [fourfold2] = useAtom(fourfoldAtom) useEffect(() => { setCount(count) }, [count, setCount]) return (
count: {count},{fourfold},{fourfold2}
) } render( , ) expect(screen.getByText('count: 0,0,0')).toBeInTheDocument() fireEvent.click(screen.getByText('one up')) expect(screen.getByText('count: 1,4,4')).toBeInTheDocument() }) it('write self atom (undocumented usage)', () => { const countAtom = atom(0, (get, set, _arg) => { set(countAtom, get(countAtom) + 1) }) const Counter = () => { const [count, inc] = useAtom(countAtom) return ( <>
count: {count}
) } render( , ) expect(screen.getByText('count: 0')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 1')).toBeInTheDocument() }) it('async chain for multiple sync and async atoms (#443)', async () => { const num1Atom = atom(async () => { return 1 }) const num2Atom = atom(async () => { return 2 }) // "async" is required to reproduce the issue const sumAtom = atom( async (get) => (await get(num1Atom)) + (await get(num2Atom)), ) const countAtom = atom((get) => get(sumAtom)) const Counter = () => { const [count] = useAtom(countAtom) return ( <>
count: {count}
) } await act(() => render( , ), ) // FIXME this is not working //screen.getByText('loading') await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('count: 3')).toBeInTheDocument() }) it('sync re-renders with useState re-renders (#827)', () => { const atom0 = atom('atom0') const atom1 = atom('atom1') const atom2 = atom('atom2') const atoms = [atom0, atom1, atom2] const App = () => { const [currentAtomIndex, setCurrentAtomIndex] = useState(0) const rotateAtoms = () => { setCurrentAtomIndex((prev) => (prev + 1) % atoms.length) } const [atomValue] = useAtom( atoms[currentAtomIndex] as (typeof atoms)[number], ) return ( <> commits: {useCommitCount()}

{atomValue}

) } render( <> , ) expect(screen.getByText('commits: 1')).toBeInTheDocument() fireEvent.click(screen.getByText('rotate')) expect(screen.getByText('commits: 2')).toBeInTheDocument() fireEvent.click(screen.getByText('rotate')) expect(screen.getByText('commits: 3')).toBeInTheDocument() }) it('chained derive atom with onMount and useEffect (#897)', () => { const countAtom = atom(0) countAtom.onMount = (set) => { set(1) } const derivedAtom = atom((get) => get(countAtom)) const derivedObjectAtom = atom((get) => ({ count: get(derivedAtom), })) const Counter = () => { const [, setCount] = useAtom(countAtom) const [{ count }] = useAtom(derivedObjectAtom) useEffect(() => { setCount(1) }, [setCount]) return
count: {count}
} render( , ) expect(screen.getByText('count: 1')).toBeInTheDocument() }) it('onMount is not called when atom value is accessed from writeGetter in derived atom (#942)', () => { const onUnmount = vi.fn() const onMount = vi.fn(() => { return onUnmount }) const aAtom = atom(false) aAtom.onMount = onMount const bAtom = atom(null, (get) => { get(aAtom) }) const App = () => { const [, action] = useAtom(bAtom) useEffect(() => action(), [action]) return null } render( , ) expect(onMount).not.toHaveBeenCalled() expect(onUnmount).not.toHaveBeenCalled() }) it('useAtom returns consistent value with input with changing atoms (#1235)', () => { const countAtom = atom(0) const valueAtoms = [atom(0), atom(1)] const Counter = () => { const [count, setCount] = useAtom(countAtom) const [value] = useAtom(valueAtoms[count] as PrimitiveAtom) if (count !== value) { throw new Error('value mismatch') } return ( <>
count: {count}
) } render( , ) expect(screen.getByText('count: 0')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 1')).toBeInTheDocument() }) ================================================ FILE: tests/react/dependency.test.tsx ================================================ /* eslint-disable react/no-unescaped-entities */ import { StrictMode, Suspense, useEffect, useRef, useState } from 'react' import { act, fireEvent, render, screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { useAtom, useAtomValue, useSetAtom } from 'jotai/react' import { atom } from 'jotai/vanilla' import type { Atom, Getter } from 'jotai/vanilla' import { useCommitCount } from '../test-utils' beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) it('works with 2 level dependencies', () => { const countAtom = atom(1) const doubledAtom = atom((get) => get(countAtom) * 2) const tripledAtom = atom((get) => get(doubledAtom) * 3) const Counter = () => { const [count, setCount] = useAtom(countAtom) const [doubledCount] = useAtom(doubledAtom) const [tripledCount] = useAtom(tripledAtom) return ( <>
commits: {useCommitCount()}, count: {count}, doubled: {doubledCount}, tripled: {tripledCount}
) } render( <> , ) expect( screen.getByText('commits: 1, count: 1, doubled: 2, tripled: 6'), ).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect( screen.getByText('commits: 2, count: 2, doubled: 4, tripled: 12'), ).toBeInTheDocument() }) it('works a primitive atom and a dependent async atom', async () => { const countAtom = atom(1) const doubledAtom = atom(async (get) => { await new Promise((r) => setTimeout(r, 100)) return get(countAtom) * 2 }) const Counter = () => { const [count, setCount] = useAtom(countAtom) const [doubledCount] = useAtom(doubledAtom) return ( <>
count: {count}, doubled: {doubledCount}
) } await act(() => render( , ), ) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count: 1, doubled: 2')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('button'))) await act(() => vi.advanceTimersByTimeAsync(1)) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(99)) expect(screen.getByText('count: 2, doubled: 4')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('button'))) await act(() => vi.advanceTimersByTimeAsync(1)) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(99)) expect(screen.getByText('count: 3, doubled: 6')).toBeInTheDocument() }) it('should keep an atom value even if unmounted', () => { const countAtom = atom(0) const derivedFn = vi.fn((get: Getter) => get(countAtom)) const derivedAtom = atom(derivedFn) const Counter = () => { const [count, setCount] = useAtom(countAtom) return ( <>
count: {count}
) } const DerivedCounter = () => { const [derived] = useAtom(derivedAtom) return
derived: {derived}
} const Parent = () => { const [show, setShow] = useState(true) return (
{show ? ( <> ) : (
hidden
)}
) } render( , ) expect(screen.getByText('count: 0')).toBeInTheDocument() expect(screen.getByText('derived: 0')).toBeInTheDocument() expect(derivedFn).toHaveReturnedTimes(1) fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 1')).toBeInTheDocument() expect(screen.getByText('derived: 1')).toBeInTheDocument() expect(derivedFn).toHaveReturnedTimes(2) fireEvent.click(screen.getByText('toggle')) expect(screen.getByText('hidden')).toBeInTheDocument() expect(derivedFn).toHaveReturnedTimes(2) fireEvent.click(screen.getByText('toggle')) expect(screen.getByText('count: 1')).toBeInTheDocument() expect(screen.getByText('derived: 1')).toBeInTheDocument() expect(derivedFn).toHaveReturnedTimes(2) }) it('should keep a dependent atom value even if unmounted', () => { const countAtom = atom(0) const derivedFn = vi.fn((get: Getter) => get(countAtom)) const derivedAtom = atom(derivedFn) const Counter = () => { const [count, setCount] = useAtom(countAtom) return ( <>
count: {count}
) } const DerivedCounter = () => { const [derived] = useAtom(derivedAtom) return
derived: {derived}
} const Parent = () => { const [showDerived, setShowDerived] = useState(true) return (
{showDerived ? : }
) } render( , ) expect(screen.getByText('derived: 0')).toBeInTheDocument() expect(derivedFn).toHaveReturnedTimes(1) fireEvent.click(screen.getByText('toggle')) expect(screen.getByText('count: 0')).toBeInTheDocument() expect(derivedFn).toHaveReturnedTimes(1) fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 1')).toBeInTheDocument() expect(derivedFn).toHaveReturnedTimes(1) fireEvent.click(screen.getByText('toggle')) expect(screen.getByText('derived: 1')).toBeInTheDocument() expect(derivedFn).toHaveReturnedTimes(2) }) it('should bail out updating if not changed', () => { const countAtom = atom(0) const derivedFn = vi.fn((get: Getter) => get(countAtom)) const derivedAtom = atom(derivedFn) const Counter = () => { const [count, setCount] = useAtom(countAtom) return ( <>
count: {count}
) } const DerivedCounter = () => { const [derived] = useAtom(derivedAtom) return
derived: {derived}
} render( , ) expect(screen.getByText('count: 0')).toBeInTheDocument() expect(screen.getByText('derived: 0')).toBeInTheDocument() expect(derivedFn).toHaveReturnedTimes(1) fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 0')).toBeInTheDocument() expect(screen.getByText('derived: 0')).toBeInTheDocument() expect(derivedFn).toHaveReturnedTimes(1) }) it('should bail out updating if not changed, 2 level', async () => { const dataAtom = atom({ count: 1, obj: { anotherCount: 10 } }) const getDataCountFn = vi.fn((get: Getter) => get(dataAtom).count) const countAtom = atom(getDataCountFn) const getDataObjFn = vi.fn((get: Getter) => get(dataAtom).obj) const objAtom = atom(getDataObjFn) const getAnotherCountFn = vi.fn((get: Getter) => get(objAtom).anotherCount) const anotherCountAtom = atom(getAnotherCountFn) const Counter = () => { const [count] = useAtom(countAtom) const [, setData] = useAtom(dataAtom) return ( <>
count: {count}
) } const DerivedCounter = () => { const [anotherCount] = useAtom(anotherCountAtom) return
anotherCount: {anotherCount}
} render( , ) expect(screen.getByText('count: 1')).toBeInTheDocument() expect(screen.getByText('anotherCount: 10')).toBeInTheDocument() expect(getDataCountFn).toHaveReturnedTimes(1) expect(getDataObjFn).toHaveReturnedTimes(1) expect(getAnotherCountFn).toHaveReturnedTimes(1) fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 2')).toBeInTheDocument() expect(screen.getByText('anotherCount: 10')).toBeInTheDocument() expect(getDataCountFn).toHaveReturnedTimes(2) expect(getDataObjFn).toHaveReturnedTimes(2) expect(getAnotherCountFn).toHaveReturnedTimes(1) }) it('derived atom to update base atom in callback', () => { const countAtom = atom(1) const doubledAtom = atom( (get) => get(countAtom) * 2, (_get, _set, callback: () => void) => { callback() }, ) const Counter = () => { const [count, setCount] = useAtom(countAtom) const [doubledCount, dispatch] = useAtom(doubledAtom) return ( <>
commits: {useCommitCount()}, count: {count}, doubled: {doubledCount}
) } render( <> , ) expect( screen.getByText('commits: 1, count: 1, doubled: 2'), ).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect( screen.getByText('commits: 2, count: 2, doubled: 4'), ).toBeInTheDocument() }) it('can read sync derived atom in write without initializing', () => { const countAtom = atom(1) const doubledAtom = atom((get) => get(countAtom) * 2) const addAtom = atom(null, (get, set, num: number) => { set(countAtom, get(doubledAtom) / 2 + num) }) const Counter = () => { const [count] = useAtom(countAtom) const [, add] = useAtom(addAtom) return ( <>
count: {count}
) } render( , ) expect(screen.getByText('count: 1')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 2')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 3')).toBeInTheDocument() }) it('can remount atoms with dependency (#490)', () => { const countAtom = atom(0) const derivedAtom = atom((get) => get(countAtom)) const Counter = () => { const [count, setCount] = useAtom(countAtom) return ( <>
count: {count}
) } const DerivedCounter = () => { const [derived] = useAtom(derivedAtom) return
derived: {derived}
} const Parent = () => { const [showChildren, setShowChildren] = useState(true) return (
{showChildren ? ( <> ) : (
hidden
)}
) } render( , ) expect(screen.getByText('count: 0')).toBeInTheDocument() expect(screen.getByText('derived: 0')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 1')).toBeInTheDocument() expect(screen.getByText('derived: 1')).toBeInTheDocument() fireEvent.click(screen.getByText('toggle')) expect(screen.getByText('hidden')).toBeInTheDocument() fireEvent.click(screen.getByText('toggle')) expect(screen.getByText('count: 1')).toBeInTheDocument() expect(screen.getByText('derived: 1')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 2')).toBeInTheDocument() expect(screen.getByText('derived: 2')).toBeInTheDocument() }) it('can remount atoms with intermediate atom', () => { const countAtom = atom(1) const resultAtom = atom(0) const intermediateAtom = atom((get) => { const count = get(countAtom) const initAtom = atom(null, (_get, set) => { set(resultAtom, count * 2) }) initAtom.onMount = (init) => { init() } return initAtom }) const derivedAtom = atom((get) => { const initAtom = get(intermediateAtom) get(initAtom) return get(resultAtom) }) const Counter = () => { const [count, setCount] = useAtom(countAtom) return ( <>
count: {count}
) } const DerivedCounter = () => { const [derived] = useAtom(derivedAtom) return
derived: {derived}
} const Parent = () => { const [showChildren, setShowChildren] = useState(true) return (
{showChildren ? :
hidden
}
) } render( , ) expect(screen.getByText('count: 1')).toBeInTheDocument() expect(screen.getByText('derived: 2')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 2')).toBeInTheDocument() expect(screen.getByText('derived: 4')).toBeInTheDocument() fireEvent.click(screen.getByText('toggle')) expect(screen.getByText('count: 2')).toBeInTheDocument() expect(screen.getByText('hidden')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 3')).toBeInTheDocument() expect(screen.getByText('hidden')).toBeInTheDocument() fireEvent.click(screen.getByText('toggle')) expect(screen.getByText('count: 3')).toBeInTheDocument() expect(screen.getByText('derived: 6')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 4')).toBeInTheDocument() expect(screen.getByText('derived: 8')).toBeInTheDocument() }) it('can update dependents with useEffect (#512)', () => { const enabledAtom = atom(false) const countAtom = atom(1) const derivedAtom = atom((get) => { const enabled = get(enabledAtom) if (!enabled) { return 0 } const count = get(countAtom) return count * 2 }) const Counter = () => { const [count, setCount] = useAtom(countAtom) return ( <>
count: {count}
) } const DerivedCounter = () => { const [derived] = useAtom(derivedAtom) return
derived: {derived}
} const Parent = () => { const [, setEnabled] = useAtom(enabledAtom) useEffect(() => { setEnabled(true) }, [setEnabled]) return (
) } render( , ) expect(screen.getByText('count: 1')).toBeInTheDocument() expect(screen.getByText('derived: 2')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 2')).toBeInTheDocument() expect(screen.getByText('derived: 4')).toBeInTheDocument() }) it('update unmounted atom with intermediate atom', () => { const enabledAtom = atom(true) const countAtom = atom(1) const intermediateAtom = atom((get) => { const count = get(countAtom) const enabled = get(enabledAtom) const tmpAtom = atom(enabled ? count * 2 : -1) return tmpAtom }) const derivedAtom = atom((get) => { const tmpAtom = get(intermediateAtom) return get(tmpAtom) }) const DerivedCounter = () => { const [derived] = useAtom(derivedAtom) return
derived: {derived}
} const Control = () => { const [, setEnabled] = useAtom(enabledAtom) const [, setCount] = useAtom(countAtom) return ( <> ) } render( , ) expect(screen.getByText('derived: 2')).toBeInTheDocument() fireEvent.click(screen.getByText('toggle enabled')) fireEvent.click(screen.getByText('increment count')) expect(screen.getByText('derived: -1')).toBeInTheDocument() fireEvent.click(screen.getByText('toggle enabled')) expect(screen.getByText('derived: 4')).toBeInTheDocument() }) it('Should bail for derived sync chains (#877)', () => { let syncAtomCount = 0 const textAtom = atom('hello') const syncAtom = atom((get) => { get(textAtom) syncAtomCount++ return 'My very long data' }) const derivedAtom = atom((get) => { return get(syncAtom) }) const Input = () => { const [result] = useAtom(derivedAtom) return
{result}
} const ForceValue = () => { const setText = useAtom(textAtom)[1] return (
) } render( , ) expect(screen.getByText('My very long data')).toBeInTheDocument() expect(syncAtomCount).toBe(1) fireEvent.click(screen.getByText(`set value to 'hello'`)) expect(screen.getByText('My very long data')).toBeInTheDocument() expect(syncAtomCount).toBe(1) }) it('Should bail for derived async chains (#877)', async () => { let syncAtomCount = 0 const textAtom = atom('hello') const asyncAtom = atom(async (get) => { get(textAtom) syncAtomCount++ return 'My very long data' }) const derivedAtom = atom((get) => { return get(asyncAtom) }) const Input = () => { const [result] = useAtom(derivedAtom) return
{result}
} const ForceValue = () => { const setText = useAtom(textAtom)[1] return (
) } await act(() => render( , ), ) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('My very long data')).toBeInTheDocument() expect(syncAtomCount).toBe(1) fireEvent.click(screen.getByText("set value to 'hello'")) expect(screen.getByText('My very long data')).toBeInTheDocument() expect(syncAtomCount).toBe(1) }) it('update correctly with async updates (#1250)', async () => { const countAtom = atom(0) const countIsGreaterThanOneAtom = atom((get) => get(countAtom) > 1) const alsoCountAtom = atom((get) => { const count = get(countAtom) get(countIsGreaterThanOneAtom) return count }) const App = () => { const setCount = useSetAtom(countAtom) const alsoCount = useAtomValue(alsoCountAtom) const countIsGreaterThanOne = useAtomValue(countIsGreaterThanOneAtom) const incrementCountTwice = () => { setTimeout(() => setCount((count) => count + 1)) setTimeout(() => setCount((count) => count + 1)) } return (
alsoCount: {alsoCount}
countIsGreaterThanOne: {countIsGreaterThanOne.toString()}
) } render( , ) expect(screen.getByText('alsoCount: 0')).toBeInTheDocument() expect(screen.getByText('countIsGreaterThanOne: false')).toBeInTheDocument() fireEvent.click(screen.getByText('Increment Count Twice')) await act(() => vi.advanceTimersByTime(0)) expect(screen.getByText('alsoCount: 2')).toBeInTheDocument() expect(screen.getByText('countIsGreaterThanOne: true')).toBeInTheDocument() }) describe('glitch free', () => { it('basic', () => { const baseAtom = atom(0) const derived1Atom = atom((get) => get(baseAtom)) const derived2Atom = atom((get) => get(derived1Atom)) const computeValue = vi.fn((get: Getter) => { const v0 = get(baseAtom) const v1 = get(derived1Atom) const v2 = get(derived2Atom) return `v0: ${v0}, v1: ${v1}, v2: ${v2}` }) const derived3Atom = atom(computeValue) const App = () => { const value = useAtomValue(derived3Atom) return
value: {value}
} const Control = () => { const setCount = useSetAtom(baseAtom) return ( <> ) } render( , ) expect(screen.getByText('value: v0: 0, v1: 0, v2: 0')).toBeInTheDocument() expect(computeValue).toHaveBeenCalledTimes(1) fireEvent.click(screen.getByText('button')) expect(screen.getByText('value: v0: 1, v1: 1, v2: 1')).toBeInTheDocument() expect(computeValue).toHaveBeenCalledTimes(2) }) it('same value', () => { const baseAtom = atom(0) const derived1Atom = atom((get) => get(baseAtom) * 0) const derived2Atom = atom((get) => get(derived1Atom) * 0) const computeValue = vi.fn((get: Getter) => { const v0 = get(baseAtom) const v1 = get(derived1Atom) const v2 = get(derived2Atom) return v0 + (v1 - v2) }) const derived3Atom = atom(computeValue) const App = () => { const value = useAtomValue(derived3Atom) return
value: {value}
} const Control = () => { const setCount = useSetAtom(baseAtom) return ( <> ) } render( , ) expect(screen.getByText('value: 0')).toBeInTheDocument() expect(computeValue).toHaveBeenCalledTimes(1) fireEvent.click(screen.getByText('button')) expect(screen.getByText('value: 1')).toBeInTheDocument() expect(computeValue).toHaveBeenCalledTimes(2) }) it('double chain', () => { const baseAtom = atom(0) const derived1Atom = atom((get) => get(baseAtom)) const derived2Atom = atom((get) => get(derived1Atom)) const derived3Atom = atom((get) => get(derived2Atom)) const computeValue = vi.fn((get: Getter) => { const v0 = get(baseAtom) const v1 = get(derived1Atom) const v2 = get(derived2Atom) const v3 = get(derived3Atom) return v0 + (v1 - v2) + v3 * 0 }) const derived4Atom = atom(computeValue) const App = () => { const value = useAtomValue(derived4Atom) return
value: {value}
} const Control = () => { const setCount = useSetAtom(baseAtom) return ( <> ) } render( , ) expect(screen.getByText('value: 0')).toBeInTheDocument() expect(computeValue).toHaveBeenCalledTimes(1) fireEvent.click(screen.getByText('button')) expect(screen.getByText('value: 1')).toBeInTheDocument() expect(computeValue).toHaveBeenCalledTimes(2) }) }) it('should not call read function for unmounted atoms in StrictMode (#2076)', () => { const countAtom = atom(1) let firstDerivedFn: | (((get: Getter) => number) & { mockClear: () => void }) | undefined const Component = () => { const memoizedAtomRef = useRef | null>(null) if (!memoizedAtomRef.current) { const derivedFn = vi.fn((get: Getter) => get(countAtom)) if (!firstDerivedFn) { // eslint-disable-next-line react-hooks/globals firstDerivedFn = derivedFn } memoizedAtomRef.current = atom(derivedFn) } useAtomValue(memoizedAtomRef.current) return null } const Main = () => { const [show, setShow] = useState(true) const setCount = useSetAtom(countAtom) return ( <> {show && } ) } render(
, ) fireEvent.click(screen.getByText('hide')) expect(firstDerivedFn).toBeCalledTimes(1) firstDerivedFn?.mockClear() fireEvent.click(screen.getByText('show')) expect(firstDerivedFn).toBeCalledTimes(0) }) it('works with unused hook (#2554)', async () => { const isFooAtom = atom(false) const isBarAtom = atom(false) const isActive1Atom = atom((get) => { return get(isFooAtom) && get(isBarAtom) }) const isActive2Atom = atom((get) => { return get(isFooAtom) && get(isActive1Atom) }) const activateAction = atom(undefined, async (_get, set) => { set(isFooAtom, true) set(isBarAtom, true) }) const App = () => { const activate = useSetAtom(activateAction) useAtomValue(isActive1Atom) const isRunning = useAtomValue(isActive2Atom) return (
{isRunning ? 'running' : 'not running'}
) } render( , ) expect(screen.getByText('not running')).toBeInTheDocument() fireEvent.click(screen.getByText('Activate')) expect(screen.getByText('running')).toBeInTheDocument() }) it('works with async dependencies (#2565)', async () => { const countAtom = atom(0) const countUpAction = atom(null, (_get, set) => { set(countAtom, (prev) => prev + 1) }) const totalCountAtom = atom(async (get) => { const base = await Promise.resolve(100) const count = get(countAtom) return base + count }) const Count = () => { const count = useAtomValue(totalCountAtom) return

count: {count}

} const App = () => { const up = useSetAtom(countUpAction) return (
) } await act(() => render( , ), ) // FIXME this is not working // expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('count: 100')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('Count Up'))) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('count: 101')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('Count Up'))) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('count: 102')).toBeInTheDocument() }) ================================================ FILE: tests/react/error.test.tsx ================================================ import { Component, StrictMode, Suspense, version as reactVersion, useEffect, useState, } from 'react' import type { ReactNode } from 'react' import { act, fireEvent, render, screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { useAtom } from 'jotai/react' import { atom } from 'jotai/vanilla' import { sleep } from '../test-utils' const consoleError = console.error const errorMessages: string[] = [] beforeEach(() => { vi.useFakeTimers() errorMessages.splice(0) console.error = vi.fn((err: string) => { const match = /^(.*?)(\n|$)/.exec(err) if (match?.[1]) { errorMessages.push(match[1]) } }) }) afterEach(() => { vi.useRealTimers() console.error = consoleError }) class ErrorBoundary extends Component< { children: ReactNode }, { hasError: false } | { hasError: true; error: Error } > { constructor(props: { message?: string; children: ReactNode }) { super(props) this.state = { hasError: false } } static getDerivedStateFromError(error: Error) { return { hasError: true, error } } render() { return this.state.hasError ? (
Errored: {this.state.error.message}
) : ( this.props.children ) } } it('can throw an initial error in read function', () => { const errorAtom = atom(() => { throw new Error() }) const Counter = () => { useAtom(errorAtom) return ( <>
no error
) } render( , ) expect(screen.getByText('Errored:')).toBeInTheDocument() }) it('can throw an error in read function', () => { const countAtom = atom(0) const errorAtom = atom((get) => { if (get(countAtom) === 0) { return 0 } throw new Error() }) const Counter = () => { const [, setCount] = useAtom(countAtom) const [count] = useAtom(errorAtom) return ( <>
count: {count}
no error
) } render( , ) expect(screen.getByText('no error')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect(screen.getByText('Errored:')).toBeInTheDocument() }) it('can throw an initial chained error in read function', () => { const errorAtom = atom(() => { throw new Error() }) const derivedAtom = atom((get) => get(errorAtom)) const Counter = () => { useAtom(derivedAtom) return ( <>
no error
) } render( , ) expect(screen.getByText('Errored:')).toBeInTheDocument() }) it('can throw a chained error in read function', () => { const countAtom = atom(0) const errorAtom = atom((get) => { if (get(countAtom) === 0) { return 0 } throw new Error() }) const derivedAtom = atom((get) => get(errorAtom)) const Counter = () => { const [, setCount] = useAtom(countAtom) const [count] = useAtom(derivedAtom) return ( <>
count: {count}
no error
) } render( , ) expect(screen.getByText('no error')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect(screen.getByText('Errored:')).toBeInTheDocument() }) it('can throw an initial error in async read function', async () => { const errorAtom = atom(async () => { await sleep(100) throw new Error() }) const Counter = () => { useAtom(errorAtom) return ( <>
no error
) } await act(() => render( , ), ) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('Errored:')).toBeInTheDocument() }) it('can throw an error in async read function', async () => { const countAtom = atom(0) const errorAtom = atom(async (get) => { await sleep(100) if (get(countAtom) === 0) { return 0 } throw new Error() }) const Counter = () => { const [, setCount] = useAtom(countAtom) const [count] = useAtom(errorAtom) return ( <>
count: {count}
no error
) } await act(() => render( , ), ) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('no error')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('button'))) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('Errored:')).toBeInTheDocument() }) it('can throw an error in write function', () => { const countAtom = atom(0) const errorAtom = atom( (get) => get(countAtom), () => { throw new Error('error_in_write_function') }, ) const Counter = () => { const [count, dispatch] = useAtom(errorAtom) const onClick = () => { try { dispatch() } catch (e) { console.error(e) } } return ( <>
count: {count}
no error
) } render( , ) expect(screen.getByText('no error')).toBeInTheDocument() expect(errorMessages).not.toContain('Error: error_in_write_function') fireEvent.click(screen.getByText('button')) expect(errorMessages).toContain('Error: error_in_write_function') }) it('can throw an error in async write function', async () => { const countAtom = atom(0) const errorAtom = atom( (get) => get(countAtom), async () => { throw new Error('error_in_async_write_function') }, ) const Counter = () => { const [count, dispatch] = useAtom(errorAtom) const onClick = async () => { try { await dispatch() } catch (e) { console.error(e) } } return ( <>
count: {count}
no error
) } render( , ) expect(screen.getByText('no error')).toBeInTheDocument() expect(errorMessages).not.toContain('Error: error_in_async_write_function') fireEvent.click(screen.getByText('button')) await act(() => vi.advanceTimersByTimeAsync(0)) expect(errorMessages).toContain('Error: error_in_async_write_function') }) it('can throw a chained error in write function', () => { const countAtom = atom(0) const errorAtom = atom( (get) => get(countAtom), () => { throw new Error('chained_err_in_write') }, ) const chainedAtom = atom( (get) => get(errorAtom), (_get, set) => { set(errorAtom) }, ) const Counter = () => { const [count, dispatch] = useAtom(chainedAtom) const onClick = () => { try { dispatch() } catch (e) { console.error(e) } } return ( <>
count: {count}
no error
) } render( , ) expect(screen.getByText('no error')).toBeInTheDocument() expect(errorMessages).not.toContain('Error: chained_err_in_write') fireEvent.click(screen.getByText('button')) expect(errorMessages).toContain('Error: chained_err_in_write') }) it('throws an error while updating in effect', () => { const countAtom = atom(0) const Counter = () => { const [, setCount] = useAtom(countAtom) useEffect(() => { try { setCount(() => { throw new Error('err_updating_in_effect') }) } catch (e) { console.error(e) } }, [setCount]) return ( <>
no error
) } render( , ) expect(screen.getByText('no error')).toBeInTheDocument() expect(errorMessages).toContain('Error: err_updating_in_effect') }) describe('throws an error while updating in effect cleanup', () => { const countAtom = atom(0) let doubleSetCount = false const Counter = () => { const [, setCount] = useAtom(countAtom) useEffect(() => { return () => { if (doubleSetCount) { setCount((x) => x + 1) } setCount(() => { throw new Error('err_in_effect_cleanup') }) } }, [setCount]) return ( <>
no error
) } const Main = () => { const [hide, setHide] = useState(false) return ( <> {!hide && } ) } it('[DEV-ONLY] single setCount', () => { render( <>
, ) expect(screen.getByText('no error')).toBeInTheDocument() expect(errorMessages.some((m) => m.includes('err_in_effect_cleanup'))).toBe( false, ) fireEvent.click(screen.getByText('close')) // NOTE: Conditional expect is required because behavior differs by React version // AND build mode (dev vs prod). Using it.runIf/skipIf causes production build failures. /* eslint-disable vitest/no-conditional-expect */ if (reactVersion.startsWith('17.')) { expect( errorMessages.some((m) => m.includes('err_in_effect_cleanup')), ).toBe(true) } else { expect( screen.getByText('Errored: err_in_effect_cleanup'), ).toBeInTheDocument() } /* eslint-enable vitest/no-conditional-expect */ }) it('[DEV-ONLY] double setCount', () => { doubleSetCount = true render( <>
, ) expect(screen.getByText('no error')).toBeInTheDocument() expect(errorMessages.some((m) => m.includes('err_in_effect_cleanup'))).toBe( false, ) fireEvent.click(screen.getByText('close')) // NOTE: Conditional expect is required because behavior differs by React version // AND build mode (dev vs prod). Using it.runIf/skipIf causes production build failures. /* eslint-disable vitest/no-conditional-expect */ if (reactVersion.startsWith('17.')) { expect( errorMessages.some((m) => m.includes('err_in_effect_cleanup')), ).toBe(true) } else { expect( screen.getByText('Errored: err_in_effect_cleanup'), ).toBeInTheDocument() } /* eslint-enable vitest/no-conditional-expect */ }) }) describe('error recovery', () => { const createCounter = () => { const counterAtom = atom(0) const Counter = () => { const [count, setCount] = useAtom(counterAtom) return } return { Counter, counterAtom } } it('recovers from sync errors', () => { const { counterAtom, Counter } = createCounter() const syncAtom = atom((get) => { const value = get(counterAtom) if (value === 0) { throw new Error('An error occurred') } return value }) const Display = () => { return
Value: {useAtom(syncAtom)[0]}
} render( , ) expect(screen.getByText('Errored: An error occurred')).toBeInTheDocument() fireEvent.click(screen.getByText('increment')) fireEvent.click(screen.getByText('retry')) expect(screen.getByText('Value: 1')).toBeInTheDocument() }) it('recovers from async errors', async () => { const { counterAtom, Counter } = createCounter() const asyncAtom = atom(async (get) => { const value = get(counterAtom) await sleep(100) if (value === 0) { throw new Error('An error occurred') } return value }) const Display = () => { return
Value: {useAtom(asyncAtom)[0]}
} await act(() => render( , ), ) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('Errored: An error occurred')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('increment'))) await act(() => fireEvent.click(screen.getByText('retry'))) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('Value: 1')).toBeInTheDocument() }) }) ================================================ FILE: tests/react/items.test.tsx ================================================ import { StrictMode } from 'react' import { fireEvent, render, screen } from '@testing-library/react' import { expect, it } from 'vitest' import { useAtom } from 'jotai/react' import { atom } from 'jotai/vanilla' import type { PrimitiveAtom } from 'jotai/vanilla' it('remove an item, then add another', () => { type Item = { text: string checked: boolean } let itemIndex = 0 const itemsAtom = atom[]>([]) const ListItem = ({ itemAtom, remove, }: { itemAtom: PrimitiveAtom remove: () => void }) => { const [item, setItem] = useAtom(itemAtom) const toggle = () => setItem((prev) => ({ ...prev, checked: !prev.checked })) return ( <>
{item.text} checked: {item.checked ? 'yes' : 'no'}
) } const List = () => { const [items, setItems] = useAtom(itemsAtom) const addItem = () => { setItems((prev) => [ ...prev, atom({ text: `item${++itemIndex}`, checked: false }), ]) } const removeItem = (itemAtom: PrimitiveAtom) => { setItems((prev) => prev.filter((x) => x !== itemAtom)) } return (
    {items.map((itemAtom) => ( removeItem(itemAtom)} /> ))}
) } render( , ) fireEvent.click(screen.getByText('Add')) expect(screen.getByText('item1 checked: no')).toBeInTheDocument() fireEvent.click(screen.getByText('Add')) expect(screen.getByText('item1 checked: no')).toBeInTheDocument() expect(screen.getByText('item2 checked: no')).toBeInTheDocument() fireEvent.click(screen.getByText('Check item2')) expect(screen.getByText('item1 checked: no')).toBeInTheDocument() expect(screen.getByText('item2 checked: yes')).toBeInTheDocument() fireEvent.click(screen.getByText('Remove item1')) expect(screen.getByText('item2 checked: yes')).toBeInTheDocument() fireEvent.click(screen.getByText('Add')) expect(screen.getByText('item2 checked: yes')).toBeInTheDocument() expect(screen.getByText('item3 checked: no')).toBeInTheDocument() }) it('add an item with filtered list', () => { type Item = { text: string checked: boolean } type ItemAtoms = PrimitiveAtom[] type Update = (prev: ItemAtoms) => ItemAtoms let itemIndex = 0 const itemAtomsAtom = atom([]) const setItemsAtom = atom(null, (_get, set, update: Update) => set(itemAtomsAtom, update), ) const filterAtom = atom<'all' | 'checked' | 'not-checked'>('all') const filteredAtom = atom((get) => { const filter = get(filterAtom) const items = get(itemAtomsAtom) if (filter === 'all') { return items } if (filter === 'checked') { return items.filter((atom) => get(atom).checked) } return items.filter((atom) => !get(atom).checked) }) const ListItem = ({ itemAtom, remove, }: { itemAtom: PrimitiveAtom remove: () => void }) => { const [item, setItem] = useAtom(itemAtom) const toggle = () => setItem((prev) => ({ ...prev, checked: !prev.checked })) return ( <>
{item.text} checked: {item.checked ? 'yes' : 'no'}
) } const Filter = () => { const [filter, setFilter] = useAtom(filterAtom) return ( <>
{filter}
) } const FilteredList = ({ removeItem, }: { removeItem: (itemAtom: PrimitiveAtom) => void }) => { const [items] = useAtom(filteredAtom) return (
    {items.map((itemAtom) => ( removeItem(itemAtom)} /> ))}
) } const List = () => { const [, setItems] = useAtom(setItemsAtom) const addItem = () => { setItems((prev) => [ ...prev, atom({ text: `item${++itemIndex}`, checked: false }), ]) } const removeItem = (itemAtom: PrimitiveAtom) => { setItems((prev) => prev.filter((x) => x !== itemAtom)) } return ( <> ) } render( , ) fireEvent.click(screen.getByText('Checked')) fireEvent.click(screen.getByText('Add')) fireEvent.click(screen.getByText('All')) expect(screen.getByText('item1 checked: no')).toBeInTheDocument() }) ================================================ FILE: tests/react/onmount.test.tsx ================================================ import { StrictMode, Suspense, useState } from 'react' import { act, fireEvent, render, screen } from '@testing-library/react' import { afterEach, beforeEach, expect, it, vi } from 'vitest' import { useAtom } from 'jotai/react' import { atom } from 'jotai/vanilla' import { sleep } from '../test-utils' beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) it('one atom, one effect', () => { const countAtom = atom(1) const onMountFn = vi.fn(() => {}) countAtom.onMount = onMountFn const Counter = () => { const [count, setCount] = useAtom(countAtom) return ( <>
count: {count}
) } render( <> , ) expect(screen.getByText('count: 1')).toBeInTheDocument() expect(onMountFn).toHaveBeenCalledTimes(1) fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 2')).toBeInTheDocument() expect(onMountFn).toHaveBeenCalledTimes(1) }) it('two atoms, one each', () => { const countAtom = atom(1) const countAtom2 = atom(1) const onMountFn = vi.fn(() => {}) const onMountFn2 = vi.fn(() => {}) countAtom.onMount = onMountFn countAtom2.onMount = onMountFn2 const Counter = () => { const [count, setCount] = useAtom(countAtom) const [count2, setCount2] = useAtom(countAtom2) return ( <>
count: {count}
count2: {count2}
) } render( <> , ) expect(screen.getByText('count: 1')).toBeInTheDocument() expect(screen.getByText('count2: 1')).toBeInTheDocument() expect(onMountFn).toHaveBeenCalledTimes(1) expect(onMountFn2).toHaveBeenCalledTimes(1) fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 2')).toBeInTheDocument() expect(screen.getByText('count2: 2')).toBeInTheDocument() expect(onMountFn).toHaveBeenCalledTimes(1) expect(onMountFn2).toHaveBeenCalledTimes(1) }) it('one derived atom, one onMount', () => { const countAtom = atom(1) const countAtom2 = atom((get) => get(countAtom)) const onMountFn = vi.fn(() => {}) countAtom.onMount = onMountFn const Counter = () => { const [count] = useAtom(countAtom2) return ( <>
count: {count}
) } render( <> , ) expect(screen.getByText('count: 1')).toBeInTheDocument() expect(onMountFn).toHaveBeenCalledTimes(1) }) it('mount/unmount test', () => { const countAtom = atom(1) const onUnMountFn = vi.fn() const onMountFn = vi.fn(() => onUnMountFn) countAtom.onMount = onMountFn const Counter = () => { const [count] = useAtom(countAtom) return ( <>
count: {count}
) } const Display = () => { const [display, setDisplay] = useState(true) return ( <> {display ? : null} ) } render( <> , ) expect(onMountFn).toHaveBeenCalledTimes(1) expect(onUnMountFn).toHaveBeenCalledTimes(0) fireEvent.click(screen.getByText('button')) expect(onMountFn).toHaveBeenCalledTimes(1) expect(onUnMountFn).toHaveBeenCalledTimes(1) }) it('one derived atom, one onMount for the derived one, and one for the regular atom + onUnMount', () => { const countAtom = atom(1) const derivedAtom = atom( (get) => get(countAtom), (_get, set, update: number) => { set(countAtom, update) set(derivedAtom, update) }, ) const onUnMountFn = vi.fn() const onMountFn = vi.fn(() => onUnMountFn) countAtom.onMount = onMountFn const derivedOnUnMountFn = vi.fn() const derivedOnMountFn = vi.fn(() => derivedOnUnMountFn) derivedAtom.onMount = derivedOnMountFn const Counter = () => { const [count] = useAtom(derivedAtom) return ( <>
count: {count}
) } const Display = () => { const [display, setDisplay] = useState(true) return ( <> {display ? : null} ) } render( <> , ) expect(derivedOnMountFn).toHaveBeenCalledTimes(1) expect(derivedOnUnMountFn).toHaveBeenCalledTimes(0) expect(onMountFn).toHaveBeenCalledTimes(1) expect(onUnMountFn).toHaveBeenCalledTimes(0) fireEvent.click(screen.getByText('button')) expect(derivedOnMountFn).toHaveBeenCalledTimes(1) expect(derivedOnUnMountFn).toHaveBeenCalledTimes(1) expect(onMountFn).toHaveBeenCalledTimes(1) expect(onUnMountFn).toHaveBeenCalledTimes(1) }) it('mount/unMount order', () => { const committed: number[] = [0, 0] const countAtom = atom(1) const derivedAtom = atom( (get) => get(countAtom), (_get, set, update: number) => { set(countAtom, update) set(derivedAtom, update) }, ) const onUnMountFn = vi.fn(() => { committed[0] = 0 }) const onMountFn = vi.fn(() => { committed[0] = 1 return onUnMountFn }) countAtom.onMount = onMountFn const derivedOnUnMountFn = vi.fn(() => { committed[1] = 0 }) const derivedOnMountFn = vi.fn(() => { committed[1] = 1 return derivedOnUnMountFn }) derivedAtom.onMount = derivedOnMountFn const Counter2 = () => { const [count] = useAtom(derivedAtom) return ( <>
count: {count}
) } const Counter = () => { const [count] = useAtom(countAtom) const [display, setDisplay] = useState(false) return ( <>
count: {count}
{display ? : null} ) } const Display = () => { const [display, setDisplay] = useState(false) return ( <> {display ? : null} ) } render( , ) expect(committed).toEqual([0, 0]) fireEvent.click(screen.getByText('button')) expect(committed).toEqual([1, 0]) fireEvent.click(screen.getByText('derived atom')) expect(committed).toEqual([1, 1]) fireEvent.click(screen.getByText('derived atom')) expect(committed).toEqual([1, 0]) fireEvent.click(screen.getByText('button')) expect(committed).toEqual([0, 0]) }) it('mount/unmount test with async atom', async () => { const countAtom = atom( async () => { await sleep(100) return 0 }, () => {}, ) const onUnMountFn = vi.fn() const onMountFn = vi.fn(() => onUnMountFn) countAtom.onMount = onMountFn const Counter = () => { const [count] = useAtom(countAtom) return ( <>
count: {count}
) } const Display = () => { const [display, setDisplay] = useState(true) return ( <> {display ? : null} ) } await act(() => render( <> , ), ) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count: 0')).toBeInTheDocument() expect(onMountFn).toHaveBeenCalledTimes(1) expect(onUnMountFn).toHaveBeenCalledTimes(0) fireEvent.click(screen.getByText('button')) expect(onMountFn).toHaveBeenCalledTimes(1) expect(onUnMountFn).toHaveBeenCalledTimes(1) }) it('subscription usage test', () => { const store = { count: 10, listeners: new Set<() => void>(), inc: () => { store.count += 1 store.listeners.forEach((listener) => listener()) }, } const countAtom = atom(1) countAtom.onMount = (setCount) => { const callback = () => { setCount(store.count) } store.listeners.add(callback) callback() return () => store.listeners.delete(callback) } const Counter = () => { const [count] = useAtom(countAtom) return ( <>
count: {count}
) } const Display = () => { const [display, setDisplay] = useState(true) return ( <> {display ? : 'N/A'} ) } render( , ) expect(screen.getByText('count: 10')).toBeInTheDocument() act(() => store.inc()) expect(screen.getByText('count: 11')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect(screen.getByText('N/A')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 11')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect(screen.getByText('N/A')).toBeInTheDocument() act(() => store.inc()) expect(screen.getByText('N/A')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 12')).toBeInTheDocument() }) it('subscription in base atom test', () => { const store = { count: 10, listeners: new Set<() => void>(), add: (n: number) => { store.count += n store.listeners.forEach((listener) => listener()) }, } const countAtom = atom(1) countAtom.onMount = (setCount) => { const callback = () => { setCount(store.count) } store.listeners.add(callback) callback() return () => store.listeners.delete(callback) } const derivedAtom = atom( (get) => get(countAtom), (_get, _set, n: number) => { store.add(n) }, ) const Counter = () => { const [count, add] = useAtom(derivedAtom) return ( <>
count: {count}
) } render( , ) expect(screen.getByText('count: 10')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 11')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 12')).toBeInTheDocument() }) it('create atom with onMount in async get', async () => { const store = { count: 10, listeners: new Set<() => void>(), add: (n: number) => { store.count += n store.listeners.forEach((listener) => listener()) }, } const holderAtom = atom(async () => { const countAtom = atom(1) countAtom.onMount = (setCount) => { const callback = () => { setCount(store.count) } store.listeners.add(callback) callback() return () => store.listeners.delete(callback) } return countAtom }) const derivedAtom = atom( async (get) => get(await get(holderAtom)), (_get, _set, n: number) => { store.add(n) }, ) const Counter = () => { const [count, add] = useAtom(derivedAtom) return ( <>
count: {count}
) } await act(() => render( , ), ) // FIXME this is not working // await screen.findByText('count: 1') await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('count: 10')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('button'))) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('count: 11')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('button'))) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('count: 12')).toBeInTheDocument() }) ================================================ FILE: tests/react/optimization.test.tsx ================================================ import { StrictMode, useEffect } from 'react' import { fireEvent, render, screen } from '@testing-library/react' import { expect, it } from 'vitest' import { useAtom } from 'jotai/react' import { atom } from 'jotai/vanilla' it('only relevant render function called (#156)', () => { const count1Atom = atom(0) const count2Atom = atom(0) let viewCount1 = 0 let viewCount2 = 0 const Counter1 = () => { const [count, setCount] = useAtom(count1Atom) ++viewCount1 return ( <>
count1: {count}
) } const Counter2 = () => { const [count, setCount] = useAtom(count2Atom) ++viewCount2 return ( <>
count2: {count}
) } render( <> , ) expect(screen.getByText('count1: 0')).toBeInTheDocument() expect(screen.getByText('count2: 0')).toBeInTheDocument() const viewCount1AfterMount = viewCount1 const viewCount2AfterMount = viewCount2 fireEvent.click(screen.getByText('button1')) expect(screen.getByText('count1: 1')).toBeInTheDocument() expect(screen.getByText('count2: 0')).toBeInTheDocument() expect(viewCount1).toBe(viewCount1AfterMount + 1) expect(viewCount2).toBe(viewCount2AfterMount + 0) fireEvent.click(screen.getByText('button2')) expect(screen.getByText('count1: 1')).toBeInTheDocument() expect(screen.getByText('count2: 1')).toBeInTheDocument() expect(viewCount1).toBe(viewCount1AfterMount + 1) expect(viewCount2).toBe(viewCount2AfterMount + 1) }) it('only render once using atoms with write-only atom', () => { const count1Atom = atom(0) const count2Atom = atom(0) const incrementAtom = atom(null, (_get, set, _arg) => { set(count1Atom, (c) => c + 1) set(count2Atom, (c) => c + 1) }) let viewCount = 0 const Counter = () => { const [count1] = useAtom(count1Atom) const [count2] = useAtom(count2Atom) ++viewCount return (
count1: {count1}, count2: {count2}
) } const Control = () => { const [, increment] = useAtom(incrementAtom) return } render( <> , ) expect(screen.getByText('count1: 0, count2: 0')).toBeInTheDocument() const viewCountAfterMount = viewCount fireEvent.click(screen.getByText('button')) expect(screen.getByText('count1: 1, count2: 1')).toBeInTheDocument() expect(viewCount).toBe(viewCountAfterMount + 1) fireEvent.click(screen.getByText('button')) expect(screen.getByText('count1: 2, count2: 2')).toBeInTheDocument() expect(viewCount).toBe(viewCountAfterMount + 2) }) it('useless re-renders with static atoms (#355)', () => { // check out https://codesandbox.io/s/m82r5 to see the expected re-renders const countAtom = atom(0) const unrelatedAtom = atom(0) let viewCount = 0 const Counter = () => { const [count, setCount] = useAtom(countAtom) useAtom(unrelatedAtom) ++viewCount return ( <>
count: {count}
) } render( <> , ) expect(screen.getByText('count: 0')).toBeInTheDocument() const viewCountAfterMount = viewCount fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 1')).toBeInTheDocument() expect(viewCount).toBe(viewCountAfterMount + 1) fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 2')).toBeInTheDocument() expect(viewCount).toBe(viewCountAfterMount + 2) }) it('does not re-render if value is the same (#1158)', () => { const countAtom = atom(0) let viewCount = 0 const Counter = () => { const [count, setCount] = useAtom(countAtom) ++viewCount return ( <>
count: {count}
) } render( <> , ) expect(screen.getByText('count: 0')).toBeInTheDocument() const viewCountAfterMount = viewCount fireEvent.click(screen.getByText('noop')) expect(screen.getByText('count: 0')).toBeInTheDocument() expect(viewCount).toBe(viewCountAfterMount + 0) fireEvent.click(screen.getByText('inc')) expect(screen.getByText('count: 1')).toBeInTheDocument() expect(viewCount).toBe(viewCountAfterMount + 1) fireEvent.click(screen.getByText('noop')) expect(screen.getByText('count: 1')).toBeInTheDocument() expect(viewCount).toBe(viewCountAfterMount + 1) fireEvent.click(screen.getByText('inc')) expect(screen.getByText('count: 2')).toBeInTheDocument() expect(viewCount).toBe(viewCountAfterMount + 2) }) it('no extra rerenders after commit with derived atoms (#1213)', () => { const baseAtom = atom({ count1: 0, count2: 0 }) const count1Atom = atom((get) => get(baseAtom).count1) const count2Atom = atom((get) => get(baseAtom).count2) let viewCount1 = 0 let viewCount1AfterCommit = 0 const Counter1 = () => { const [count1] = useAtom(count1Atom) ++viewCount1 useEffect(() => { viewCount1AfterCommit = viewCount1 }) return
count1: {count1}
} let viewCount2 = 0 let viewCount2AfterCommit = 0 const Counter2 = () => { const [count2] = useAtom(count2Atom) ++viewCount2 useEffect(() => { viewCount2AfterCommit = viewCount2 }) return
count2: {count2}
} const Control = () => { const [, setValue] = useAtom(baseAtom) const inc1 = () => { setValue((prev) => ({ ...prev, count1: prev.count1 + 1 })) } const inc2 = () => { setValue((prev) => ({ ...prev, count2: prev.count2 + 1 })) } return (
) } render( , ) expect(screen.getByText('count1: 0')).toBeInTheDocument() expect(screen.getByText('count2: 0')).toBeInTheDocument() expect(viewCount1 > 0).toBeTruthy() expect(viewCount2 > 0).toBeTruthy() fireEvent.click(screen.getByText('inc1')) expect(screen.getByText('count1: 1')).toBeInTheDocument() expect(screen.getByText('count2: 0')).toBeInTheDocument() expect(viewCount1).toBe(viewCount1AfterCommit) fireEvent.click(screen.getByText('inc2')) expect(screen.getByText('count1: 1')).toBeInTheDocument() expect(screen.getByText('count2: 1')).toBeInTheDocument() expect(viewCount2).toBe(viewCount2AfterCommit) fireEvent.click(screen.getByText('inc1')) expect(screen.getByText('count1: 2')).toBeInTheDocument() expect(screen.getByText('count2: 1')).toBeInTheDocument() expect(viewCount1).toBe(viewCount1AfterCommit) }) ================================================ FILE: tests/react/provider.test.tsx ================================================ import { StrictMode } from 'react' import { render, screen } from '@testing-library/react' import { expect, it } from 'vitest' import { Provider, useAtom } from 'jotai/react' import { atom, createStore } from 'jotai/vanilla' it('uses initial values from provider', () => { const countAtom = atom(1) const petAtom = atom('cat') const Display = () => { const [count] = useAtom(countAtom) const [pet] = useAtom(petAtom) return ( <>

count: {count}

pet: {pet}

) } const store = createStore() store.set(countAtom, 0) store.set(petAtom, 'dog') render( , ) expect(screen.getByText('count: 0')).toBeInTheDocument() expect(screen.getByText('pet: dog')).toBeInTheDocument() }) it('only uses initial value from provider for specific atom', () => { const countAtom = atom(1) const petAtom = atom('cat') const Display = () => { const [count] = useAtom(countAtom) const [pet] = useAtom(petAtom) return ( <>

count: {count}

pet: {pet}

) } const store = createStore() store.set(petAtom, 'dog') render( , ) expect(screen.getByText('count: 1')).toBeInTheDocument() expect(screen.getByText('pet: dog')).toBeInTheDocument() }) it('renders correctly without children', () => { const { container } = render( , ) expect(container).toBeInTheDocument() }) ================================================ FILE: tests/react/transition.test.tsx ================================================ import ReactExports, { StrictMode, Suspense, useEffect } from 'react' import { act, fireEvent, render, screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { useAtom, useAtomValue, useSetAtom } from 'jotai/react' import { atom } from 'jotai/vanilla' import { sleep } from '../test-utils' beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) const { useTransition } = ReactExports describe.skipIf(typeof useTransition !== 'function')('useTransition', () => { it('no extra commit with useTransition (#1125)', async () => { const countAtom = atom(0) const delayedAtom = atom(async (get) => { await sleep(100) return get(countAtom) }) const committed: { pending: boolean; delayed: number }[] = [] const Counter = () => { const setCount = useSetAtom(countAtom) const delayed = useAtomValue(delayedAtom) const [pending, startTransition] = useTransition() useEffect(() => { committed.push({ pending, delayed }) }) return ( <>
delayed: {delayed}
) } await act(() => render( <> , ), ) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('delayed: 0')).toBeInTheDocument() await act(async () => { fireEvent.click(screen.getByText('button')) }) expect(screen.getByText('delayed: 0')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('delayed: 1')).toBeInTheDocument() expect(committed).toEqual([ { pending: false, delayed: 0 }, { pending: true, delayed: 0 }, { pending: false, delayed: 1 }, ]) }) it('can update normal atom with useTransition (#1151)', async () => { const countAtom = atom(0) const toggleAtom = atom(false) const pendingAtom = atom((get) => { if (get(toggleAtom)) { return new Promise(() => {}) } return false }) const Counter = () => { const [count, setCount] = useAtom(countAtom) const toggle = useSetAtom(toggleAtom) useAtomValue(pendingAtom) const [pending, startTransition] = useTransition() return ( <>
count: {count}
{pending && 'pending'} ) } await act(() => render( , ), ) expect(screen.getByText('count: 0')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('toggle'))) expect(screen.getByText('pending')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('increment'))) expect(screen.getByText('count: 1')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('increment'))) expect(screen.getByText('count: 2')).toBeInTheDocument() }) }) ================================================ FILE: tests/react/types.test.tsx ================================================ import { expect, expectTypeOf, it } from 'vitest' import { useAtom, useSetAtom } from 'jotai/react' import { atom } from 'jotai/vanilla' it('useAtom should return the correct types', () => { function Component() { // primitive atom const primitiveAtom = atom(0) // NOTE: expectTypeOf is not available in TypeScript 4.0.5 and below // [ONLY-TS-4.0.5] [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore expectTypeOf(useAtom(primitiveAtom)).toEqualTypeOf< [number, (arg: number | ((prev: number) => number)) => void] >() // read-only derived atom const readonlyDerivedAtom = atom((get) => get(primitiveAtom) * 2) // [ONLY-TS-4.0.5] [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore expectTypeOf(useAtom(readonlyDerivedAtom)).toEqualTypeOf<[number, never]>() // read-write derived atom const readWriteDerivedAtom = atom( (get) => get(primitiveAtom), (get, set, value: number) => { set(primitiveAtom, get(primitiveAtom) + value) }, ) // [ONLY-TS-4.0.5] [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore expectTypeOf(useAtom(readWriteDerivedAtom)).toEqualTypeOf< [number, (arg: number) => void] >() // write-only derived atom const writeonlyDerivedAtom = atom(null, (get, set) => { set(primitiveAtom, get(primitiveAtom) - 1) }) // [ONLY-TS-4.0.5] [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore expectTypeOf(useAtom(writeonlyDerivedAtom)).toEqualTypeOf< [null, () => void] >() } expect(Component).toBeDefined() }) it('useAtom should handle inference of atoms (#1831 #1387)', () => { const fieldAtoms = { username: atom(''), age: atom(0), checked: atom(false), } const useField = (prop: T) => { return useAtom(fieldAtoms[prop]) } function Component() { const [username, setUsername] = useField('username') // [ONLY-TS-4.0.5] [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore expectTypeOf(username).toEqualTypeOf() // [ONLY-TS-4.0.5] [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore expectTypeOf(setUsername).toEqualTypeOf< (arg: string | ((prev: string) => string)) => void >() const [age, setAge] = useField('age') // [ONLY-TS-4.0.5] [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore expectTypeOf(age).toEqualTypeOf() // [ONLY-TS-4.0.5] [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore expectTypeOf(setAge).toEqualTypeOf< (arg: number | ((prev: number) => number)) => void >() const [checked, setChecked] = useField('checked') // [ONLY-TS-4.0.5] [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore expectTypeOf(checked).toEqualTypeOf() // [ONLY-TS-4.0.5] [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore expectTypeOf(setChecked).toEqualTypeOf< (arg: boolean | ((prev: boolean) => boolean)) => void >() } expect(Component).toBeDefined() }) it('useAtom should handle inference of read-only atoms', () => { const fieldAtoms = { username: atom(() => ''), age: atom(() => 0), checked: atom(() => false), } const useField = (prop: T) => { return useAtom(fieldAtoms[prop]) } function Component() { // [ONLY-TS-4.0.5] [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore expectTypeOf(useField('username')).toEqualTypeOf<[string, never]>() // [ONLY-TS-4.0.5] [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore expectTypeOf(useField('age')).toEqualTypeOf<[number, never]>() // [ONLY-TS-4.0.5] [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore expectTypeOf(useField('checked')).toEqualTypeOf<[boolean, never]>() } expect(Component).toBeDefined() }) it('useSetAtom should handle inference of atoms', () => { const fieldAtoms = { username: atom(''), age: atom(0), checked: atom(false), } const useSetField = (prop: T) => { return useSetAtom(fieldAtoms[prop]) } function Component() { const setUsername = useSetField('username') // [ONLY-TS-4.0.5] [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore expectTypeOf(setUsername).toEqualTypeOf< (arg: string | ((prev: string) => string)) => void >() const setAge = useSetField('age') // [ONLY-TS-4.0.5] [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore expectTypeOf(setAge).toEqualTypeOf< (arg: number | ((prev: number) => number)) => void >() const setChecked = useSetField('checked') // [ONLY-TS-4.0.5] [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore expectTypeOf(setChecked).toEqualTypeOf< (arg: boolean | ((prev: boolean) => boolean)) => void >() } expect(Component).toBeDefined() }) it('useAtom should handle primitive atom with one type argument', () => { const countAtom = atom(0) function Component() { const [count, setCount] = useAtom(countAtom) // [ONLY-TS-4.0.5] [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore expectTypeOf(count).toEqualTypeOf() // [ONLY-TS-4.0.5] [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore expectTypeOf(setCount).toEqualTypeOf< (arg: number | ((prev: number) => number)) => void >() } expect(Component).toBeDefined() }) ================================================ FILE: tests/react/useAtomValue.test.tsx ================================================ import { Component, StrictMode, Suspense } from 'react' import type { ReactNode } from 'react' import { act, fireEvent, render, screen } from '@testing-library/react' import { afterEach, beforeEach, expect, it, vi } from 'vitest' import { useAtomValue, useSetAtom } from 'jotai/react' import { atom } from 'jotai/vanilla' beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) it('useAtomValue basic test', async () => { const countAtom = atom(0) const Counter = () => { const count = useAtomValue(countAtom) const setCount = useSetAtom(countAtom) return ( <>
count: {count}
) } render( , ) expect(screen.getByText('count: 0')).toBeInTheDocument() fireEvent.click(screen.getByText('dispatch')) expect(screen.getByText('count: 1')).toBeInTheDocument() }) it('useAtomValue with async atom (promise)', async () => { const asyncAtom = atom(async () => 42) const AsyncComponent = () => { const value = useAtomValue(asyncAtom) return
value: {value}
} await act(() => render( , ), ) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('value: 42')).toBeInTheDocument() }) class ErrorBoundary extends Component< { children: ReactNode }, { error: Error | null } > { constructor(props: { children: ReactNode }) { super(props) this.state = { error: null } } static getDerivedStateFromError(error: Error) { return { error } } render() { if (this.state.error) { return
error: {this.state.error.message}
} return this.props.children } } it('useAtomValue with error throwing atom', async () => { const errorAtom = atom(() => { throw new Error('fail') }) const ErrorComponent = () => { useAtomValue(errorAtom) return
no error
} render( , ) expect(screen.getByText('error: fail')).toBeInTheDocument() }) it('useAtomValue with atom returning object', async () => { const objAtom = atom({ a: 1, b: 2 }) const ObjComponent = () => { const value = useAtomValue(objAtom) return (
obj: {value.a},{value.b}
) } render( , ) expect(screen.getByText('obj: 1,2')).toBeInTheDocument() }) ================================================ FILE: tests/react/useSetAtom.test.tsx ================================================ import { StrictMode, useEffect } from 'react' import type { PropsWithChildren } from 'react' import { fireEvent, render, screen } from '@testing-library/react' import { expect, it } from 'vitest' import { useAtomValue, useSetAtom } from 'jotai/react' import { atom } from 'jotai/vanilla' import { useCommitCount } from '../test-utils' it('useSetAtom does not trigger rerender in component', async () => { const countAtom = atom(0) const Displayer = () => { const count = useAtomValue(countAtom) const commits = useCommitCount() return (
count: {count}, commits: {commits}
) } const Updater = () => { const setCount = useSetAtom(countAtom) const commits = useCommitCount() return ( <>
updater commits: {commits}
) } const Parent = () => { return ( <> ) } render( <> , ) expect(screen.getByText('count: 0, commits: 1')).toBeInTheDocument() expect(screen.getByText('updater commits: 1')).toBeInTheDocument() fireEvent.click(screen.getByText('increment')) expect(screen.getByText('count: 1, commits: 2')).toBeInTheDocument() expect(screen.getByText('updater commits: 1')).toBeInTheDocument() fireEvent.click(screen.getByText('increment')) expect(screen.getByText('count: 2, commits: 3')).toBeInTheDocument() expect(screen.getByText('updater commits: 1')).toBeInTheDocument() fireEvent.click(screen.getByText('increment')) expect(screen.getByText('count: 3, commits: 4')).toBeInTheDocument() expect(screen.getByText('updater commits: 1')).toBeInTheDocument() }) it('useSetAtom with write without an argument', async () => { const countAtom = atom(0) const incrementCountAtom = atom(null, (get, set) => set(countAtom, get(countAtom) + 1), ) const Button = ({ cb, children }: PropsWithChildren<{ cb: () => void }>) => ( ) const Displayer = () => { const count = useAtomValue(countAtom) return
count: {count}
} const Updater = () => { const setCount = useSetAtom(incrementCountAtom) return } const Parent = () => { return ( <> ) } render( , ) expect(screen.getByText('count: 0')).toBeInTheDocument() fireEvent.click(screen.getByText('increment')) expect(screen.getByText('count: 1')).toBeInTheDocument() }) it('[DEV-ONLY] useSetAtom throws when called with a read-only atom', () => { expect.assertions(1) const countAtom = atom(0) const readOnlyAtom = atom((get) => get(countAtom)) function TestComponent() { const setAtom = useSetAtom(readOnlyAtom as any) useEffect(() => { expect(() => setAtom(1)).toThrow('not writable atom') }, [setAtom]) return null } render() }) ================================================ FILE: tests/react/utils/types.test.tsx ================================================ import { expect, it } from 'vitest' import { useHydrateAtoms } from 'jotai/react/utils' import { atom } from 'jotai/vanilla' it('useHydrateAtoms should not allow invalid atom types when array is passed', () => { function Component() { const countAtom = atom(0) const activeAtom = atom(true) // @ts-expect-error TS2769 [SKIP-TS-3.9.7] // [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore useHydrateAtoms([ [countAtom, 'foo'], [activeAtom, 0], ]) // @ts-expect-error TS2769 [SKIP-TS-5.0.4] [SKIP-TS-4.9.5] [SKIP-TS-4.8.4] [SKIP-TS-4.7.4] [SKIP-TS-4.6.4] [SKIP-TS-4.5.5] [SKIP-TS-4.4.4] [SKIP-TS-4.3.5] [SKIP-TS-4.2.3] [SKIP-TS-4.1.5] [SKIP-TS-4.0.5] [SKIP-TS-3.9.7] // [ONLY-TS-5.0.4] [ONLY-TS-4.9.5] [ONLY-TS-4.8.4] [ONLY-TS-4.7.4] [ONLY-TS-4.6.4] [ONLY-TS-4.5.5] [ONLY-TS-4.4.4] [ONLY-TS-4.3.5] [ONLY-TS-4.2.3] [ONLY-TS-4.1.5] [ONLY-TS-4.0.5] [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore useHydrateAtoms([ [countAtom, 1], [activeAtom, 0], ]) // @ts-expect-error TS2769 [SKIP-TS-5.0.4] [SKIP-TS-4.9.5] [SKIP-TS-4.8.4] [SKIP-TS-4.7.4] [SKIP-TS-4.6.4] [SKIP-TS-4.5.5] [SKIP-TS-4.4.4] [SKIP-TS-4.3.5] [SKIP-TS-4.2.3] [SKIP-TS-4.1.5] [SKIP-TS-4.0.5] [SKIP-TS-3.9.7] // [ONLY-TS-5.0.4] [ONLY-TS-4.9.5] [ONLY-TS-4.8.4] [ONLY-TS-4.7.4] [ONLY-TS-4.6.4] [ONLY-TS-4.5.5] [ONLY-TS-4.4.4] [ONLY-TS-4.3.5] [ONLY-TS-4.2.3] [ONLY-TS-4.1.5] [ONLY-TS-4.0.5] [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore useHydrateAtoms([ [countAtom, true], [activeAtom, false], ]) // Valid case // [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore useHydrateAtoms([ [countAtom, 1], [activeAtom, true], ]) } expect(Component).toBeDefined() }) ================================================ FILE: tests/react/utils/useAtomCallback.test.tsx ================================================ import { StrictMode, useCallback, useEffect, useState } from 'react' import { act, fireEvent, render, screen } from '@testing-library/react' import { afterEach, beforeEach, expect, it, vi } from 'vitest' import { useAtom } from 'jotai/react' import { useAtomCallback } from 'jotai/react/utils' import { atom } from 'jotai/vanilla' beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) it('useAtomCallback with get', async () => { const countAtom = atom(0) const Counter = () => { const [count, setCount] = useAtom(countAtom) return ( <>
atom count: {count}
) } const Monitor = () => { const [count, setCount] = useState(0) const readCount = useAtomCallback( useCallback((get) => { const currentCount = get(countAtom) setCount(currentCount) return currentCount }, []), ) useEffect(() => { const timer = setInterval(() => { readCount() }, 100) return () => { clearInterval(timer) } }, [readCount]) return ( <>
state count: {count}
) } render( , ) expect(screen.getByText('atom count: 0')).toBeInTheDocument() fireEvent.click(screen.getByText('dispatch')) await act(() => vi.advanceTimersByTime(100)) expect(screen.getByText('atom count: 1')).toBeInTheDocument() expect(screen.getByText('state count: 1')).toBeInTheDocument() }) it('useAtomCallback with set and update', async () => { const countAtom = atom(0) const changeableAtom = atom(0) const Counter = () => { const [count, setCount] = useAtom(countAtom) return ( <>
count: {count}
) } const Monitor = () => { const [changeableCount] = useAtom(changeableAtom) const changeCount = useAtomCallback( useCallback((get, set) => { const currentCount = get(countAtom) set(changeableAtom, currentCount) return currentCount }, []), ) useEffect(() => { const timer = setInterval(() => { changeCount() }, 100) return () => { clearInterval(timer) } }, [changeCount]) return ( <>
changeable count: {changeableCount}
) } render( , ) expect(screen.getByText('count: 0')).toBeInTheDocument() fireEvent.click(screen.getByText('dispatch')) await act(() => vi.advanceTimersByTime(100)) expect(screen.getByText('count: 1')).toBeInTheDocument() expect(screen.getByText('changeable count: 1')).toBeInTheDocument() }) it('useAtomCallback with set and update and arg', () => { const countAtom = atom(0) const App = () => { const [count] = useAtom(countAtom) const setCount = useAtomCallback( useCallback((_get, set, arg: number) => { set(countAtom, arg) return arg }, []), ) return (

count: {count}

) } render( , ) expect(screen.getByText('count: 0')).toBeInTheDocument() fireEvent.click(screen.getByText('dispatch')) expect(screen.getByText('count: 42')).toBeInTheDocument() }) it('useAtomCallback with sync atom (#1100)', () => { const countAtom = atom(0) const Counter = () => { const [count, setCount] = useAtom(countAtom) const readCount = useAtomCallback(useCallback((get) => get(countAtom), [])) useEffect(() => { const promiseOrValue = readCount() if (typeof promiseOrValue !== 'number') { throw new Error('should return number') } }, [readCount]) return ( <>
atom count: {count}
) } render( , ) expect(screen.getByText('atom count: 0')).toBeInTheDocument() fireEvent.click(screen.getByText('dispatch')) expect(screen.getByText('atom count: 1')).toBeInTheDocument() }) ================================================ FILE: tests/react/utils/useHydrateAtoms.test.tsx ================================================ import { StrictMode } from 'react' import { fireEvent, render, screen } from '@testing-library/react' import { expect, it, vi } from 'vitest' import { useAtom, useAtomValue } from 'jotai/react' import { useHydrateAtoms } from 'jotai/react/utils' import type { Atom, PrimitiveAtom, WritableAtom } from 'jotai/vanilla' import { atom } from 'jotai/vanilla' import { useCommitCount } from '../../test-utils' it('useHydrateAtoms should only hydrate on first render', () => { const countAtom = atom(0) const statusAtom = atom('fulfilled') const Counter = ({ initialCount, initialStatus, }: { initialCount: number initialStatus: string }) => { // [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore useHydrateAtoms([ [countAtom, initialCount], [statusAtom, initialStatus], ]) const [countValue, setCount] = useAtom(countAtom) const [statusValue, setStatus] = useAtom(statusAtom) return ( <>
count: {countValue}
status: {statusValue}
) } const { rerender } = render( , ) expect(screen.getByText('count: 42')).toBeInTheDocument() expect(screen.getByText('status: rejected')).toBeInTheDocument() fireEvent.click(screen.getByText('dispatch')) fireEvent.click(screen.getByText('update')) expect(screen.getByText('count: 43')).toBeInTheDocument() expect(screen.getByText('status: fulfilled')).toBeInTheDocument() rerender( , ) expect(screen.getByText('count: 43')).toBeInTheDocument() expect(screen.getByText('status: fulfilled')).toBeInTheDocument() }) it('useHydrateAtoms should only hydrate on first render using a Map', () => { const countAtom = atom(0) const activeAtom = atom(true) const Counter = ({ initialActive = false, initialCount, }: { initialActive?: boolean initialCount: number }) => { useHydrateAtoms( new Map< typeof activeAtom | typeof countAtom, typeof initialActive | typeof initialCount >([ [activeAtom, initialActive], [countAtom, initialCount], ]), ) const activeValue = useAtomValue(activeAtom) const [countValue, setCount] = useAtom(countAtom) return ( <>
is active: {activeValue ? 'yes' : 'no'}
count: {countValue}
) } const { rerender } = render( , ) expect(screen.getByText('count: 42')).toBeInTheDocument() expect(screen.getByText('is active: no')).toBeInTheDocument() fireEvent.click(screen.getByText('dispatch')) expect(screen.getByText('count: 43')).toBeInTheDocument() rerender( , ) expect(screen.getByText('count: 43')).toBeInTheDocument() expect(screen.getByText('is active: no')).toBeInTheDocument() }) it('useHydrateAtoms should not trigger unnecessary re-renders', () => { const countAtom = atom(0) const Counter = ({ initialCount }: { initialCount: number }) => { // [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore useHydrateAtoms([[countAtom, initialCount]]) const [countValue, setCount] = useAtom(countAtom) const commits = useCommitCount() return ( <>
commits: {commits}
count: {countValue}
) } render( <> , ) expect(screen.getByText('count: 42')).toBeInTheDocument() expect(screen.getByText('commits: 1')).toBeInTheDocument() fireEvent.click(screen.getByText('dispatch')) expect(screen.getByText('count: 43')).toBeInTheDocument() expect(screen.getByText('commits: 2')).toBeInTheDocument() }) it('useHydrateAtoms should work with derived atoms', () => { const countAtom = atom(0) const doubleAtom = atom((get) => get(countAtom) * 2) const Counter = ({ initialCount }: { initialCount: number }) => { // [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore useHydrateAtoms([[countAtom, initialCount]]) const [countValue, setCount] = useAtom(countAtom) const [doubleCount] = useAtom(doubleAtom) return ( <>
count: {countValue}
doubleCount: {doubleCount}
) } render( , ) expect(screen.getByText('count: 42')).toBeInTheDocument() expect(screen.getByText('doubleCount: 84')).toBeInTheDocument() fireEvent.click(screen.getByText('dispatch')) expect(screen.getByText('count: 43')).toBeInTheDocument() expect(screen.getByText('doubleCount: 86')).toBeInTheDocument() }) it('useHydrateAtoms can only restore an atom once', () => { const countAtom = atom(0) const Counter = ({ initialCount }: { initialCount: number }) => { // [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore useHydrateAtoms([[countAtom, initialCount]]) const [countValue, setCount] = useAtom(countAtom) return ( <>
count: {countValue}
) } const Counter2 = ({ count }: { count: number }) => { // [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore useHydrateAtoms([[countAtom, count]]) const [countValue, setCount] = useAtom(countAtom) return ( <>
count: {countValue}
) } const { rerender } = render( , ) expect(screen.getByText('count: 42')).toBeInTheDocument() fireEvent.click(screen.getByText('dispatch')) expect(screen.getByText('count: 43')).toBeInTheDocument() rerender( , ) expect(screen.getByText('count: 43')).toBeInTheDocument() fireEvent.click(screen.getByText('dispatch')) expect(screen.getByText('count: 44')).toBeInTheDocument() }) it('useHydrateAtoms should respect onMount', () => { const countAtom = atom(0) const onMountFn = vi.fn(() => {}) countAtom.onMount = onMountFn const Counter = ({ initialCount }: { initialCount: number }) => { // [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore useHydrateAtoms([[countAtom, initialCount]]) const [countValue] = useAtom(countAtom) return
count: {countValue}
} render( <> , ) expect(screen.getByText('count: 42')).toBeInTheDocument() expect(onMountFn).toHaveBeenCalledTimes(1) }) it('passing dangerouslyForceHydrate to useHydrateAtoms will re-hydrated atoms', () => { const countAtom = atom(0) const statusAtom = atom('fulfilled') const Counter = ({ initialCount, initialStatus, dangerouslyForceHydrate = false, }: { initialCount: number initialStatus: string dangerouslyForceHydrate?: boolean }) => { useHydrateAtoms( // [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore [ [countAtom, initialCount], [statusAtom, initialStatus], ], { dangerouslyForceHydrate, }, ) const [countValue, setCount] = useAtom(countAtom) const [statusValue, setStatus] = useAtom(statusAtom) return ( <>
count: {countValue}
status: {statusValue}
) } const { rerender } = render( , ) expect(screen.getByText('count: 42')).toBeInTheDocument() expect(screen.getByText('status: rejected')).toBeInTheDocument() fireEvent.click(screen.getByText('dispatch')) fireEvent.click(screen.getByText('update')) expect(screen.getByText('count: 43')).toBeInTheDocument() expect(screen.getByText('status: fulfilled')).toBeInTheDocument() rerender( , ) expect(screen.getByText('count: 43')).toBeInTheDocument() expect(screen.getByText('status: fulfilled')).toBeInTheDocument() rerender( , ) expect(screen.getByText('count: 11')).toBeInTheDocument() expect(screen.getByText('status: rejected')).toBeInTheDocument() }) // types-only tests // eslint-disable-next-line vitest/expect-expect it('types: useHydrateAtoms should enforce tuple/value/args types', () => { const numberAtom = {} as PrimitiveAtom const booleanAtom = {} as PrimitiveAtom const stringUnionAtom = {} as PrimitiveAtom<'pending' | 'fulfilled'> const readOnlyAtom = {} as Atom const writeOnlySingleNumberAtom = {} as WritableAtom const writeOnlyDoubleNumberAtom = {} as WritableAtom< number, [number, number], void > // positive cases (should type-check) /* eslint-disable @typescript-eslint/no-unused-expressions */ ;() => // [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore useHydrateAtoms([ [numberAtom, 1], [booleanAtom, true], [stringUnionAtom, 'fulfilled'], ] as const) // [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore ;() => useHydrateAtoms([[writeOnlySingleNumberAtom, 2]]) // [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore ;() => useHydrateAtoms([[writeOnlyDoubleNumberAtom, 1, 2]]) ;() => // [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore useHydrateAtoms( new Map< typeof numberAtom | typeof stringUnionAtom, number | 'pending' | 'fulfilled' >([ [numberAtom, 123], [stringUnionAtom, 'pending'], ]), ) type AnyWritableAtom = WritableAtom // [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore ;() => useHydrateAtoms([] as (readonly [AnyWritableAtom, unknown])[]) // negative cases (should fail type-check) // @ts-expect-error wrong value type for primitive atom [SKIP-TS-3.9.7] // [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore ;() => useHydrateAtoms([[numberAtom, 'oops']]) // @ts-expect-error wrong value type for boolean atom [SKIP-TS-3.9.7] // [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore ;() => useHydrateAtoms([[booleanAtom, 0]]) // @ts-expect-error read-only atom is not writable [SKIP-TS-4.2.3] [SKIP-TS-4.1.5] [SKIP-TS-4.0.5] [SKIP-TS-3.9.7] // [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore ;() => useHydrateAtoms([[readOnlyAtom, 1]]) // @ts-expect-error wrong arg type for writable derived atom [SKIP-TS-3.9.7] // [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore ;() => useHydrateAtoms([[writeOnlySingleNumberAtom, 'x']]) // @ts-expect-error missing one arg for writable derived with two args [SKIP-TS-3.9.7] // [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore ;() => useHydrateAtoms([[writeOnlyDoubleNumberAtom, 1]]) // @ts-expect-error too many args for writable derived with two args [SKIP-TS-3.9.7] // [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore ;() => useHydrateAtoms([[writeOnlyDoubleNumberAtom, 1, 2, 3]]) // @ts-expect-error map with read-only atom key [SKIP-TS-4.2.3] [SKIP-TS-4.1.5] [SKIP-TS-4.0.5] [SKIP-TS-3.9.7] // [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore ;() => useHydrateAtoms(new Map([[readOnlyAtom, 1]])) /* eslint-enable @typescript-eslint/no-unused-expressions */ }) ================================================ FILE: tests/react/utils/useReducerAtom.test.tsx ================================================ import { StrictMode } from 'react' import { fireEvent, render, screen } from '@testing-library/react' import { afterEach, beforeEach, expect, it, vi } from 'vitest' import { useReducerAtom } from 'jotai/react/utils' import { atom } from 'jotai/vanilla' let savedConsoleWarn: any beforeEach(() => { savedConsoleWarn = console.warn console.warn = vi.fn() }) afterEach(() => { console.warn = savedConsoleWarn }) it('useReducerAtom with no action argument', () => { const countAtom = atom(0) const reducer = (state: number) => state + 2 const Parent = () => { const [count, dispatch] = useReducerAtom(countAtom, reducer) return ( <>
count: {count}
) } render( , ) expect(screen.getByText('count: 0')).toBeInTheDocument() fireEvent.click(screen.getByText('dispatch')) expect(screen.getByText('count: 2')).toBeInTheDocument() fireEvent.click(screen.getByText('dispatch')) expect(screen.getByText('count: 4')).toBeInTheDocument() }) it('useReducerAtom with optional action argument', () => { const countAtom = atom(0) const reducer = (state: number, action?: 'INCREASE' | 'DECREASE') => { switch (action) { case 'INCREASE': return state + 1 case 'DECREASE': return state - 1 case undefined: return state } } const Parent = () => { const [count, dispatch] = useReducerAtom(countAtom, reducer) return ( <>
count: {count}
) } render( , ) expect(screen.getByText('count: 0')).toBeInTheDocument() fireEvent.click(screen.getByText('dispatch INCREASE')) expect(screen.getByText('count: 1')).toBeInTheDocument() fireEvent.click(screen.getByText('dispatch empty')) expect(screen.getByText('count: 1')).toBeInTheDocument() fireEvent.click(screen.getByText('dispatch DECREASE')) expect(screen.getByText('count: 0')).toBeInTheDocument() }) it('useReducerAtom with non-optional action argument', () => { const countAtom = atom(0) const reducer = (state: number, action: 'INCREASE' | 'DECREASE') => { switch (action) { case 'INCREASE': return state + 1 case 'DECREASE': return state - 1 } } const Parent = () => { const [count, dispatch] = useReducerAtom(countAtom, reducer) return ( <>
count: {count}
) } render( , ) expect(screen.getByText('count: 0')).toBeInTheDocument() fireEvent.click(screen.getByText('dispatch INCREASE')) expect(screen.getByText('count: 1')).toBeInTheDocument() fireEvent.click(screen.getByText('dispatch DECREASE')) expect(screen.getByText('count: 0')).toBeInTheDocument() }) ================================================ FILE: tests/react/utils/useResetAtom.test.tsx ================================================ import { StrictMode } from 'react' import { fireEvent, render, screen } from '@testing-library/react' import { expect, it } from 'vitest' import { useAtom } from 'jotai/react' import { useResetAtom } from 'jotai/react/utils' import { atom } from 'jotai/vanilla' import { RESET, atomWithReducer, atomWithReset } from 'jotai/vanilla/utils' it('atomWithReset resets to its first value', () => { const countAtom = atomWithReset(0) const Parent = () => { const [count, setValue] = useAtom(countAtom) const resetAtom = useResetAtom(countAtom) return ( <>
count: {count}
) } render( , ) expect(screen.getByText('count: 0')).toBeInTheDocument() fireEvent.click(screen.getByText('increment')) expect(screen.getByText('count: 1')).toBeInTheDocument() fireEvent.click(screen.getByText('increment')) expect(screen.getByText('count: 2')).toBeInTheDocument() fireEvent.click(screen.getByText('increment')) expect(screen.getByText('count: 3')).toBeInTheDocument() fireEvent.click(screen.getByText('reset')) expect(screen.getByText('count: 0')).toBeInTheDocument() fireEvent.click(screen.getByText('set to 10')) expect(screen.getByText('count: 10')).toBeInTheDocument() fireEvent.click(screen.getByText('increment')) expect(screen.getByText('count: 11')).toBeInTheDocument() fireEvent.click(screen.getByText('increment')) expect(screen.getByText('count: 12')).toBeInTheDocument() fireEvent.click(screen.getByText('increment')) expect(screen.getByText('count: 13')).toBeInTheDocument() }) it('atomWithReset reset based on previous value', () => { const countAtom = atomWithReset(0) const Parent = () => { const [count, setValue] = useAtom(countAtom) return ( <>
count: {count}
) } render( , ) expect(screen.getByText('count: 0')).toBeInTheDocument() fireEvent.click(screen.getByText('increment till 3, then reset')) expect(screen.getByText('count: 1')).toBeInTheDocument() fireEvent.click(screen.getByText('increment till 3, then reset')) expect(screen.getByText('count: 2')).toBeInTheDocument() fireEvent.click(screen.getByText('increment till 3, then reset')) expect(screen.getByText('count: 3')).toBeInTheDocument() fireEvent.click(screen.getByText('increment till 3, then reset')) expect(screen.getByText('count: 0')).toBeInTheDocument() }) it('atomWithReset through read-write atom', () => { const primitiveAtom = atomWithReset(0) const countAtom = atom( (get) => get(primitiveAtom), (_get, set, newValue: number | typeof RESET) => set(primitiveAtom, newValue as never), ) const Parent = () => { const [count, setValue] = useAtom(countAtom) const resetAtom = useResetAtom(countAtom) return ( <>
count: {count}
) } render( , ) expect(screen.getByText('count: 0')).toBeInTheDocument() fireEvent.click(screen.getByText('set to 10')) expect(screen.getByText('count: 10')).toBeInTheDocument() fireEvent.click(screen.getByText('reset')) expect(screen.getByText('count: 0')).toBeInTheDocument() }) it('useResetAtom with custom atom', () => { const reducer = (state: number, action: 'INCREASE' | typeof RESET) => { switch (action) { case 'INCREASE': return state + 1 case RESET: return 0 default: throw new Error('unknown action') } } const countAtom = atomWithReducer(0, reducer) const Parent = () => { const [count, dispatch] = useAtom(countAtom) const resetAtom = useResetAtom(countAtom) return ( <>
count: {count}
) } render( , ) expect(screen.getByText('count: 0')).toBeInTheDocument() fireEvent.click(screen.getByText('increment')) expect(screen.getByText('count: 1')).toBeInTheDocument() fireEvent.click(screen.getByText('increment')) expect(screen.getByText('count: 2')).toBeInTheDocument() fireEvent.click(screen.getByText('increment')) expect(screen.getByText('count: 3')).toBeInTheDocument() fireEvent.click(screen.getByText('reset')) expect(screen.getByText('count: 0')).toBeInTheDocument() }) ================================================ FILE: tests/react/vanilla-utils/atomFamily.test.tsx ================================================ import { StrictMode, Suspense, useState } from 'react' import { act, fireEvent, render, screen } from '@testing-library/react' import { afterEach, beforeEach, expect, it, vi } from 'vitest' import { useAtom, useSetAtom } from 'jotai/react' import { atom } from 'jotai/vanilla' import type { SetStateAction, WritableAtom } from 'jotai/vanilla' import { atomFamily } from 'jotai/vanilla/utils' import { sleep } from '../../test-utils' beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) it('new atomFamily impl', () => { const myFamily = atomFamily((param: string) => atom(param)) const Displayer = ({ index }: { index: string }) => { const [count] = useAtom(myFamily(index)) return
count: {count}
} render( , ) expect(screen.getByText('count: a')).toBeInTheDocument() }) it('primitive atomFamily returns same reference for same parameters', () => { const myFamily = atomFamily((num: number) => atom({ num })) expect(myFamily(0)).toEqual(myFamily(0)) expect(myFamily(0)).not.toEqual(myFamily(1)) expect(myFamily(1)).not.toEqual(myFamily(0)) }) it('read-only derived atomFamily returns same reference for same parameters', () => { const arrayAtom = atom([0]) const myFamily = atomFamily((num: number) => atom((get) => get(arrayAtom)[num] as number), ) expect(myFamily(0)).toEqual(myFamily(0)) expect(myFamily(0)).not.toEqual(myFamily(1)) expect(myFamily(1)).not.toEqual(myFamily(0)) }) it('removed atom creates a new reference', () => { const bigAtom = atom([0]) const myFamily = atomFamily((num: number) => atom((get) => get(bigAtom)[num] as number), ) const savedReference = myFamily(0) expect(savedReference).toEqual(myFamily(0)) myFamily.remove(0) const newReference = myFamily(0) expect(savedReference).not.toEqual(newReference) myFamily.remove(1337) expect(myFamily(0)).toEqual(newReference) }) it('primitive atomFamily initialized with props', () => { const myFamily = atomFamily((param: number) => atom(param)) const Displayer = ({ index }: { index: number }) => { const [count, setCount] = useAtom(myFamily(index)) return (
count: {count}
) } const Parent = () => { const [index, setIndex] = useState(1) return (
) } render( , ) expect(screen.getByText('count: 1')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 11')).toBeInTheDocument() fireEvent.click(screen.getByText('increment')) expect(screen.getByText('count: 2')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 12')).toBeInTheDocument() }) it('derived atomFamily functionality as usual', () => { const arrayAtom = atom([0, 0, 0]) const myFamily = atomFamily((param: number) => atom( (get) => get(arrayAtom)[param] as number, (_, set, update) => { set(arrayAtom, (oldArray) => { if (typeof oldArray[param] === 'undefined') return oldArray const newValue = typeof update === 'function' ? update(oldArray[param] as number) : update const newArray = [ ...oldArray.slice(0, param), newValue, ...oldArray.slice(param + 1), ] return newArray }) }, ), ) const Displayer = ({ index, countAtom, }: { index: number countAtom: WritableAtom], void> }) => { const [count, setCount] = useAtom(countAtom) return (
index: {index}, count: {count}
) } const indicesAtom = atom((get) => [...new Array(get(arrayAtom).length)]) const Parent = () => { const [indices] = useAtom(indicesAtom) return (
{indices.map((_, index) => ( ))}
) } render( , ) expect(screen.getByText('index: 0, count: 0')).toBeInTheDocument() expect(screen.getByText('index: 1, count: 0')).toBeInTheDocument() expect(screen.getByText('index: 2, count: 0')).toBeInTheDocument() fireEvent.click(screen.getByText('increment #1')) expect(screen.getByText('index: 0, count: 0')).toBeInTheDocument() expect(screen.getByText('index: 1, count: 1')).toBeInTheDocument() expect(screen.getByText('index: 2, count: 0')).toBeInTheDocument() fireEvent.click(screen.getByText('increment #0')) expect(screen.getByText('index: 0, count: 1')).toBeInTheDocument() expect(screen.getByText('index: 1, count: 1')).toBeInTheDocument() expect(screen.getByText('index: 2, count: 0')).toBeInTheDocument() fireEvent.click(screen.getByText('increment #2')) expect(screen.getByText('index: 0, count: 1')).toBeInTheDocument() expect(screen.getByText('index: 1, count: 1')).toBeInTheDocument() expect(screen.getByText('index: 2, count: 1')).toBeInTheDocument() }) it('custom equality function work', () => { const bigAtom = atom([0]) const badFamily = atomFamily((num: { index: number }) => atom((get) => get(bigAtom)[num.index] as number), ) const goodFamily = atomFamily( (num: { index: number }) => atom((get) => get(bigAtom)[num.index] as number), (l, r) => l.index === r.index, ) expect(badFamily({ index: 0 })).not.toEqual(badFamily({ index: 0 })) expect(badFamily({ index: 0 })).not.toEqual(badFamily({ index: 0 })) expect(goodFamily({ index: 0 })).toEqual(goodFamily({ index: 0 })) expect(goodFamily({ index: 0 })).not.toEqual(goodFamily({ index: 1 })) }) it('a derived atom from an async atomFamily (#351)', async () => { const countAtom = atom(1) const getAsyncAtom = atomFamily((n: number) => atom(async () => { await sleep(100) return n + 10 }), ) const derivedAtom = atom((get) => get(getAsyncAtom(get(countAtom)))) const Counter = () => { const setCount = useSetAtom(countAtom) const [derived] = useAtom(derivedAtom) return ( <>
derived: {derived}
) } await act(() => render( , ), ) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('derived: 11')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('button'))) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('derived: 12')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('button'))) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('derived: 13')).toBeInTheDocument() }) it('setShouldRemove with custom equality function', () => { const myFamily = atomFamily( (num: { index: number }) => atom(num), (l, r) => l.index === r.index, ) let firstTime = true myFamily.setShouldRemove(() => { if (firstTime) { firstTime = false return true } return false }) const family1 = myFamily({ index: 0 }) const family2 = myFamily({ index: 0 }) const family3 = myFamily({ index: 0 }) expect(family1).not.toBe(family2) expect(family2).toBe(family3) }) ================================================ FILE: tests/react/vanilla-utils/atomWithDefault.test.tsx ================================================ import { StrictMode, Suspense } from 'react' import { act, fireEvent, render, screen } from '@testing-library/react' import { afterEach, beforeEach, expect, it, vi } from 'vitest' import { useAtom } from 'jotai/react' import { atom } from 'jotai/vanilla' import { RESET, atomWithDefault } from 'jotai/vanilla/utils' import { sleep } from '../../test-utils' beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) it('simple sync get default', () => { const count1Atom = atom(1) const count2Atom = atomWithDefault((get) => get(count1Atom) * 2) const Counter = () => { const [count1, setCount1] = useAtom(count1Atom) const [count2, setCount2] = useAtom(count2Atom) return ( <>
count1: {count1}, count2: {count2}
) } render( , ) expect(screen.getByText('count1: 1, count2: 2')).toBeInTheDocument() fireEvent.click(screen.getByText('button1')) expect(screen.getByText('count1: 2, count2: 4')).toBeInTheDocument() fireEvent.click(screen.getByText('button2')) expect(screen.getByText('count1: 2, count2: 5')).toBeInTheDocument() fireEvent.click(screen.getByText('button1')) expect(screen.getByText('count1: 3, count2: 5')).toBeInTheDocument() }) it('simple async get default', async () => { const count1Atom = atom(1) const count2Atom = atomWithDefault(async (get) => { await sleep(100) return get(count1Atom) * 2 }) const Counter = () => { const [count1, setCount1] = useAtom(count1Atom) const [count2, setCount2] = useAtom(count2Atom) return ( <>
count1: {count1}, count2: {count2}
) } await act(() => render( , ), ) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count1: 1, count2: 2')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('button1'))) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count1: 2, count2: 4')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('button2'))) await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count1: 2, count2: 5')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('button1'))) await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count1: 3, count2: 5')).toBeInTheDocument() }) it('refresh sync atoms to default values', () => { const count1Atom = atom(1) const count2Atom = atomWithDefault((get) => get(count1Atom) * 2) const Counter = () => { const [count1, setCount1] = useAtom(count1Atom) const [count2, setCount2] = useAtom(count2Atom) return ( <>
count1: {count1}, count2: {count2}
) } render( , ) expect(screen.getByText('count1: 1, count2: 2')).toBeInTheDocument() fireEvent.click(screen.getByText('button1')) expect(screen.getByText('count1: 2, count2: 4')).toBeInTheDocument() fireEvent.click(screen.getByText('button2')) expect(screen.getByText('count1: 2, count2: 5')).toBeInTheDocument() fireEvent.click(screen.getByText('button1')) expect(screen.getByText('count1: 3, count2: 5')).toBeInTheDocument() fireEvent.click(screen.getByText('Refresh count2')) expect(screen.getByText('count1: 3, count2: 6')).toBeInTheDocument() fireEvent.click(screen.getByText('button1')) expect(screen.getByText('count1: 4, count2: 8')).toBeInTheDocument() }) it('refresh async atoms to default values', async () => { const count1Atom = atom(1) const count2Atom = atomWithDefault(async (get) => { await new Promise((resolve) => setTimeout(resolve, 100)) return get(count1Atom) * 2 }) const Counter = () => { const [count1, setCount1] = useAtom(count1Atom) const [count2, setCount2] = useAtom(count2Atom) return ( <>
count1: {count1}, count2: {count2}
) } await act(() => render( , ), ) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count1: 1, count2: 2')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('button1'))) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count1: 2, count2: 4')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('button2'))) await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count1: 2, count2: 5')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('button1'))) await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count1: 3, count2: 5')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('Refresh count2'))) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count1: 3, count2: 6')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('button1'))) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count1: 4, count2: 8')).toBeInTheDocument() }) it('can be set synchronously by passing value', () => { const countAtom = atomWithDefault(() => 1) const Counter = () => { const [count, setCount] = useAtom(countAtom) return ( <>
count: {count}
) } render( , ) expect(screen.getByText('count: 1')).toBeInTheDocument() fireEvent.click(screen.getByRole('button', { name: 'Set to 10' })) expect(screen.getByText('count: 10')).toBeInTheDocument() }) ================================================ FILE: tests/react/vanilla-utils/atomWithObservable.test.tsx ================================================ import { Component, StrictMode, Suspense, useState } from 'react' import type { ReactElement, ReactNode } from 'react' import { act, fireEvent, render, screen } from '@testing-library/react' import { BehaviorSubject, Observable, Subject, delay, interval, map, of, switchMap, take, } from 'rxjs' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { fromValue, makeSubject, pipe, toObservable } from 'wonka' import { useAtom, useAtomValue, useSetAtom } from 'jotai/react' import { atom, createStore } from 'jotai/vanilla' import { atomWithObservable } from 'jotai/vanilla/utils' const consoleError = console.error beforeEach(() => { vi.useFakeTimers() // A workaround for missing performance.mark after using fake timers // https://github.com/pmndrs/jotai/pull/1913#discussion_r1186527192 if (!performance.mark) { performance.mark = (() => {}) as any performance.clearMarks = (() => {}) as any performance.clearMeasures = (() => {}) as any } // suppress error log console.error = vi.fn((...args: unknown[]) => { const message = String(args) if ( message.includes('at ErrorBoundary') || message.includes('Test Error') ) { return } return consoleError(...args) }) }) afterEach(() => { vi.runAllTimers() vi.useRealTimers() console.error = consoleError }) class ErrorBoundary extends Component< { children: ReactNode }, { error: string } > { state = { error: '', } static getDerivedStateFromError(error: Error) { return { error: error.message } } render() { if (this.state.error) { return
Error: {this.state.error}
} return this.props.children } } it('count state', () => { const observableAtom = atomWithObservable(() => of(1)) const Counter = () => { const [state] = useAtom(observableAtom) return <>count: {state} } render( , ) expect(screen.getByText('count: 1')).toBeInTheDocument() }) it('count state with Symbol.observable', () => { const subject = new BehaviorSubject(1) const observable = { [Symbol.observable]: () => ({ subscribe: subject.subscribe.bind(subject), }), } const observableAtom = atomWithObservable(() => observable) const Counter = () => { const [state] = useAtom(observableAtom) return <>count: {state} } render( , ) expect(screen.getByText('count: 1')).toBeInTheDocument() }) it('writable count state', () => { const subject = new BehaviorSubject(1) const observableAtom = atomWithObservable(() => subject) const Counter = () => { const [state, dispatch] = useAtom(observableAtom) return ( <> count: {state} ) } render( , ) expect(screen.getByText('count: 1')).toBeInTheDocument() act(() => subject.next(2)) act(() => vi.advanceTimersByTime(0)) expect(screen.getByText('count: 2')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 9')).toBeInTheDocument() expect(subject.value).toBe(9) }) it('writable count state without initial value', async () => { const subject = new Subject() const observableAtom = atomWithObservable(() => subject) const CounterValue = () => { const state = useAtomValue(observableAtom) return <>count: {state} } const CounterButton = () => { const dispatch = useSetAtom(observableAtom) return } await act(() => render( , ), ) expect(screen.getByText('loading')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('count: 9')).toBeInTheDocument() act(() => subject.next(3)) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('count: 3')).toBeInTheDocument() }) it('writable count state with delayed value', async () => { const subject = new Subject() const observableAtom = atomWithObservable(() => { const observable = of(1).pipe(delay(10 * 1000)) observable.subscribe((n) => subject.next(n)) return subject }) const Counter = () => { const [state, dispatch] = useAtom(observableAtom) return ( <> count: {state} ) } await act(() => render( , ), ) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10000)) expect(screen.getByText('count: 1')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 9')).toBeInTheDocument() }) it('only subscribe once per atom', async () => { const subject = new Subject() let totalSubscriptions = 0 const observable = new Observable((subscriber) => { totalSubscriptions++ subject.subscribe(subscriber) }) const observableAtom = atomWithObservable(() => observable) const Counter = () => { const [state] = useAtom(observableAtom) return <>count: {state} } let rerender: (ui: ReactNode) => void await act( () => ({ rerender } = render( <> , )), ) expect(screen.getByText('loading')).toBeInTheDocument() act(() => subject.next(1)) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('count: 1')).toBeInTheDocument() rerender!(
) expect(totalSubscriptions).toEqual(1) rerender!( <> , ) act(() => subject.next(2)) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('count: 2')).toBeInTheDocument() expect(totalSubscriptions).toEqual(2) }) it('cleanup subscription', async () => { const subject = new Subject() let activeSubscriptions = 0 const observable = new Observable((subscriber) => { activeSubscriptions++ subject.subscribe(subscriber) return () => { activeSubscriptions-- } }) const observableAtom = atomWithObservable(() => observable) const Counter = () => { const [state] = useAtom(observableAtom) return <>count: {state} } let rerender: (ui: ReactNode) => void await act( () => ({ rerender } = render( , )), ) expect(screen.getByText('loading')).toBeInTheDocument() subject.next(1) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('count: 1')).toBeInTheDocument() expect(activeSubscriptions).toEqual(1) rerender!(
) await act(() => vi.advanceTimersByTimeAsync(0)) expect(activeSubscriptions).toEqual(0) }) it('resubscribe on remount', async () => { const subject = new Subject() const observableAtom = atomWithObservable(() => subject) const Counter = () => { const [state] = useAtom(observableAtom) return <>count: {state} } const Toggle = ({ children }: { children: ReactElement }) => { const [visible, setVisible] = useState(true) return ( <> {visible && children} ) } await act(() => render( , ), ) expect(screen.getByText('loading')).toBeInTheDocument() act(() => subject.next(1)) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('count: 1')).toBeInTheDocument() fireEvent.click(screen.getByText('Toggle')) fireEvent.click(screen.getByText('Toggle')) act(() => subject.next(2)) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('count: 2')).toBeInTheDocument() }) it("count state with initialValue doesn't suspend", () => { const subject = new Subject() const observableAtom = atomWithObservable(() => subject, { initialValue: 5 }) const Counter = () => { const [state] = useAtom(observableAtom) return <>count: {state} } render( , ) expect(screen.getByText('count: 5')).toBeInTheDocument() act(() => subject.next(10)) act(() => vi.advanceTimersByTime(0)) expect(screen.getByText('count: 10')).toBeInTheDocument() }) it('writable count state with initialValue', () => { const subject = new Subject() const observableAtom = atomWithObservable(() => subject, { initialValue: 5 }) const Counter = () => { const [state, dispatch] = useAtom(observableAtom) return ( <> count: {state} ) } render( , ) expect(screen.getByText('count: 5')).toBeInTheDocument() act(() => subject.next(1)) act(() => vi.advanceTimersByTime(0)) expect(screen.getByText('count: 1')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) act(() => vi.advanceTimersByTime(0)) expect(screen.getByText('count: 9')).toBeInTheDocument() }) it('writable count state with error', async () => { const subject = new Subject() const observableAtom = atomWithObservable(() => subject) const Counter = () => { const [state, dispatch] = useAtom(observableAtom) return ( <> count: {state} ) } await act(() => render( , ), ) expect(screen.getByText('loading')).toBeInTheDocument() subject.error(new Error('Test Error')) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('Error: Test Error')).toBeInTheDocument() }) it('synchronous subscription with initial value', () => { const observableAtom = atomWithObservable(() => of(1), { initialValue: 5 }) const Counter = () => { const [state] = useAtom(observableAtom) return <>count: {state} } render( , ) expect(screen.getByText('count: 1')).toBeInTheDocument() }) it('synchronous subscription with BehaviorSubject', () => { const observableAtom = atomWithObservable(() => new BehaviorSubject(1)) const Counter = () => { const [state] = useAtom(observableAtom) return <>count: {state} } render( , ) expect(screen.getByText('count: 1')).toBeInTheDocument() }) it('synchronous subscription with already emitted value', () => { const observableAtom = atomWithObservable(() => of(1)) const Counter = () => { const [state] = useAtom(observableAtom) return <>count: {state} } render( , ) expect(screen.getByText('count: 1')).toBeInTheDocument() }) it('with falsy initial value', () => { const observableAtom = atomWithObservable(() => new Subject(), { initialValue: 0, }) const Counter = () => { const [state] = useAtom(observableAtom) return <>count: {state} } render( , ) expect(screen.getByText('count: 0')).toBeInTheDocument() }) it('with initially emitted undefined value', async () => { const subject = new Subject() const observableAtom = atomWithObservable(() => subject) const Counter = () => { const [state] = useAtom(observableAtom) return <>count: {state === undefined ? '-' : state} } await act(() => render( , ), ) expect(screen.getByText('loading')).toBeInTheDocument() act(() => subject.next(undefined)) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('count: -')).toBeInTheDocument() act(() => subject.next(1)) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('count: 1')).toBeInTheDocument() }) it("don't omit values emitted between init and mount", async () => { const subject = new Subject() const observableAtom = atomWithObservable(() => subject) const Counter = () => { const [state, dispatch] = useAtom(observableAtom) return ( <> count: {state} ) } await act(() => render( , ), ) expect(screen.getByText('loading')).toBeInTheDocument() subject.next(1) subject.next(2) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('count: 2')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 9')).toBeInTheDocument() }) describe('error handling', () => { class ErrorBoundary extends Component< { message?: string; retry?: () => void; children: ReactNode }, { hasError: boolean } > { constructor(props: { message?: string; children: ReactNode }) { super(props) this.state = { hasError: false } } static getDerivedStateFromError() { return { hasError: true } } render() { return this.state.hasError ? (
{this.props.message || 'errored'} {this.props.retry && ( )}
) : ( this.props.children ) } } it('can catch error in error boundary', async () => { const subject = new Subject() const countAtom = atomWithObservable(() => subject) const Counter = () => { const [count] = useAtom(countAtom) return ( <>
count: {count}
) } await act(() => render( , ), ) expect(screen.getByText('loading')).toBeInTheDocument() subject.error(new Error('Test Error')) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('errored')).toBeInTheDocument() }) it('can recover from error with dependency', async () => { const baseAtom = atom(0) const countAtom = atomWithObservable((get) => { const base = get(baseAtom) if (base % 2 === 0) { const subject = new Subject() const observable = of(1).pipe(delay(10 * 1000)) observable.subscribe(() => subject.error(new Error('Test Error'))) return subject } const observable = of(base).pipe(delay(10 * 1000)) return observable }) const Counter = () => { const [count] = useAtom(countAtom) const setBase = useSetAtom(baseAtom) return ( <>
count: {count}
) } const App = () => { const setBase = useSetAtom(baseAtom) const retry = () => { setBase((c) => c + 1) } return ( ) } await act(() => render( , ), ) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10000)) expect(screen.getByText('errored')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('retry'))) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10000)) expect(screen.getByText('count: 1')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('next'))) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10000)) expect(screen.getByText('errored')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('retry'))) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10000)) expect(screen.getByText('count: 3')).toBeInTheDocument() }) it('can recover with intermediate atom', async () => { let count = -1 let willThrowError = false const refreshAtom = atom(0) const countObservableAtom = atom((get) => { get(refreshAtom) const observableAtom = atomWithObservable(() => { willThrowError = !willThrowError ++count const subject = new Subject<{ data: number } | { error: Error }>() setTimeout(() => { if (willThrowError) { subject.next({ error: new Error('Test Error') }) } else { subject.next({ data: count }) } }, 10 * 1000) return subject }) return observableAtom }) const derivedAtom = atom((get) => { const observableAtom = get(countObservableAtom) const result = get(observableAtom) if (result instanceof Promise) { return result.then((result) => { if ('error' in result) { throw result.error } return result.data }) } if ('error' in result) { throw result.error } return result.data }) const Counter = () => { const [count] = useAtom(derivedAtom) const refresh = useSetAtom(refreshAtom) return ( <>
count: {count}
) } const App = () => { const refresh = useSetAtom(refreshAtom) const retry = () => { refresh((c) => c + 1) } return ( ) } await act(() => render( , ), ) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10000)) expect(screen.getByText('errored')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('retry'))) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10000)) expect(screen.getByText('count: 1')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('refresh'))) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10000)) expect(screen.getByText('errored')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('retry'))) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(10000)) expect(screen.getByText('count: 3')).toBeInTheDocument() }) }) it('should throw error when writing to non-subject observable', () => { const observable = of(1) const observableAtom = atomWithObservable(() => observable) const Counter = () => { const [state, dispatch] = useAtom(observableAtom as any) return ( <> count: {state} ) } render( , ) expect(screen.getByText('count: 1')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) }) describe('wonka', () => { it('count state', () => { const source = fromValue(1) const observable = pipe(source, toObservable) const observableAtom = atomWithObservable(() => observable) const Counter = () => { const [count] = useAtom(observableAtom) return <>count: {count} } render( , ) expect(screen.getByText('count: 1')).toBeInTheDocument() }) it('make subject', async () => { const subject = makeSubject() const observable = pipe(subject.source, toObservable) const observableAtom = atomWithObservable(() => observable) const countAtom = atom( (get) => get(observableAtom), (_get, _set, nextValue: number) => { subject.next(nextValue) }, ) const Counter = () => { const [count] = useAtom(countAtom) return <>count: {count} } const Controls = () => { const setCount = useSetAtom(countAtom) return } await act(() => render( , ), ) expect(screen.getByText('loading')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('count: 1')).toBeInTheDocument() }) }) describe('atomWithObservable vanilla tests', () => { it('can propagate updates with async atom chains', async () => { const store = createStore() const subject = new BehaviorSubject(1) const countAtom = atomWithObservable(() => subject) const asyncAtom = atom(async (get) => get(countAtom)) const async2Atom = atom((get) => get(asyncAtom)) const unsub = store.sub(async2Atom, () => {}) await expect(store.get(async2Atom)).resolves.toBe(1) subject.next(2) await expect(store.get(async2Atom)).resolves.toBe(2) subject.next(3) await expect(store.get(async2Atom)).resolves.toBe(3) unsub() }) it('can propagate updates with rxjs chains', () => { const store = createStore() const single$ = new Subject() const double$ = single$.pipe(map((n) => n * 2)) const singleAtom = atomWithObservable(() => single$) const doubleAtom = atomWithObservable(() => double$) const unsubs = [ store.sub(singleAtom, () => {}), store.sub(doubleAtom, () => {}), ] single$.next(1) expect(store.get(singleAtom)).toBe(1) expect(store.get(doubleAtom)).toBe(2) single$.next(2) expect(store.get(singleAtom)).toBe(2) expect(store.get(doubleAtom)).toBe(4) single$.next(3) expect(store.get(singleAtom)).toBe(3) expect(store.get(doubleAtom)).toBe(6) unsubs.forEach((unsub) => unsub()) }) it('should throw error when writing to non-subject observable', () => { const store = createStore() const observable = of(1) const observableAtom = atomWithObservable(() => observable) store.sub(observableAtom, () => {}) expect(() => store.set(observableAtom as any, 2)).toThrow( 'observable is not subject', ) }) }) it('should update continuous values in React 19', async () => { const counterSubject = interval(100).pipe( take(4), switchMap(async (i) => i), ) const counterAtom = atomWithObservable(() => counterSubject, { unstable_timeout: 1000, }) const countAtom = atom(async (get) => get(counterAtom)) const Counter = () => { const count = useAtomValue(countAtom) return
count: {count}
} await act(() => render( , ), ) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(400)) expect(screen.getByText('count: 3')).toBeInTheDocument() }) ================================================ FILE: tests/react/vanilla-utils/atomWithReducer.test.tsx ================================================ import { StrictMode } from 'react' import { fireEvent, render, screen } from '@testing-library/react' import { expect, it } from 'vitest' import { useAtom } from 'jotai/react' import { atomWithReducer } from 'jotai/vanilla/utils' it('atomWithReducer with optional action argument', () => { const reducer = (state: number, action?: 'INCREASE' | 'DECREASE') => { switch (action) { case 'INCREASE': return state + 1 case 'DECREASE': return state - 1 case undefined: return state } } const countAtom = atomWithReducer(0, reducer) const Parent = () => { const [count, dispatch] = useAtom(countAtom) return ( <>
count: {count}
) } render( , ) expect(screen.getByText('count: 0')).toBeInTheDocument() fireEvent.click(screen.getByText('dispatch INCREASE')) expect(screen.getByText('count: 1')).toBeInTheDocument() fireEvent.click(screen.getByText('dispatch empty')) expect(screen.getByText('count: 1')).toBeInTheDocument() fireEvent.click(screen.getByText('dispatch DECREASE')) expect(screen.getByText('count: 0')).toBeInTheDocument() }) it('atomWithReducer with non-optional action argument', () => { const reducer = (state: number, action: 'INCREASE' | 'DECREASE') => { switch (action) { case 'INCREASE': return state + 1 case 'DECREASE': return state - 1 } } const countAtom = atomWithReducer(0, reducer) const Parent = () => { const [count, dispatch] = useAtom(countAtom) return ( <>
count: {count}
) } render( , ) expect(screen.getByText('count: 0')).toBeInTheDocument() fireEvent.click(screen.getByText('dispatch INCREASE')) expect(screen.getByText('count: 1')).toBeInTheDocument() fireEvent.click(screen.getByText('dispatch DECREASE')) expect(screen.getByText('count: 0')).toBeInTheDocument() }) ================================================ FILE: tests/react/vanilla-utils/atomWithRefresh.test.tsx ================================================ import { StrictMode, Suspense } from 'react' import { act, fireEvent, render, screen } from '@testing-library/react' import { afterEach, beforeEach, expect, it, vi } from 'vitest' import { useAtom } from 'jotai/react' import { atomWithRefresh } from 'jotai/vanilla/utils' import { sleep } from '../../test-utils' beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) it('sync counter', () => { let counter = 0 const countAtom = atomWithRefresh(() => ++counter) const Counter = () => { const [count, setCount] = useAtom(countAtom) return ( <>
count: {count}
) } render( , ) expect(screen.getByText('count: 1')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 2')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 3')).toBeInTheDocument() expect(counter).toBe(3) }) it('async counter', async () => { let counter = 0 const countAtom = atomWithRefresh(async () => { await sleep(100) return ++counter }) const Counter = () => { const [count, setCount] = useAtom(countAtom) return ( <>
count: {count}
) } await act(() => render( , ), ) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count: 1')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('button'))) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count: 2')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('button'))) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count: 3')).toBeInTheDocument() expect(counter).toBe(3) }) it('writable counter', () => { let counter = 0 const countAtom = atomWithRefresh( () => ++counter, (_get, _set, newValue: number) => { counter = newValue }, ) const Counter = () => { const [count, setCount] = useAtom(countAtom) return ( <>
count: {count}
) } render( , ) expect(screen.getByText('count: 1')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 2')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 3')).toBeInTheDocument() fireEvent.click(screen.getByText('set9')) expect(screen.getByText('count: 3')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 10')).toBeInTheDocument() }) ================================================ FILE: tests/react/vanilla-utils/atomWithStorage.test.tsx ================================================ import { StrictMode, Suspense } from 'react' import { act, fireEvent, render, screen } from '@testing-library/react' import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi, } from 'vitest' import { useAtom } from 'jotai/react' import { atom, createStore } from 'jotai/vanilla' import { RESET, atomWithStorage, createJSONStorage, unstable_withStorageValidator as withStorageValidator, } from 'jotai/vanilla/utils' import type { SyncStringStorage } from 'jotai/vanilla/utils/atomWithStorage' import { sleep } from '../../test-utils' beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) describe('atomWithStorage (sync)', () => { const storageData: Record = { count: 10, } const dummyStorage = { getItem: (key: string, initialValue: number) => { if (!(key in storageData)) { return initialValue } return storageData[key] as number }, setItem: (key: string, newValue: number) => { storageData[key] = newValue }, removeItem: (key: string) => { delete storageData[key] }, listeners: new Set<(key: string, value: number | null) => void>(), emitStorageEvent: (key: string, newValue: number | null) => { dummyStorage.listeners.forEach((listener) => { listener(key, newValue) }) }, subscribe: ( key: string, callback: (value: number) => void, initialValue: number, ) => { const listener = (k: string, value: number | null) => { if (k === key) { callback(value ?? initialValue) } } dummyStorage.listeners.add(listener) return () => dummyStorage.listeners.delete(listener) }, } it('simple count', () => { const countAtom = atomWithStorage('count', 1, dummyStorage) const Counter = () => { const [count, setCount] = useAtom(countAtom) return ( <>
count: {count}
) } render( , ) expect(screen.getByText('count: 10')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 11')).toBeInTheDocument() expect(storageData.count).toBe(11) fireEvent.click(screen.getByText('reset')) expect(screen.getByText('count: 1')).toBeInTheDocument() expect(storageData.count).toBeUndefined() }) it('storage updates before mount (#1079)', () => { dummyStorage.setItem('count', 10) const countAtom = atomWithStorage('count', 1, dummyStorage) const Counter = () => { const [count] = useAtom(countAtom) // emulating updating before mount if (dummyStorage.getItem('count', 1) !== 9) { dummyStorage.emitStorageEvent('count', 9) } return
count: {count}
} render( , ) expect(screen.getByText('count: 9')).toBeInTheDocument() }) it('should get stored value on init with getOnInit option', () => { const store = createStore() const countAtom = atomWithStorage('count', 0, dummyStorage, { getOnInit: true, }) expect(store.get(countAtom)).toBe(10) }) }) describe('with sync string storage', () => { const storageData: Record = { count: '10', } const stringStorage = { getItem: (key: string) => { return storageData[key] || null }, setItem: (key: string, newValue: string) => { storageData[key] = newValue }, removeItem: (key: string) => { delete storageData[key] }, listeners: new Set<(key: string, value: string | null) => void>(), emitStorageEvent: (key: string, newValue: string | null) => { stringStorage.listeners.forEach((listener) => { listener(key, newValue) }) }, } const dummyStorage = createJSONStorage(() => stringStorage) dummyStorage.subscribe = (key, callback, initialValue) => { const listener = (k: string, value: string | null) => { if (k === key) { let newValue: number try { newValue = JSON.parse(value ?? '') } catch { newValue = initialValue } callback(newValue) } } stringStorage.listeners.add(listener) return () => stringStorage.listeners.delete(listener) } it('simple count', () => { const countAtom = atomWithStorage('count', 1, dummyStorage) const Counter = () => { const [count, setCount] = useAtom(countAtom) return ( <>
count: {count}
) } render( , ) expect(screen.getByText('count: 10')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 11')).toBeInTheDocument() expect(storageData.count).toBe('11') fireEvent.click(screen.getByText('reset')) expect(screen.getByText('count: 1')).toBeInTheDocument() expect(storageData.count).toBeUndefined() fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 2')).toBeInTheDocument() expect(storageData.count).toBe('2') fireEvent.click(screen.getByText('conditional reset')) expect(screen.getByText('count: 1')).toBeInTheDocument() expect(storageData.count).toBeUndefined() }) it('no entry (#1086)', () => { const noentryAtom = atomWithStorage('noentry', -1, dummyStorage) const Counter = () => { const [noentry] = useAtom(noentryAtom) return
noentry: {noentry}
} render( , ) expect(screen.getByText('noentry: -1')).toBeInTheDocument() }) }) describe('atomWithStorage (async)', () => { const asyncStorageData: Record = { count: 10, } const asyncDummyStorage = { getItem: async (key: string, initialValue: number) => { await sleep(100) if (!(key in asyncStorageData)) { return initialValue } return asyncStorageData[key] as number }, setItem: async (key: string, newValue: number) => { await sleep(100) asyncStorageData[key] = newValue }, removeItem: async (key: string) => { await sleep(100) delete asyncStorageData[key] }, } it('async count', async () => { const countAtom = atomWithStorage('count', 1, asyncDummyStorage) const Counter = () => { const [count, setCount] = useAtom(countAtom) return ( <>
count: {count}
) } await act(() => render( , ), ) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count: 10')).toBeInTheDocument() await act(() => fireEvent.click(screen.getByText('button'))) await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count: 11')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(asyncStorageData.count).toBe(11) await act(() => fireEvent.click(screen.getByText('reset'))) await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count: 1')).toBeInTheDocument() expect(asyncStorageData.count).toBeUndefined() }) it('async new count', async () => { const countAtom = atomWithStorage('count2', 20, asyncDummyStorage) const Counter = () => { const [count, setCount] = useAtom(countAtom) return ( <>
count: {count}
) } await act(() => render( , ), ) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count: 20')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect(screen.getByText('count: 20')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(300)) expect(asyncStorageData.count2).toBe(21) }) it('createJSONStorage with async string storage', async () => { const asyncStringStorage = { getItem: async (key: string) => { await sleep(100) if (key === 'count') { return '10' } return null }, setItem: async () => {}, removeItem: async () => {}, } const countAtom = atomWithStorage( 'count', 0, createJSONStorage(() => asyncStringStorage), ) const Counter = () => { const [count] = useAtom(countAtom) return
count: {count}
} await act(() => render( , ), ) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count: 10')).toBeInTheDocument() }) }) describe('atomWithStorage (without localStorage) (#949)', () => { it('createJSONStorage without localStorage', () => { const countAtom = atomWithStorage( 'count', 1, createJSONStorage(() => undefined as any), ) const Counter = () => { const [count, setCount] = useAtom(countAtom) return ( <>
count: {count}
) } render( , ) expect(screen.getByText('count: 1')).toBeInTheDocument() }) }) describe('atomWithStorage (in non-browser environment)', () => { const asyncStorageData: Record = { count: '10', } const asyncDummyStorage = { getItem: async (key: string) => { await sleep(100) return asyncStorageData[key] as string }, setItem: async (key: string, newValue: string) => { await sleep(100) asyncStorageData[key] = newValue }, removeItem: async (key: string) => { await sleep(100) delete asyncStorageData[key] }, } const addEventListener = window.addEventListener const localStorage = window.localStorage const sessionStorage = window.sessionStorage const consoleWarn = window.console.warn beforeAll(() => { ;(window as any).addEventListener = undefined // patch console.warn to prevent logging along test results Object.defineProperty(window.console, 'warn', { value: () => {}, }) Object.defineProperties(window, { localStorage: { get() { throw new Error('localStorage is not available.') }, }, sessionStorage: { get() { throw new Error('sessionStorage is not available.') }, }, }) }) afterAll(() => { window.addEventListener = addEventListener Object.defineProperty(window.console, 'warn', { value: consoleWarn, }) Object.defineProperties(window, { localStorage: { get() { return localStorage }, }, sessionStorage: { get() { return sessionStorage }, }, }) }) it('createJSONStorage with undefined window.addEventListener', () => { const storage = createJSONStorage(() => asyncDummyStorage) expect(storage.subscribe).toBeUndefined() }) it('createJSONStorage with localStorage', () => { expect(() => createJSONStorage()).not.toThrow() expect(() => createJSONStorage(() => window.localStorage)).not.toThrow() }) it('createJSONStorage with sessionStorage', () => { expect(() => createJSONStorage(() => window.sessionStorage)).not.toThrow() }) }) describe('atomWithStorage (with browser storage)', () => { const addEventListener = window.addEventListener const mockAddEventListener = vi.fn() beforeAll(() => { ;(window as any).addEventListener = mockAddEventListener }) afterAll(() => { window.addEventListener = addEventListener }) it('createJSONStorage subscribes to specific window storage events', () => { const store = createStore() const mockNativeStorage = Object.create(window.Storage.prototype) mockNativeStorage.setItem = vi.fn() as Storage['setItem'] mockNativeStorage.getItem = vi.fn(() => null) as Storage['getItem'] mockNativeStorage.removeItem = vi.fn() as Storage['removeItem'] const dummyAtom = atomWithStorage( 'dummy', 1, createJSONStorage(() => mockNativeStorage), ) const DummyComponent = () => { const [value] = useAtom(dummyAtom, { store }) return ( <>
{value}
) } render( , ) expect(mockAddEventListener).toHaveBeenCalledWith( 'storage', expect.any(Function), ) const storageEventHandler = mockAddEventListener.mock.calls .filter(([eventName]: any) => eventName === 'storage') .pop()?.[1] as (e: StorageEvent) => void expect(store.get(dummyAtom)).toBe(1) act(() => { storageEventHandler?.({ key: 'dummy', newValue: '2', storageArea: {}, } as StorageEvent) }) expect(store.get(dummyAtom)).toBe(1) act(() => { storageEventHandler?.({ key: 'dummy', newValue: '2', storageArea: mockNativeStorage, } as StorageEvent) }) expect(store.get(dummyAtom)).toBe(2) // simulate removeItem() from another window act(() => { storageEventHandler?.({ key: 'dummy', newValue: null, storageArea: mockNativeStorage, } as StorageEvent) }) expect(store.get(dummyAtom)).toBe(1) }) it("should recompute dependents' state after onMount (#2098)", () => { const store = createStore() let currentValue: string | null = 'true' const mockNativeStorage = { setItem: vi.fn((_key: string, value: string) => (currentValue = value)), getItem: vi.fn(() => currentValue), removeItem: vi.fn(() => (currentValue = null)), } const isLoggedAtom = atom(false) const isDevModeStorageAtom = atomWithStorage( 'isDevModeStorageAtom', false, createJSONStorage(() => mockNativeStorage), ) const isDevModeState = atom( (get) => { if (!get(isLoggedAtom)) return false return get(isDevModeStorageAtom) }, (_get, set, value: boolean) => { set(isDevModeStorageAtom, value) }, ) const DummyComponent = () => { const [isLogged] = useAtom(isLoggedAtom, { store }) const [value, setValue] = useAtom(isDevModeState, { store }) return isLogged ? ( setValue(!value)} /> ) : null } render( , ) act(() => store.set(isLoggedAtom, true)) const checkbox = screen.getByRole('checkbox') as HTMLInputElement expect(store.get(isLoggedAtom)).toBeTruthy() expect(store.get(isDevModeStorageAtom)).toBeTruthy() expect(checkbox).toBeChecked() fireEvent.click(checkbox) expect(checkbox).not.toBeChecked() }) }) describe('atomWithStorage (with disabled browser storage)', () => { const savedLocalStorage = window.localStorage beforeAll(() => { // Firefox and chromium based browser throw DOMException when cookies are disabled Object.defineProperty(window, 'localStorage', { get() { throw new DOMException('The operation is insecure.') }, }) }) afterAll(() => { // TS < 4.5 causes type error without `as any` ;(window as any).localStorage = savedLocalStorage }) it('initial value of atomWithStorage can be used when cookies are disabled', () => { const countAtom = atomWithStorage('counter', 4) const Counter = () => { const [value] = useAtom(countAtom) return ( <>
count: {value}
) } render( , ) expect(screen.getByText('count: 4')).toBeInTheDocument() }) }) describe('atomWithStorage (with non-browser storage)', () => { const addEventListener = window.addEventListener const mockAddEventListener = vi.fn() beforeAll(() => { ;(window as any).addEventListener = mockAddEventListener }) afterAll(() => { window.addEventListener = addEventListener }) it('createJSONStorage avoids attaching event handler for non-browser storage', () => { const store = createStore() const mockNonNativeStorage = { setItem: vi.fn() as Storage['setItem'], getItem: vi.fn(() => null) as Storage['getItem'], removeItem: vi.fn() as Storage['removeItem'], } const dummyAtom = atomWithStorage( 'dummy', 1, createJSONStorage(() => mockNonNativeStorage), ) const DummyComponent = () => { const [value] = useAtom(dummyAtom, { store }) return ( <>
{value}
) } render( , ) expect(mockAddEventListener).not.toHaveBeenCalledWith( 'storage', expect.any(Function), ) }) }) describe('withStorageValidator', () => { const isNumber = (v: unknown): v is number => typeof v === 'number' it('should use withStorageValidator with isNumber', () => { const store = createStore() const storage = createJSONStorage() const numAtom = atomWithStorage( 'my-number', 0, withStorageValidator(isNumber)(storage), ) expect(store.get(numAtom)).toBe(0) }) it('should return initialValue when validator fails', () => { const store = createStore() const stringStorage: SyncStringStorage = { getItem: () => JSON.stringify('not-a-number'), setItem: () => {}, removeItem: () => {}, } const storage = createJSONStorage(() => stringStorage) const numAtom = atomWithStorage( 'my-number', 42, withStorageValidator(isNumber)(storage), ) expect(store.get(numAtom)).toBe(42) }) it('should return initialValue when sync validator fails with render', () => { const stringStorage: SyncStringStorage = { getItem: () => JSON.stringify('not-a-number'), setItem: () => {}, removeItem: () => {}, } const storage = createJSONStorage(() => stringStorage) const numAtom = atomWithStorage( 'my-number', 42, withStorageValidator(isNumber)(storage), ) const Counter = () => { const [count] = useAtom(numAtom) return
count: {count}
} render( , ) expect(screen.getByText('count: 42')).toBeInTheDocument() }) it('should return stored value when async validator succeeds', async () => { const asyncStringStorage = { getItem: async () => { await sleep(100) return JSON.stringify(99) }, setItem: async () => {}, removeItem: async () => {}, } const storage = createJSONStorage(() => asyncStringStorage) const numAtom = atomWithStorage( 'my-number', 42, withStorageValidator(isNumber)(storage), ) const Counter = () => { const [count] = useAtom(numAtom) return
count: {count}
} await act(() => render( , ), ) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count: 99')).toBeInTheDocument() }) it('should return initialValue when async validator fails', async () => { const asyncStringStorage = { getItem: async () => { await sleep(100) return JSON.stringify('invalid') }, setItem: async () => {}, removeItem: async () => {}, } const storage = createJSONStorage(() => asyncStringStorage) const numAtom = atomWithStorage( 'my-number', 42, withStorageValidator(isNumber)(storage), ) const Counter = () => { const [count] = useAtom(numAtom) return
count: {count}
} await act(() => render( , ), ) expect(screen.getByText('loading')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('count: 42')).toBeInTheDocument() }) }) describe('with subscribe method in string storage', () => { it('createJSONStorage subscriber is called correctly', () => { const store = createStore() const subscribe = vi.fn() const stringStorage = { getItem: () => { return null }, setItem: () => {}, removeItem: () => {}, subscribe, } const dummyStorage = createJSONStorage(() => stringStorage) const dummyAtom = atomWithStorage('dummy', 1, dummyStorage) const DummyComponent = () => { const [value] = useAtom(dummyAtom, { store }) return ( <>
{value}
) } render( , ) expect(subscribe).toHaveBeenCalledWith('dummy', expect.any(Function)) }) it('createJSONStorage subscriber responds to events correctly', () => { const storageData: Record = { count: '10', } const stringStorage = { getItem: (key: string) => { return storageData[key] || null }, setItem: (key: string, newValue: string) => { storageData[key] = newValue }, removeItem: (key: string) => { delete storageData[key] }, subscribe(_key, callback) { function handler(event: CustomEvent) { callback(event.detail) } window.addEventListener('dummystoragechange', handler as EventListener) return () => window.removeEventListener( 'dummystoragechange', handler as EventListener, ) }, } as SyncStringStorage const dummyStorage = createJSONStorage(() => stringStorage) const countAtom = atomWithStorage('count', 1, dummyStorage) const Counter = () => { const [count] = useAtom(countAtom) return ( <>
count: {count}
) } render( , ) expect(screen.getByText('count: 10')).toBeInTheDocument() storageData.count = '12' fireEvent( window, new CustomEvent('dummystoragechange', { detail: '12', }), ) expect(screen.getByText('count: 12')).toBeInTheDocument() // expect(storageData.count).toBe('11') }) }) describe('with custom async storage', () => { it('does not infinite loop (#2931)', async () => { let storedValue = 0 let cachedPromise: | [typeof storedValue, Promise] | null = null const counterAtom = atomWithStorage('counter', 0, { getItem(_key: string, _initialValue: number) { if (cachedPromise && cachedPromise[0] === storedValue) { return cachedPromise[1] } const promise = Promise.resolve(storedValue) cachedPromise = [storedValue, promise] return promise }, async setItem(_key, newValue) { storedValue = await new Promise((resolve) => resolve(newValue)) }, async removeItem() {}, }) const Component = () => { const [count, setCount] = useAtom(counterAtom) return ( <>
count: {count}
) } await act(() => render( , ), ) expect(screen.getByText('count: 0')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('count: 1')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('count: 2')).toBeInTheDocument() }) }) ================================================ FILE: tests/react/vanilla-utils/freezeAtom.test.tsx ================================================ import { StrictMode } from 'react' import { fireEvent, render, screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { useAtom } from 'jotai/react' import { atom } from 'jotai/vanilla' import { freezeAtom, freezeAtomCreator } from 'jotai/vanilla/utils' it('freezeAtom basic test', () => { const objAtom = atom({ deep: { count: 0 } }) const Component = () => { const [obj, setObj] = useAtom(freezeAtom(objAtom)) return ( <>
count: {obj.deep.count}, isFrozen:{' '} {`${Object.isFrozen(obj) && Object.isFrozen(obj.deep)}`}
) } render( , ) expect(screen.getByText('count: 0, isFrozen: true')).toBeInTheDocument() fireEvent.click(screen.getByText('change')) expect(screen.getByText('count: 1, isFrozen: true')).toBeInTheDocument() }) it('freezeAtom handles null correctly', () => { const nullAtom = atom(null) const Component = () => { const [value, setValue] = useAtom(freezeAtom(nullAtom)) return ( <>
value is null: {`${value === null}`}
) } render( , ) expect(screen.getByText('value is null: true')).toBeInTheDocument() }) it('freezeAtom handles primitive correctly', () => { const numberAtom = atom(123) const Component = () => { const [value, setValue] = useAtom(freezeAtom(numberAtom)) return ( <>
value: {value}
) } render( , ) expect(screen.getByText('value: 123')).toBeInTheDocument() fireEvent.click(screen.getByText('set number')) expect(screen.getByText('value: 456')).toBeInTheDocument() }) describe('freezeAtomCreator', () => { let savedConsoleWarn: any beforeEach(() => { savedConsoleWarn = console.warn console.warn = vi.fn() }) afterEach(() => { console.warn = savedConsoleWarn }) it('freezeAtomCreator basic test', () => { const createFrozenAtom = freezeAtomCreator(atom) const objAtom = createFrozenAtom({ deep: {} }, (_get, set, _ignored?) => { set(objAtom, { deep: {} }) }) const Component = () => { const [obj, setObj] = useAtom(objAtom) return ( <>
isFrozen: {`${Object.isFrozen(obj) && Object.isFrozen(obj.deep)}`}
) } render( , ) expect(screen.getByText('isFrozen: true')).toBeInTheDocument() fireEvent.click(screen.getByText('change')) expect(screen.getByText('isFrozen: true')).toBeInTheDocument() }) }) ================================================ FILE: tests/react/vanilla-utils/loadable.test.tsx ================================================ import { StrictMode, Suspense, useEffect } from 'react' import { act, fireEvent, render, screen } from '@testing-library/react' import { afterEach, beforeEach, expect, it, vi } from 'vitest' import { useAtomValue, useSetAtom } from 'jotai/react' import { atom } from 'jotai/vanilla' import type { Atom } from 'jotai/vanilla' import { loadable } from 'jotai/vanilla/utils' import { sleep } from '../../test-utils' beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) it('loadable turns suspense into values', async () => { const asyncAtom = atom(async () => { await sleep(100) return 5 }) render( , ) expect(screen.getByText('Loading...')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('Data: 5')).toBeInTheDocument() }) it('loadable turns errors into values', async () => { const asyncAtom = atom(async () => { await sleep(100) throw new Error('An error occurred') }) render( , ) expect(screen.getByText('Loading...')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('Error: An error occurred')).toBeInTheDocument() }) it('loadable turns primitive throws into values', async () => { const asyncAtom = atom(async () => { await sleep(100) throw 'An error occurred' }) render( , ) expect(screen.getByText('Loading...')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('An error occurred')).toBeInTheDocument() }) it('loadable goes back to loading after re-fetch', async () => { const refreshAtom = atom(0) const asyncAtom = atom(async (get) => { const count = get(refreshAtom) await sleep(100) return count === 0 ? 5 : 6 }) const Refresh = () => { const setRefresh = useSetAtom(refreshAtom) return ( <> ) } render( , ) expect(screen.getByText('Loading...')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('Data: 5')).toBeInTheDocument() fireEvent.click(screen.getByText('refresh')) expect(screen.getByText('Loading...')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('Data: 6')).toBeInTheDocument() }) it('loadable can recover from error', async () => { const refreshAtom = atom(0) const asyncAtom = atom(async (get) => { const count = get(refreshAtom) await sleep(100) if (count === 0) { throw new Error('An error occurred') } return 6 }) const Refresh = () => { const setRefresh = useSetAtom(refreshAtom) return ( <> ) } render( , ) expect(screen.getByText('Loading...')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('Error: An error occurred')).toBeInTheDocument() fireEvent.click(screen.getByText('refresh')) expect(screen.getByText('Loading...')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('Data: 6')).toBeInTheDocument() }) it('loadable immediately resolves sync values', () => { const syncAtom = atom(5) const effectCallback = vi.fn() render( , ) expect(screen.getByText('Data: 5')).toBeInTheDocument() expect(effectCallback.mock.calls).not.toContain( expect.objectContaining({ state: 'loading' }), ) expect(effectCallback).toHaveBeenLastCalledWith({ state: 'hasData', data: 5 }) }) it('loadable can use resolved promises synchronously', async () => { const asyncAtom = atom(Promise.resolve(5)) const effectCallback = vi.fn() const ResolveAtomComponent = () => { useAtomValue(asyncAtom) return
Ready
} let result: ReturnType await act(async () => { result = render( , ) }) await act(() => vi.advanceTimersByTimeAsync(0)) // FIXME React 18/19 Suspense behavior is non-deterministic expect( screen.queryByText('loading') ?? screen.queryByText('Ready'), ).toBeInTheDocument() result!.rerender( , ) await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('Data: 5')).toBeInTheDocument() expect(effectCallback.mock.calls).not.toContain( expect.objectContaining({ state: 'loading' }), ) expect(effectCallback).toHaveBeenLastCalledWith({ state: 'hasData', data: 5 }) }) it('loadable of a derived async atom does not trigger infinite loop (#1114)', async () => { const baseAtom = atom(0) const asyncAtom = atom(async (get) => { get(baseAtom) await sleep(100) return 5 }) const Trigger = () => { const trigger = useSetAtom(baseAtom) return ( <> ) } render( , ) expect(screen.getByText('Loading...')).toBeInTheDocument() fireEvent.click(screen.getByText('trigger')) await act(() => vi.advanceTimersByTimeAsync(100)) expect(screen.getByText('Data: 5')).toBeInTheDocument() }) it('loadable of a derived async atom with error does not trigger infinite loop (#1330)', async () => { const baseAtom = atom(() => { throw new Error('thrown in baseAtom') }) const asyncAtom = atom(async (get) => { get(baseAtom) return '' }) render( , ) expect(screen.getByText('Loading...')).toBeInTheDocument() await act(() => vi.advanceTimersByTimeAsync(0)) expect(screen.getByText('Error: thrown in baseAtom')).toBeInTheDocument() }) it('does not repeatedly attempt to get the value of an unresolved promise atom wrapped in a loadable (#1481)', async () => { const baseAtom = atom(new Promise(() => {})) let callsToGetBaseAtom = 0 const derivedAtom = atom((get) => { callsToGetBaseAtom++ return get(baseAtom) }) render( , ) // we need a small delay to reproduce the issue await act(() => vi.advanceTimersByTimeAsync(10)) // depending on provider-less mode or versioned-write mode, there will be // either 2 or 3 calls. expect(callsToGetBaseAtom).toBeLessThanOrEqual(3) }) it('should handle sync error (#1843)', () => { const syncAtom = atom(() => { throw new Error('thrown in syncAtom') }) render( , ) expect(screen.getByText('Error: thrown in syncAtom')).toBeInTheDocument() }) type LoadableComponentProps = { asyncAtom: Atom | Promise | string | number> effectCallback?: (loadableValue: any) => void } const LoadableComponent = ({ asyncAtom, effectCallback, }: LoadableComponentProps) => { const value = useAtomValue(loadable(asyncAtom)) useEffect(() => { if (effectCallback) { effectCallback(value) } }, [value, effectCallback]) if (value.state === 'loading') { return <>Loading... } if (value.state === 'hasError') { return <>{String(value.error)} } // this is to ensure correct typing const data: number | string = value.data return <>Data: {data} } ================================================ FILE: tests/react/vanilla-utils/selectAtom.test.tsx ================================================ import { StrictMode } from 'react' import { fireEvent, render, screen } from '@testing-library/react' import { expect, it } from 'vitest' import { useAtomValue, useSetAtom } from 'jotai/react' import { atom } from 'jotai/vanilla' import { selectAtom } from 'jotai/vanilla/utils' import { useCommitCount } from '../../test-utils' it('selectAtom works as expected', () => { const bigAtom = atom({ a: 0, b: 'othervalue' }) const littleAtom = selectAtom(bigAtom, (v) => v.a) const Parent = () => { const setValue = useSetAtom(bigAtom) return ( <> ) } const Selector = () => { const a = useAtomValue(littleAtom) return ( <>
a: {a}
) } render( , ) expect(screen.getByText('a: 0')).toBeInTheDocument() fireEvent.click(screen.getByText('increment')) expect(screen.getByText('a: 1')).toBeInTheDocument() fireEvent.click(screen.getByText('increment')) expect(screen.getByText('a: 2')).toBeInTheDocument() fireEvent.click(screen.getByText('increment')) expect(screen.getByText('a: 3')).toBeInTheDocument() }) it('do not update unless equality function says value has changed', () => { const bigAtom = atom({ a: 0 }) const littleAtom = selectAtom( bigAtom, (value) => value, (left, right) => JSON.stringify(left) === JSON.stringify(right), ) const Parent = () => { const setValue = useSetAtom(bigAtom) return ( <> ) } const Selector = () => { const value = useAtomValue(littleAtom) const commits = useCommitCount() return ( <>
value: {JSON.stringify(value)}
commits: {commits}
) } render( <> , ) expect(screen.getByText('value: {"a":0}')).toBeInTheDocument() expect(screen.getByText('commits: 1')).toBeInTheDocument() fireEvent.click(screen.getByText('copy')) expect(screen.getByText('value: {"a":0}')).toBeInTheDocument() expect(screen.getByText('commits: 1')).toBeInTheDocument() fireEvent.click(screen.getByText('increment')) expect(screen.getByText('value: {"a":1}')).toBeInTheDocument() expect(screen.getByText('commits: 2')).toBeInTheDocument() fireEvent.click(screen.getByText('copy')) expect(screen.getByText('value: {"a":1}')).toBeInTheDocument() expect(screen.getByText('commits: 2')).toBeInTheDocument() fireEvent.click(screen.getByText('increment')) expect(screen.getByText('value: {"a":2}')).toBeInTheDocument() expect(screen.getByText('commits: 3')).toBeInTheDocument() fireEvent.click(screen.getByText('copy')) expect(screen.getByText('value: {"a":2}')).toBeInTheDocument() expect(screen.getByText('commits: 3')).toBeInTheDocument() fireEvent.click(screen.getByText('increment')) expect(screen.getByText('value: {"a":3}')).toBeInTheDocument() expect(screen.getByText('commits: 4')).toBeInTheDocument() fireEvent.click(screen.getByText('copy')) expect(screen.getByText('value: {"a":3}')).toBeInTheDocument() expect(screen.getByText('commits: 4')).toBeInTheDocument() }) it('creates fresh cache path when deps differ (memo3)', () => { const baseAtom = atom({ a: 0, b: 1 }) const derivedAtom1 = selectAtom(baseAtom, (v) => v) const derivedAtom2 = selectAtom(baseAtom, (v) => v) expect(derivedAtom1).not.toBe(derivedAtom2) const selector = (v: { a: number; b: number }) => v.a const derivedAtom3 = selectAtom(baseAtom, selector) const derivedAtom4 = selectAtom(baseAtom, selector) expect(derivedAtom3).toBe(derivedAtom4) }) ================================================ FILE: tests/react/vanilla-utils/splitAtom.test.tsx ================================================ import { StrictMode, useEffect, useRef } from 'react' import { fireEvent, render, screen } from '@testing-library/react' import { expect, it } from 'vitest' import { useAtom, useAtomValue, useSetAtom } from 'jotai/react' import { atom, createStore } from 'jotai/vanilla' import type { Atom, PrimitiveAtom } from 'jotai/vanilla' import { splitAtom } from 'jotai/vanilla/utils' import { useCommitCount } from '../../test-utils' type TodoItem = { task: string; checked?: boolean } it('no unnecessary updates when updating atoms', () => { const todosAtom = atom([ { task: 'get cat food', checked: false }, { task: 'get dragon food', checked: false }, ]) const TaskList = ({ listAtom }: { listAtom: typeof todosAtom }) => { const [atoms, dispatch] = useAtom(splitAtom(listAtom)) return ( <> TaskListUpdates: {useCommitCount()} {atoms.map((anAtom) => ( dispatch({ type: 'remove', atom: anAtom })} itemAtom={anAtom} /> ))} ) } const TaskItem = ({ itemAtom, }: { itemAtom: PrimitiveAtom onRemove: () => void }) => { const [value, onChange] = useAtom(itemAtom) const toggle = () => onChange((value) => ({ ...value, checked: !value.checked })) return (
  • {value.task} commits: {useCommitCount()}
  • ) } render( <> , ) expect(screen.getByText('TaskListUpdates: 1')).toBeInTheDocument() expect(screen.getByText('get cat food commits: 1')).toBeInTheDocument() expect(screen.getByText('get dragon food commits: 1')).toBeInTheDocument() const catBox = screen.getByTestId('get cat food-checkbox') as HTMLInputElement const dragonBox = screen.getByTestId( 'get dragon food-checkbox', ) as HTMLInputElement expect(catBox).not.toBeChecked() expect(dragonBox).not.toBeChecked() fireEvent.click(catBox) expect(screen.getByText('TaskListUpdates: 1')).toBeInTheDocument() expect(screen.getByText('get cat food commits: 2')).toBeInTheDocument() expect(screen.getByText('get dragon food commits: 1')).toBeInTheDocument() expect(catBox).toBeChecked() expect(dragonBox).not.toBeChecked() fireEvent.click(dragonBox) expect(screen.getByText('TaskListUpdates: 1')).toBeInTheDocument() expect(screen.getByText('get cat food commits: 2')).toBeInTheDocument() expect(screen.getByText('get dragon food commits: 2')).toBeInTheDocument() expect(catBox).toBeChecked() expect(dragonBox).toBeChecked() }) it('removing atoms', () => { const todosAtom = atom([ { task: 'get cat food', checked: false }, { task: 'get dragon food', checked: false }, { task: 'help nana', checked: false }, ]) const TaskList = ({ listAtom }: { listAtom: typeof todosAtom }) => { const [atoms, dispatch] = useAtom(splitAtom(listAtom)) return ( <> {atoms.map((anAtom) => ( dispatch({ type: 'remove', atom: anAtom })} itemAtom={anAtom} /> ))} ) } const TaskItem = ({ itemAtom, onRemove, }: { itemAtom: PrimitiveAtom onRemove: () => void }) => { const [value] = useAtom(itemAtom) return (
  • {value.task}
  • ) } render( , ) expect(screen.getByText('get cat food')).toBeInTheDocument() expect(screen.getByText('get dragon food')).toBeInTheDocument() expect(screen.getByText('help nana')).toBeInTheDocument() fireEvent.click(screen.getByTestId('get cat food-removebutton')) expect(screen.queryByText('get cat food')).not.toBeInTheDocument() expect(screen.getByText('get dragon food')).toBeInTheDocument() expect(screen.getByText('help nana')).toBeInTheDocument() fireEvent.click(screen.getByTestId('get dragon food-removebutton')) expect(screen.queryByText('get cat food')).not.toBeInTheDocument() expect(screen.queryByText('get dragon food')).not.toBeInTheDocument() expect(screen.getByText('help nana')).toBeInTheDocument() fireEvent.click(screen.getByTestId('help nana-removebutton')) expect(screen.queryByText('get cat food')).not.toBeInTheDocument() expect(screen.queryByText('get dragon food')).not.toBeInTheDocument() expect(screen.queryByText('help nana')).not.toBeInTheDocument() }) it('inserting atoms', () => { const todosAtom = atom([ { task: 'get cat food' }, { task: 'get dragon food' }, { task: 'help nana' }, ]) const TaskList = ({ listAtom }: { listAtom: typeof todosAtom }) => { const [atoms, dispatch] = useAtom(splitAtom(listAtom)) return ( <>
      {atoms.map((anAtom) => ( dispatch({ type: 'insert', value: newValue, before: anAtom, }) } itemAtom={anAtom} /> ))}
    ) } let taskCount = 1 const TaskItem = ({ itemAtom, onInsert, }: { itemAtom: PrimitiveAtom onInsert: (newValue: TodoItem) => void }) => { const [value] = useAtom(itemAtom) return (
  • {value.task}
  • ) } render( , ) expect(screen.getByTestId('list')).toHaveTextContent( 'get cat food+get dragon food+help nana+', ) fireEvent.click(screen.getByTestId('help nana-insertbutton')) expect(screen.getByTestId('list')).toHaveTextContent( 'get cat food+get dragon food+new task1+help nana+', ) fireEvent.click(screen.getByTestId('get cat food-insertbutton')) expect(screen.getByTestId('list')).toHaveTextContent( 'new task2+get cat food+get dragon food+new task1+help nana+', ) fireEvent.click(screen.getByTestId('addtaskbutton')) expect(screen.getByTestId('list')).toHaveTextContent( 'new task2+get cat food+get dragon food+new task1+help nana+end+', ) }) it('moving atoms', () => { const todosAtom = atom([ { task: 'get cat food' }, { task: 'get dragon food' }, { task: 'help nana' }, ]) const TaskList = ({ listAtom }: { listAtom: typeof todosAtom }) => { const [atoms, dispatch] = useAtom(splitAtom(listAtom)) return (
      {atoms.map((anAtom, index) => ( { if (index === 0) { dispatch({ type: 'move', atom: anAtom, }) } else if (index > 0) { dispatch({ type: 'move', atom: anAtom, before: atoms[index - 1] as PrimitiveAtom, }) } }} onMoveRight={() => { if (index === atoms.length - 1) { dispatch({ type: 'move', atom: anAtom, }) } else if (index < atoms.length - 1) { dispatch({ type: 'move', atom: anAtom, before: atoms[index + 2] as PrimitiveAtom, }) } }} itemAtom={anAtom} /> ))}
    ) } const TaskItem = ({ itemAtom, onMoveLeft, onMoveRight, }: { itemAtom: PrimitiveAtom onMoveLeft: () => void onMoveRight: () => void }) => { const [value] = useAtom(itemAtom) return (
  • {value.task}
  • ) } render( , ) expect(screen.getByTestId('list')).toHaveTextContent( 'get cat food<>get dragon food<>help nana<>', ) fireEvent.click(screen.getByTestId('help nana-leftbutton')) expect(screen.getByTestId('list')).toHaveTextContent( 'get cat food<>help nana<>get dragon food<>', ) fireEvent.click(screen.getByTestId('get cat food-rightbutton')) expect(screen.getByTestId('list')).toHaveTextContent( 'help nana<>get cat food<>get dragon food<>', ) fireEvent.click(screen.getByTestId('get cat food-rightbutton')) expect(screen.getByTestId('list')).toHaveTextContent( 'help nana<>get dragon food<>get cat food<>', ) fireEvent.click(screen.getByTestId('help nana-leftbutton')) expect(screen.getByTestId('list')).toHaveTextContent( 'get dragon food<>get cat food<>help nana<>', ) }) it('read-only array atom', () => { const todosAtom = atom(() => [ { task: 'get cat food', checked: false }, { task: 'get dragon food', checked: false }, ]) const TaskList = ({ listAtom }: { listAtom: typeof todosAtom }) => { const [atoms] = useAtom(splitAtom(listAtom)) return ( <> {atoms.map((anAtom) => ( ))} ) } const TaskItem = ({ itemAtom }: { itemAtom: Atom }) => { const [value] = useAtom(itemAtom) return (
  • ) } render( , ) const catBox = screen.getByTestId('get cat food-checkbox') as HTMLInputElement const dragonBox = screen.getByTestId( 'get dragon food-checkbox', ) as HTMLInputElement expect(catBox).not.toBeChecked() expect(dragonBox).not.toBeChecked() }) it('no error with cached atoms (fix 510)', () => { const filterAtom = atom('all') const numsAtom = atom([0, 1, 2, 3, 4]) const filteredAtom = atom((get) => { const filter = get(filterAtom) const nums = get(numsAtom) if (filter === 'even') { return nums.filter((num) => num % 2 === 0) } return nums }) const filteredAtomsAtom = splitAtom(filteredAtom, (num) => num) function useCachedAtoms(atoms: T[]) { const prevAtoms = useRef(atoms) // eslint-disable-next-line react-hooks/refs return prevAtoms.current } type NumItemProps = { atom: Atom } const NumItem = ({ atom }: NumItemProps) => { const [readOnlyItem] = useAtom(atom) if (typeof readOnlyItem !== 'number') { throw new Error('expecting a number') } return <>{readOnlyItem} } function Filter() { const [, setFilter] = useAtom(filterAtom) return } const Filtered = () => { const [todos] = useAtom(filteredAtomsAtom) const cachedAtoms = useCachedAtoms(todos) return ( <> {cachedAtoms.map((atom) => ( ))} ) } render( , ) expect(screen.getByText('01234')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect(screen.getByText('01234')).toBeInTheDocument() }) it('variable sized split atom', () => { const lengthAtom = atom(3) const collectionAtom = atom([]) const collectionAtomsAtom = splitAtom(collectionAtom) const derivativeAtom = atom((get) => get(collectionAtomsAtom).map((ca) => get(ca)), ) function App() { const [length, setLength] = useAtom(lengthAtom) const setCollection = useSetAtom(collectionAtom) const [derivative] = useAtom(derivativeAtom) useEffect(() => { setCollection([1, 2, 3].splice(0, length)) }, [length, setCollection]) return (
    numbers: {derivative.join(',')}
    ) } render( , ) expect(screen.getByText('numbers: 1,2,3')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect(screen.getByText('numbers: 1,2')).toBeInTheDocument() }) it('should not update split atom when single item is set to identical value', () => { const initialCollection = [1, 2, 3] const collectionAtom = atom(initialCollection) const collectionAtomsAtom = splitAtom(collectionAtom) function App() { const collectionAtoms = useAtomValue(collectionAtomsAtom) const setItem2 = useSetAtom(collectionAtoms[1]!) const currentCollection = useAtomValue(collectionAtom) return (
    changed: {(!Object.is(currentCollection, initialCollection)).toString()}
    ) } render( , ) expect(screen.getByText('changed: false')).toBeInTheDocument() fireEvent.click(screen.getByText('button')) expect(screen.getByText('changed: false')).toBeInTheDocument() }) it('should throw error when writing to a removed item atom', () => { const store = createStore() const arrAtom = atom([1, 2, 3]) const splittedAtom = splitAtom(arrAtom) const atomList = store.get(splittedAtom) const secondAtom = atomList[1]! store.set(arrAtom, []) expect(() => store.set(secondAtom, 10)).toThrow( 'splitAtom: index out of bounds for write', ) }) ================================================ FILE: tests/setup.ts ================================================ import '@testing-library/jest-dom/vitest' ================================================ FILE: tests/test-utils.ts ================================================ import { useEffect, useRef } from 'react' export function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)) } export function useCommitCount(): number { const commitCountRef = useRef(1) useEffect(() => { commitCountRef.current += 1 }) // eslint-disable-next-line react-hooks/refs return commitCountRef.current } ================================================ FILE: tests/vanilla/basic.test.tsx ================================================ import { expect, it } from 'vitest' import { atom } from 'jotai/vanilla' it('creates atoms', () => { // primitive atom const countAtom = atom(0) const anotherCountAtom = atom(1) // read-only derived atom const doubledCountAtom = atom((get) => get(countAtom) * 2) // read-write derived atom const sumCountAtom = atom( (get) => get(countAtom) + get(anotherCountAtom), (get, set, value: number) => { set(countAtom, get(countAtom) + value / 2) set(anotherCountAtom, get(anotherCountAtom) + value / 2) }, ) // write-only derived atom const decrementCountAtom = atom(null, (get, set) => { set(countAtom, get(countAtom) - 1) }) delete countAtom.debugLabel delete doubledCountAtom.debugLabel delete sumCountAtom.debugLabel delete decrementCountAtom.debugLabel expect({ countAtom, doubledCountAtom, sumCountAtom, decrementCountAtom, }).toMatchInlineSnapshot(` { "countAtom": { "init": 0, "read": [Function], "toString": [Function], "write": [Function], }, "decrementCountAtom": { "init": null, "read": [Function], "toString": [Function], "write": [Function], }, "doubledCountAtom": { "read": [Function], "toString": [Function], }, "sumCountAtom": { "read": [Function], "toString": [Function], "write": [Function], }, } `) }) it('[DEV-ONLY] should include debugLabel in toString output', () => { const countAtom = atom(0) countAtom.debugLabel = 'count' expect(countAtom.toString()).toContain(':count') }) it('should let users mark atoms as private', () => { const internalAtom = atom(0) internalAtom.debugPrivate = true delete internalAtom.debugLabel expect(internalAtom).toMatchInlineSnapshot(` { "debugPrivate": true, "init": 0, "read": [Function], "toString": [Function], "write": [Function], } `) }) ================================================ FILE: tests/vanilla/dependency.test.tsx ================================================ import { afterEach, beforeEach, expect, it, vi } from 'vitest' import { atom, createStore } from 'jotai/vanilla' import { sleep } from '../test-utils' beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) it('can propagate updates with async atom chains', async () => { const store = createStore() const countAtom = atom(1) const asyncAtom = atom(async (get) => { const count = get(countAtom) await sleep(100) return count }) const async2Atom = atom((get) => get(asyncAtom)) const async3Atom = atom((get) => get(async2Atom)) expect(store.get(async3Atom) instanceof Promise).toBeTruthy() await vi.advanceTimersByTimeAsync(100) await expect(store.get(async3Atom)).resolves.toBe(1) store.set(countAtom, (c) => c + 1) expect(store.get(async3Atom) instanceof Promise).toBeTruthy() await vi.advanceTimersByTimeAsync(100) await expect(store.get(async3Atom)).resolves.toBe(2) store.set(countAtom, (c) => c + 1) expect(store.get(async3Atom) instanceof Promise).toBeTruthy() await vi.advanceTimersByTimeAsync(100) await expect(store.get(async3Atom)).resolves.toBe(3) }) it('can get async atom with deps more than once before resolving (#1668)', async () => { const countAtom = atom(0) const asyncAtom = atom(async (get) => { const count = get(countAtom) await sleep(100) return count }) const store = createStore() store.set(countAtom, (c) => c + 1) store.get(asyncAtom) store.set(countAtom, (c) => c + 1) await vi.advanceTimersByTimeAsync(100) await expect(store.get(asyncAtom)).resolves.toBe(2) }) it('correctly updates async derived atom after get/set update', async () => { const baseAtom = atom(0) const derivedAsyncAtom = atom( async (get) => get(baseAtom) + 1, async (_get, set, val) => set(baseAtom, val as number), ) const store = createStore() // NOTE: Have to .set() straight after await on .get(), so that it executes // in the same JS event loop cycle! let derived = await store.get(derivedAsyncAtom) await store.set(derivedAsyncAtom, 2) expect(derived).toBe(1) expect(store.get(baseAtom)).toBe(2) derived = await store.get(derivedAsyncAtom) expect(derived).toBe(3) }) it('correctly handles the same promise being returned twice from an atom getter (#2151)', async () => { const asyncDataAtom = atom(async () => { return 'Asynchronous Data' }) const counterAtom = atom(0) const derivedAtom = atom((get) => { get(counterAtom) // depending on sync data return get(asyncDataAtom) // returning a promise from another atom }) const store = createStore() store.get(derivedAtom) // setting the `counterAtom` dependency on the same JS event loop cycle, before // the `derivedAtom` promise resolves. store.set(counterAtom, 1) await expect(store.get(derivedAtom)).resolves.toBe('Asynchronous Data') }) it('keeps atoms mounted between recalculations', async () => { const metrics1 = { mounted: 0, unmounted: 0, } const atom1 = atom(0) atom1.onMount = () => { ++metrics1.mounted return () => { ++metrics1.unmounted } } const metrics2 = { mounted: 0, unmounted: 0, } const atom2 = atom(0) atom2.onMount = () => { ++metrics2.mounted return () => { ++metrics2.unmounted } } const derivedAtom = atom(async (get) => { get(atom1) await sleep(100) get(atom2) }) const unrelatedAtom = atom(0) const store = createStore() store.sub(derivedAtom, () => {}) await vi.advanceTimersByTimeAsync(100) store.set(unrelatedAtom, (c) => c + 1) expect(metrics1).toEqual({ mounted: 1, unmounted: 0, }) expect(metrics2).toEqual({ mounted: 1, unmounted: 0, }) store.set(atom1, (c) => c + 1) await vi.advanceTimersByTimeAsync(100) expect(metrics1).toEqual({ mounted: 1, unmounted: 0, }) expect(metrics2).toEqual({ mounted: 1, unmounted: 0, }) }) it('should not provide stale values to conditional dependents', () => { const dataAtom = atom([100]) const hasFilterAtom = atom(false) const filteredAtom = atom((get) => { const data = get(dataAtom) const hasFilter = get(hasFilterAtom) if (hasFilter) { return [] } else { return data } }) const stageAtom = atom((get) => { const hasFilter = get(hasFilterAtom) if (hasFilter) { const filtered = get(filteredAtom) return filtered.length === 0 ? 'is-empty' : 'has-data' } else { return 'no-filter' } }) const store = createStore() store.sub(filteredAtom, () => undefined) store.sub(stageAtom, () => undefined) expect(store.get(stageAtom), 'should start without filter').toBe('no-filter') store.set(hasFilterAtom, true) expect(store.get(stageAtom), 'should update').toBe('is-empty') }) it('settles never resolving async derivations with deps picked up sync', async () => { const resolve: ((value: number) => void)[] = [] const syncAtom = atom({ promise: new Promise((r) => resolve.push(r)), }) const asyncAtom = atom(async (get) => { return await get(syncAtom).promise }) const store = createStore() let sub = 0 const values: unknown[] = [] store.get(asyncAtom).then((value) => values.push(value)) store.sub(asyncAtom, () => { sub++ store.get(asyncAtom).then((value) => values.push(value)) }) store.set(syncAtom, { promise: new Promise((r) => resolve.push(r)), }) resolve[1]?.(1) await vi.advanceTimersByTimeAsync(0) expect(values).toEqual([1]) expect(sub).toBe(1) }) it('settles never resolving async derivations with deps picked up async', async () => { const resolve: ((value: number) => void)[] = [] const syncAtom = atom({ promise: new Promise((r) => resolve.push(r)), }) const asyncAtom = atom(async (get) => { // we want to pick up `syncAtom` as an async dep await sleep(100) return await get(syncAtom).promise }) const store = createStore() let sub = 0 const values: unknown[] = [] store.get(asyncAtom).then((value) => values.push(value)) store.sub(asyncAtom, () => { sub++ store.get(asyncAtom).then((value) => values.push(value)) }) await vi.advanceTimersByTimeAsync(100) store.set(syncAtom, { promise: new Promise((r) => resolve.push(r)), }) resolve[1]?.(1) await vi.advanceTimersByTimeAsync(100) expect(values).toEqual([1]) expect(sub).toBe(1) }) it('refreshes deps for each async read', async () => { const countAtom = atom(0) const depAtom = atom(false) const values: number[] = [] const asyncAtom = atom(async (get) => { const count = get(countAtom) values.push(count) if (count === 0) { get(depAtom) } await sleep(100) return count }) const store = createStore() store.get(asyncAtom) store.set(countAtom, (c) => c + 1) await vi.advanceTimersByTimeAsync(100) await expect(store.get(asyncAtom)).resolves.toBe(1) store.set(depAtom, true) store.get(asyncAtom) await vi.advanceTimersByTimeAsync(0) expect(values).toEqual([0, 1]) }) it('should not re-evaluate stable derived atom values in situations where dependencies are re-ordered (#2738)', () => { const callCounter = vi.fn() const countAtom = atom(0) const rootAtom = atom(false) const stableDep = atom((get) => { get(rootAtom) return 1 }) const stableDepDep = atom((get) => { get(stableDep) callCounter() return 2 + get(countAtom) }) const newAtom = atom((get) => { if (get(rootAtom) || get(countAtom) > 0) { return get(stableDepDep) } return get(stableDep) }) const store = createStore() store.sub(stableDepDep, () => {}) store.sub(newAtom, () => {}) expect(store.get(stableDepDep)).toBe(2) expect(callCounter).toHaveBeenCalledTimes(1) store.set(rootAtom, true) expect(store.get(newAtom)).toBe(2) expect(callCounter).toHaveBeenCalledTimes(1) store.set(rootAtom, false) store.set(countAtom, 1) expect(store.get(newAtom)).toBe(3) expect(callCounter).toHaveBeenCalledTimes(2) }) it('handles complex dependency chains', async () => { const baseAtom = atom(1) const derived1 = atom((get) => get(baseAtom) * 2) const derived2 = atom((get) => get(derived1) + 1) const asyncDerived = atom(async (get) => { const value = get(derived2) await sleep(100) return value * 2 }) const store = createStore() store.get(asyncDerived) await vi.advanceTimersByTimeAsync(100) await expect(store.get(asyncDerived)).resolves.toBe(6) store.set(baseAtom, 2) store.get(asyncDerived) await vi.advanceTimersByTimeAsync(100) await expect(store.get(asyncDerived)).resolves.toBe(10) }) it('can read sync derived atom in write without initializing', () => { const store = createStore() const a = atom(0) const b = atom((get) => get(a) + 1) const c = atom(null, (get, set) => set(a, get(b))) store.set(c) expect(store.get(a)).toBe(1) store.set(c) // note: this is why write get needs to update deps expect(store.get(a)).toBe(2) }) it('can read in write function without changing dependencies', () => { // https://github.com/pmndrs/jotai/discussions/2789 const a = atom(0) let bReadCount = 0 const b = atom( (get) => { ++bReadCount return get(a) }, () => {}, ) let bIsMounted = false b.onMount = () => { bIsMounted = true } const c = atom((get) => get(a)) const w = atom(null, (get, set) => { expect(bReadCount).toBe(0) const bValue = get(b) expect(bReadCount).toBe(1) set(a, bValue + 1) expect(bReadCount).toBe(1) }) const store = createStore() store.sub(c, () => {}) // mounts c,a store.set(w) expect(bIsMounted).toBe(false) }) it('can cache reading an atom in write function (without mounting)', () => { let aReadCount = 0 const a = atom(() => { ++aReadCount return 'a' }) const w = atom(null, (get) => get(a)) const store = createStore() store.set(w) expect(aReadCount).toBe(1) store.set(w) expect(aReadCount).toBe(1) }) it('can cache reading an atom in write function (with mounting)', () => { let aReadCount = 0 const a = atom(() => { ++aReadCount return 'a' }) const w = atom(null, (get) => get(a)) const store = createStore() store.sub(a, () => {}) // mounts a store.set(w) expect(aReadCount).toBe(1) store.set(w) expect(aReadCount).toBe(1) }) it('batches sync writes', () => { const a = atom(0) const b = atom((get) => get(a)) const fetch = vi.fn() const c = atom((get) => fetch(get(a))) const w = atom(null, (get, set) => { set(a, 1) expect(get(b)).toBe(1) expect(fetch).toHaveBeenCalledTimes(0) }) const store = createStore() store.sub(b, () => {}) store.sub(c, () => {}) fetch.mockClear() store.set(w) expect(fetch).toHaveBeenCalledOnce() expect(fetch).toBeCalledWith(1) expect(store.get(a)).toBe(1) }) ================================================ FILE: tests/vanilla/derive.test.tsx ================================================ import { describe, expect, it, vi } from 'vitest' import { atom, createStore } from 'jotai/vanilla' import type { Atom } from 'jotai/vanilla' import { INTERNAL_buildStoreRev2 as INTERNAL_buildStore, INTERNAL_getBuildingBlocksRev2 as INTERNAL_getBuildingBlocks, } from 'jotai/vanilla/internals' type AtomStateMapType = ReturnType[0] const deriveStore = ( store: ReturnType, enhanceAtomStateMap: (atomStateMap: AtomStateMapType) => AtomStateMapType, ): ReturnType => { const buildingBlocks = INTERNAL_getBuildingBlocks(store) const atomStateMap = buildingBlocks[0] const derivedStore = INTERNAL_buildStore(enhanceAtomStateMap(atomStateMap)) return derivedStore } describe('deriveStore for scoping atoms', () => { /** * a * S1[a]: a1 */ it('primitive atom', () => { const a = atom('a') a.onMount = (setSelf) => setSelf((v) => v + ':mounted') const scopedAtoms = new Set>([a]) const store = createStore() const derivedStore = deriveStore(store, (atomStateMap) => { const scopedAtomStateMap = new WeakMap() return { get: (atom) => { if (scopedAtoms.has(atom)) { return scopedAtomStateMap.get(atom) } return atomStateMap.get(atom) }, set: (atom, atomState) => { if (scopedAtoms.has(atom)) { scopedAtomStateMap.set(atom, atomState) } else { atomStateMap.set(atom, atomState) } }, has: (atom) => { if (scopedAtoms.has(atom)) { return scopedAtomStateMap.has(atom) } return atomStateMap.has(atom) }, delete: (atom) => { if (scopedAtoms.has(atom)) { return scopedAtomStateMap.delete(atom) } return atomStateMap.delete(atom) }, } }) expect(store.get(a)).toBe('a') expect(derivedStore.get(a)).toBe('a') derivedStore.sub(a, vi.fn()) expect(store.get(a)).toBe('a') expect(derivedStore.get(a)).toBe('a:mounted') derivedStore.set(a, (v) => v + ':updated') expect(store.get(a)).toBe('a') expect(derivedStore.get(a)).toBe('a:mounted:updated') }) /** * a, b, c(a + b) * S1[a]: a1, b0, c0(a1 + b0) */ it('derived atom (scoping primitive)', () => { const a = atom('a') const b = atom('b') const c = atom((get) => get(a) + get(b)) const scopedAtoms = new Set>([a]) const store = createStore() const derivedStore = deriveStore(store, (atomStateMap) => { const scopedAtomStateMap = new WeakMap() return { get: (atom) => { if (scopedAtoms.has(atom)) { return scopedAtomStateMap.get(atom) } return atomStateMap.get(atom) }, set: (atom, atomState) => { if (scopedAtoms.has(atom)) { scopedAtomStateMap.set(atom, atomState) } else { atomStateMap.set(atom, atomState) } }, has: (atom) => { if (scopedAtoms.has(atom)) { return scopedAtomStateMap.has(atom) } return atomStateMap.has(atom) }, delete: (atom) => { if (scopedAtoms.has(atom)) { return scopedAtomStateMap.delete(atom) } return atomStateMap.delete(atom) }, } }) expect(store.get(c)).toBe('ab') expect(derivedStore.get(c)).toBe('ab') derivedStore.set(a, 'a2') expect(store.get(c)).toBe('ab') expect(derivedStore.get(c)).toBe('a2b') }) /** * a, b(a) * S1[a]: a1, b0(a1) */ it('derived atom with subscribe', () => { const a = atom('a') const b = atom( (get) => get(a), (_get, set, v: string) => set(a, v), ) const scopedAtoms = new Set>([a]) function makeStores() { const store = createStore() const derivedStore = deriveStore(store, (atomStateMap) => { const scopedAtomStateMap = new WeakMap() return { get: (atom) => { if (scopedAtoms.has(atom)) { return scopedAtomStateMap.get(atom) } return atomStateMap.get(atom) }, set: (atom, atomState) => { if (scopedAtoms.has(atom)) { scopedAtomStateMap.set(atom, atomState) } else { atomStateMap.set(atom, atomState) } }, has: (atom) => { if (scopedAtoms.has(atom)) { return scopedAtomStateMap.has(atom) } return atomStateMap.has(atom) }, delete: (atom) => { if (scopedAtoms.has(atom)) { return scopedAtomStateMap.delete(atom) } return atomStateMap.delete(atom) }, } }) expect(store.get(b)).toBe('a') expect(derivedStore.get(b)).toBe('a') return { store, derivedStore } } /** * Ba[ ]: a0, b0(a0) * S1[a]: a1, b0(a1) */ { const { store, derivedStore } = makeStores() store.set(b, '*') expect(store.get(b)).toBe('*') expect(derivedStore.get(b)).toBe('a') } { const { store, derivedStore } = makeStores() derivedStore.set(b, '*') expect(store.get(b)).toBe('a') expect(derivedStore.get(b)).toBe('*') } { const { store, derivedStore } = makeStores() const storeCallback = vi.fn() const derivedCallback = vi.fn() store.sub(b, storeCallback) derivedStore.sub(b, derivedCallback) store.set(b, '*') expect(store.get(b)).toBe('*') //expect(derivedStore.get(b)).toBe('a') // FIXME: received '*' expect(storeCallback).toHaveBeenCalledTimes(1) //expect(derivedCallback).toHaveBeenCalledTimes(0) // FIXME: received 1 } { const { store, derivedStore } = makeStores() const storeCallback = vi.fn() const derivedCallback = vi.fn() store.sub(b, storeCallback) derivedStore.sub(b, derivedCallback) derivedStore.set(b, '*') //expect(store.get(b)).toBe('a') // FIXME: received '*' expect(derivedStore.get(b)).toBe('*') expect(storeCallback).toHaveBeenCalledTimes(0) expect(derivedCallback).toHaveBeenCalledTimes(1) } }) }) it('should pass the correct store instance to the atom initializer', () => { expect.assertions(2) const baseStore = createStore() const derivedStore = deriveStore(baseStore, (atomStateMap) => { const initializedAtoms = new WeakSet() return { get: (atom) => { if (!initializedAtoms.has(atom)) { return undefined } return atomStateMap.get(atom) }, set: (atom, atomState) => { initializedAtoms.add(atom) atomStateMap.set(atom, atomState) }, has: (atom) => { if (!initializedAtoms.has(atom)) { return false } return atomStateMap.has(atom) }, delete: (atom) => { initializedAtoms.delete(atom) return atomStateMap.delete(atom) }, } }) const a = atom(null) a.INTERNAL_onInit = (store) => { expect(store).toBe(baseStore) } baseStore.get(a) a.INTERNAL_onInit = (store) => { expect(store).toBe(derivedStore) } derivedStore.get(a) }) ================================================ FILE: tests/vanilla/effect.test.ts ================================================ import { afterEach, beforeEach, expect, it, vi } from 'vitest' import type { Atom, Getter, Setter, WritableAtom } from 'jotai/vanilla' import { atom, createStore } from 'jotai/vanilla' import { INTERNAL_getBuildingBlocksRev2 as INTERNAL_getBuildingBlocks, INTERNAL_initializeStoreHooksRev2 as INTERNAL_initializeStoreHooks, } from 'jotai/vanilla/internals' beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) type Cleanup = () => void type Effect = (get: Getter, set: Setter) => Cleanup | void type Ref = { get?: Getter inProgress: number epoch: number cleanup?: Cleanup | undefined } function syncEffect(effect: Effect): Atom { const refAtom = atom(() => ({ inProgress: 0, epoch: 0 })) const refreshAtom = atom(0) const internalAtom = atom( (get) => { get(refreshAtom) const ref = get(refAtom) if (ref.inProgress) { return ref.epoch } ref.get = get return ++ref.epoch }, () => {}, ) internalAtom.onMount = () => { return () => {} } internalAtom.INTERNAL_onInit = (store) => { const ref = store.get(refAtom) const runEffect = () => { const deps = new Set>() try { ref.cleanup?.() ref.cleanup = effect( (a) => { deps.add(a) return store.get(a) }, (a, ...args) => { try { ++ref.inProgress return store.set(a, ...args) } finally { --ref.inProgress } }, ) || undefined } finally { deps.forEach(ref.get!) } } const buildingBlocks = INTERNAL_getBuildingBlocks(store) const storeHooks = INTERNAL_initializeStoreHooks(buildingBlocks[6]) const syncEffectChannel = ensureSyncEffectChannel(store) storeHooks.m.add(internalAtom, () => { // mount store.set(refreshAtom, (v) => v + 1) }) storeHooks.u.add(internalAtom, () => { // unmount syncEffectChannel.add(() => { ref.cleanup?.() delete ref.cleanup }) }) storeHooks.c.add(internalAtom, () => { // update syncEffectChannel.add(runEffect) }) } return atom((get) => { get(internalAtom) }) } const syncEffectChannelSymbol = Symbol() function ensureSyncEffectChannel(store: any) { if (!store[syncEffectChannelSymbol]) { store[syncEffectChannelSymbol] = new Set<() => void>() const buildingBlocks = INTERNAL_getBuildingBlocks(store) const storeHooks = INTERNAL_initializeStoreHooks(buildingBlocks[6]) storeHooks.f.add(() => { const syncEffectChannel = store[syncEffectChannelSymbol] as Set< () => void > const fns = Array.from(syncEffectChannel) syncEffectChannel.clear() fns.forEach((fn: () => void) => fn()) }) } return store[syncEffectChannelSymbol] as Set<() => void> } it('fires after recomputeDependents and before atom listeners', async function test() { const store = createStore() const a = atom({} as { v?: number }) let r const e = syncEffect(function effect(get) { r = get(a).v }) const b = atom(function bAtomRead(get) { const aValue = get(a) get(e) // sets property `v` inside recomputeDependents aValue.v = 1 return aValue }) store.sub(b, function bAtomListener() { // sets property `v` inside atom listener store.get(a).v = 2 }) store.set(a, { v: 0 }) expect(r).toBe(1) }) it('responds to changes to atoms when subscribed', () => { const store = createStore() const a = atom(1) const b = atom(1) const w = atom(null, (_get, set, value: number) => { set(a, value) set(b, value) }) const results: number[] = [] const cleanup = vi.fn() const effect = vi.fn((get: Getter) => { results.push(get(a) * 10 + get(b)) return cleanup }) const e = syncEffect(effect) const unsub = store.sub(e, () => {}) // mount syncEffect expect(effect).toBeCalledTimes(1) expect(results).toStrictEqual([11]) // initial values at time of effect mount store.set(a, 2) expect(results).toStrictEqual([11, 21]) store.set(b, 2) expect(results).toStrictEqual([11, 21, 22]) store.set(w, 3) // intermediate state of '32' should not be recorded since the effect runs _after_ graph has been computed expect(results).toStrictEqual([11, 21, 22, 33]) expect(cleanup).toBeCalledTimes(3) expect(effect).toBeCalledTimes(4) unsub() expect(cleanup).toBeCalledTimes(4) expect(effect).toBeCalledTimes(4) store.set(a, 4) // the effect is unmounted so no more updates expect(results).toStrictEqual([11, 21, 22, 33]) expect(effect).toBeCalledTimes(4) }) it('responds to changes to atoms when mounted with get', () => { const store = createStore() const a = atom(1) const b = atom(1) const w = atom(null, (_get, set, value: number) => { set(a, value) set(b, value) }) const results: number[] = [] const cleanup = vi.fn() const effect = vi.fn((get: Getter) => { results.push(get(a) * 10 + get(b)) return cleanup }) const e = syncEffect(effect) const d = atom((get) => get(e)) const unsub = store.sub(d, () => {}) // mount syncEffect expect(effect).toBeCalledTimes(1) expect(results).toStrictEqual([11]) // initial values at time of effect mount store.set(a, 2) expect(results).toStrictEqual([11, 21]) store.set(b, 2) expect(results).toStrictEqual([11, 21, 22]) store.set(w, 3) // intermediate state of '32' should not be recorded since the effect runs _after_ graph has been computed expect(results).toStrictEqual([11, 21, 22, 33]) expect(cleanup).toBeCalledTimes(3) expect(effect).toBeCalledTimes(4) unsub() expect(cleanup).toBeCalledTimes(4) expect(effect).toBeCalledTimes(4) }) it('sets values to atoms without causing infinite loop', () => { const store = createStore() const a = atom(1) const effect = vi.fn((get: Getter, set: Setter) => { set(a, get(a) + 1) }) const e = syncEffect(effect) const unsub = store.sub(e, () => {}) // mount syncEffect expect(effect).toBeCalledTimes(1) expect(store.get(a)).toBe(2) // initial values at time of effect mount store.set(a, (v) => ++v) expect(store.get(a)).toBe(4) expect(effect).toBeCalledTimes(2) unsub() expect(effect).toBeCalledTimes(2) }) // TODO: consider removing this after we provide a new syncEffect implementation it('supports recursive setting synchronous in read', async () => { const store = createStore() const a = atom(0) const refreshAtom = atom(0) type Ref = { isMounted?: true recursing: number set: Setter } const refAtom = atom( () => ({ recursing: 0 }) as Ref, (get, set) => { const ref = get(refAtom) ref.isMounted = true ref.set = set set(refreshAtom, (v) => v + 1) }, ) refAtom.onMount = (mount) => mount() const effectAtom = atom((get) => { get(refreshAtom) const ref = get(refAtom) if (!ref.isMounted) { return } const recurse = ( a: WritableAtom, ...args: Args ): Result => { ++ref.recursing const value = ref.set(a, ...args) return value as Result } function runEffect() { const v = get(a) if (v < 5) { recurse(a, (v) => v + 1) } } if (ref.recursing) { let prevRecursing = ref.recursing do { prevRecursing = ref.recursing runEffect() } while (prevRecursing !== ref.recursing) ref.recursing = 0 return Promise.resolve() } return Promise.resolve().then(runEffect) }) store.sub(effectAtom, () => {}) await vi.advanceTimersByTimeAsync(0) expect(store.get(a)).toBe(5) }) ================================================ FILE: tests/vanilla/internals.test.tsx ================================================ import { describe, expect, it, vi } from 'vitest' import { atom, createStore } from 'jotai' import type { INTERNAL_AtomState, INTERNAL_AtomStateMap, INTERNAL_BuildingBlocks, INTERNAL_InvalidatedAtoms, } from 'jotai/vanilla/internals' import { INTERNAL_buildStoreRev2 as INTERNAL_buildStore, INTERNAL_getBuildingBlocksRev2 as INTERNAL_getBuildingBlocks, INTERNAL_initializeStoreHooksRev2 as INTERNAL_initializeStoreHooks, } from 'jotai/vanilla/internals' const buildingBlockLength = 28 describe('internals', () => { it('should not return a sparse building blocks array', () => { { const store = createStore() const buildingBlocks = INTERNAL_getBuildingBlocks(store) expect(isBuildingBlocks(buildingBlocks)).toBe(true) } { const store = INTERNAL_buildStore() const buildingBlocks = INTERNAL_getBuildingBlocks(store) expect(isBuildingBlocks(buildingBlocks)).toBe(true) } }) it('internals should not hold stale references', () => { const createMockAtomStateMap = () => { return { get: vi.fn(() => { return { d: new Map(), p: new Set(), n: 0, v: 0, } as INTERNAL_AtomState }), set: vi.fn(), has: vi.fn(() => true), delete: vi.fn(() => true), } as INTERNAL_AtomStateMap } const mockAtomStateMap1 = createMockAtomStateMap() const buildingBlocks1: Partial = [ mockAtomStateMap1, ] const store1 = INTERNAL_buildStore(...buildingBlocks1) const buildingBlocks2 = [ ...INTERNAL_getBuildingBlocks(store1), ] as INTERNAL_BuildingBlocks const mockAtomStateMap2 = createMockAtomStateMap() buildingBlocks2[0] = mockAtomStateMap2 const store2 = INTERNAL_buildStore(...buildingBlocks2) store2.get(atom(0)) expect(mockAtomStateMap1.get).not.toBeCalled() expect(mockAtomStateMap2.get).toBeCalled() }) it('should transform external building blocks differently from internal ones', () => { const didRun = { internal: vi.fn(), external: vi.fn(), } const bb0 = [] as Partial bb0[21] = function storeGet1() { didRun.internal() } as INTERNAL_BuildingBlocks[21] let bbInternal: Readonly | undefined function storeGet() { didRun.external() } bb0[24] = (bbi) => { bbInternal = bbi const bb1 = [...bbi] as INTERNAL_BuildingBlocks bb1[21] = storeGet as INTERNAL_BuildingBlocks[21] return bb1 } const store1 = INTERNAL_buildStore(...bb0) const bb1 = INTERNAL_getBuildingBlocks(store1) expect(isBuildingBlocks(bb1)).toBe(true) expect(isBuildingBlocks(bbInternal)).toBe(true) const store2 = INTERNAL_buildStore(...bb1) const bb2 = INTERNAL_getBuildingBlocks(store2) expect(isBuildingBlocks(bb2)).toBe(true) expect(isBuildingBlocks(bbInternal)).toBe(true) expect(bb0[21]).not.toBe(bb1[21]) expect(bb1[21]).toBe(bb2[21]) store1.get(atom(0)) expect(didRun.internal).toBeCalledTimes(1) expect(didRun.external).toBeCalledTimes(0) vi.clearAllMocks() store2.get(atom(0)) expect(didRun.internal).toBeCalledTimes(0) expect(didRun.external).toBeCalledTimes(1) }) it('invalidateDependents should not invalidate the same dependent twice via multiple paths', () => { const invalidatedAtoms = (() => { const map = new WeakMap() return { get: (key) => map.get(key), set: (key, value) => { const prev = map.get(key) if (prev === value) { throw new Error('duplicate invalidation') } map.set(key, value) }, has: (key) => map.has(key), delete: (key) => map.delete(key), } as INTERNAL_InvalidatedAtoms })() const buildingBlocks: Partial = [] buildingBlocks[2] = invalidatedAtoms const store = INTERNAL_buildStore(...buildingBlocks) const baseAtom = atom(0) const midAtom1 = atom((get) => get(baseAtom)) const midAtom2 = atom((get) => get(baseAtom)) const leafAtom = atom((get) => get(midAtom1) + get(midAtom2)) const unsub = store.sub(leafAtom, () => {}) const invalidateDependents = INTERNAL_getBuildingBlocks(store)[15] expect(() => invalidateDependents(store, baseAtom)).not.toThrow() unsub() }) }) describe('store hooks', () => { // Helper function to create store with hooks const createStoreWithHooks = () => { const storeHooks = INTERNAL_initializeStoreHooks({}) const buildingBlocks = [] as Partial buildingBlocks[6] = storeHooks const store = INTERNAL_buildStore(...buildingBlocks) return { store, storeHooks } } describe('init hook (i)', () => { it('should call init hook when atom state is initialized', () => { const { store, storeHooks } = createStoreWithHooks() const baseAtom = atom(0) const initCallback = vi.fn() storeHooks.i.add(baseAtom, initCallback) store.get(baseAtom) expect(initCallback).toHaveBeenCalledTimes(1) }) }) describe('read hook (r)', () => { it('should call read hook when atom is read', () => { const { store, storeHooks } = createStoreWithHooks() const baseAtom = atom(0) const derivedAtom = atom((get) => get(baseAtom)) const readCallback = vi.fn() storeHooks.r.add(derivedAtom, readCallback) store.get(derivedAtom) expect(readCallback).toHaveBeenCalledTimes(1) readCallback.mockClear() store.get(derivedAtom) expect(readCallback).toHaveBeenCalledTimes(0) store.set(baseAtom, 1) store.get(derivedAtom) expect(readCallback).toHaveBeenCalledTimes(1) }) }) describe('mount hook (m)', () => { it('should call mount hook when atom is mounted', () => { const { store, storeHooks } = createStoreWithHooks() const countAtom = atom(0) const mountCallback = vi.fn() storeHooks.m.add(countAtom, mountCallback) const unsub = store.sub(countAtom, () => {}) expect(mountCallback).toHaveBeenCalledTimes(1) unsub() }) }) describe('unmount hook (u)', () => { it('should call unmount hook when atom is unmounted', () => { const { store, storeHooks } = createStoreWithHooks() const countAtom = atom(0) const unmountCallback = vi.fn() storeHooks.u.add(countAtom, unmountCallback) const unsub = store.sub(countAtom, () => {}) unsub() expect(unmountCallback).toHaveBeenCalledTimes(1) }) }) describe('change hook (c)', () => { it('should call change hook when atom value changes', () => { const { store, storeHooks } = createStoreWithHooks() const countAtom = atom(0) const changeCallback = vi.fn() storeHooks.c.add(countAtom, changeCallback) const unsub = store.sub(countAtom, () => {}) store.set(countAtom, 1) expect(changeCallback).toHaveBeenCalledTimes(1) changeCallback.mockClear() store.set(countAtom, 1) expect(changeCallback).toHaveBeenCalledTimes(0) unsub() }) }) describe('flush hook (f)', () => { it('should call flush hook when callbacks are flushed', () => { const { store, storeHooks } = createStoreWithHooks() const countAtom = atom(0) const flushCallback = vi.fn() storeHooks.f.add(flushCallback) const unsub = store.sub(countAtom, () => {}) expect(flushCallback).toHaveBeenCalledTimes(1) flushCallback.mockClear() store.set(countAtom, 1) expect(flushCallback).toHaveBeenCalledTimes(1) flushCallback.mockClear() unsub() expect(flushCallback).toHaveBeenCalledTimes(1) }) }) }) function isSparse(arr: ReadonlyArray) { return arr.some((_, i) => !Object.prototype.hasOwnProperty.call(arr, i)) } function isBuildingBlocks(blocks: ReadonlyArray | undefined) { return ( blocks !== undefined && blocks.length === buildingBlockLength && isSparse(blocks) === false ) } ================================================ FILE: tests/vanilla/memoryleaks.test.ts ================================================ import LeakDetector from 'jest-leak-detector' import { describe, expect, it } from 'vitest' import { atom, createStore } from 'jotai/vanilla' import type { Atom } from 'jotai/vanilla' describe('memory leaks (get & set only)', () => { it('one atom', async () => { const store = createStore() let objAtom: Atom | undefined = atom({}) const detector = new LeakDetector(store.get(objAtom)) objAtom = undefined await Promise.resolve() expect(await detector.isLeaking()).toBe(false) }) it('two atoms', async () => { const store = createStore() let objAtom: Atom | undefined = atom({}) const detector1 = new LeakDetector(store.get(objAtom)) let derivedAtom: Atom | undefined = atom((get) => ({ obj: objAtom && get(objAtom), })) const detector2 = new LeakDetector(store.get(derivedAtom)) objAtom = undefined derivedAtom = undefined await Promise.resolve() expect(await detector1.isLeaking()).toBe(false) expect(await detector2.isLeaking()).toBe(false) }) it('should not hold onto dependent atoms that are not mounted', async () => { const store = createStore() const objAtom = atom({}) let depAtom: Atom | undefined = atom((get) => get(objAtom)) const detector = new LeakDetector(depAtom) store.get(depAtom) depAtom = undefined await Promise.resolve() await expect(detector.isLeaking()).resolves.toBe(false) }) it('with a long-lived base atom', async () => { const store = createStore() const objAtom = atom({}) let derivedAtom: Atom | undefined = atom((get) => ({ obj: get(objAtom), })) const detector = new LeakDetector(store.get(derivedAtom)) derivedAtom = undefined await Promise.resolve() expect(await detector.isLeaking()).toBe(false) }) }) describe('memory leaks (with subscribe)', () => { it('one atom', async () => { const store = createStore() let objAtom: Atom | undefined = atom({}) const detector = new LeakDetector(store.get(objAtom)) let unsub: (() => void) | undefined = store.sub(objAtom, () => {}) unsub() unsub = undefined objAtom = undefined await Promise.resolve() expect(await detector.isLeaking()).toBe(false) }) it('two atoms', async () => { const store = createStore() let objAtom: Atom | undefined = atom({}) const detector1 = new LeakDetector(store.get(objAtom)) let derivedAtom: Atom | undefined = atom((get) => ({ obj: objAtom && get(objAtom), })) const detector2 = new LeakDetector(store.get(derivedAtom)) let unsub: (() => void) | undefined = store.sub(objAtom, () => {}) unsub() unsub = undefined objAtom = undefined derivedAtom = undefined await Promise.resolve() expect(await detector1.isLeaking()).toBe(false) expect(await detector2.isLeaking()).toBe(false) }) it('with a long-lived base atom', async () => { const store = createStore() const objAtom = atom({}) let derivedAtom: Atom | undefined = atom((get) => ({ obj: get(objAtom), })) const detector = new LeakDetector(store.get(derivedAtom)) let unsub: (() => void) | undefined = store.sub(objAtom, () => {}) unsub() unsub = undefined derivedAtom = undefined await Promise.resolve() expect(await detector.isLeaking()).toBe(false) }) }) describe('memory leaks (with dependencies)', () => { it('sync dependency', async () => { const store = createStore() let objAtom: Atom | undefined = atom({}) const detector = new LeakDetector(store.get(objAtom)) const atom1 = atom(0) const atom2 = atom((get) => get(atom1) || (objAtom && get(objAtom))) store.sub(atom2, () => {}) store.set(atom1, 1) objAtom = undefined await Promise.resolve() expect(await detector.isLeaking()).toBe(false) }) it('async dependency', async () => { const store = createStore() let objAtom: Atom | undefined = atom({}) const detector = new LeakDetector(store.get(objAtom)) const atom1 = atom(0) const atom2 = atom(async (get) => get(atom1) || (objAtom && get(objAtom))) store.sub(atom2, () => {}) store.set(atom1, 1) objAtom = undefined await Promise.resolve() expect(await detector.isLeaking()).toBe(false) }) it('async await dependency', async () => { const store = createStore() let objAtom: Atom | undefined = atom({}) const detector = new LeakDetector(store.get(objAtom)) const atom1 = atom(0) const atom2 = atom(async (get) => { await Promise.resolve() return get(atom1) || (objAtom && get(objAtom)) }) store.sub(atom2, () => {}) store.set(atom1, 1) objAtom = undefined await Promise.resolve() expect(await detector.isLeaking()).toBe(false) }) }) ================================================ FILE: tests/vanilla/store.test.tsx ================================================ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { atom, createStore } from 'jotai/vanilla' import type { Atom, Getter, PrimitiveAtom } from 'jotai/vanilla' import { INTERNAL_buildStoreRev2 as INTERNAL_buildStore, INTERNAL_getBuildingBlocksRev2 as INTERNAL_getBuildingBlocks, INTERNAL_initializeStoreHooksRev2 as INTERNAL_initializeStoreHooks, } from 'jotai/vanilla/internals' import type { INTERNAL_Store } from 'jotai/vanilla/internals' import { sleep } from '../test-utils' let savedConsoleWarn: any beforeEach(() => { vi.useFakeTimers() savedConsoleWarn = console.warn console.warn = vi.fn() }) afterEach(() => { vi.useRealTimers() console.warn = savedConsoleWarn }) type DevStore = { get_mounted_atoms: () => Set> } const createDevStore = (): INTERNAL_Store & DevStore => { const storeHooks = INTERNAL_initializeStoreHooks({}) const store = INTERNAL_buildStore( undefined, undefined, undefined, undefined, undefined, undefined, storeHooks, ) const debugMountedAtoms = new Set>() storeHooks.m.add(undefined, (atom) => { debugMountedAtoms.add(atom) }) storeHooks.u.add(undefined, (atom) => { debugMountedAtoms.delete(atom) }) const devStore: DevStore = { get_mounted_atoms: () => debugMountedAtoms, } return Object.assign(store, devStore) } type AtomStateMapType = ReturnType[0] const deriveStore = ( store: ReturnType, enhanceAtomStateMap: (atomStateMap: AtomStateMapType) => AtomStateMapType, ): ReturnType => { const buildingBlocks = INTERNAL_getBuildingBlocks(store) const atomStateMap = buildingBlocks[0] const derivedStore = INTERNAL_buildStore(enhanceAtomStateMap(atomStateMap)) return derivedStore } it('should not fire on subscribe', async () => { const store = createStore() const countAtom = atom(0) const callback1 = vi.fn() const callback2 = vi.fn() store.sub(countAtom, callback1) store.sub(countAtom, callback2) expect(callback1).not.toHaveBeenCalled() expect(callback2).not.toHaveBeenCalled() }) it('should not fire subscription if primitive atom value is the same', () => { const store = createStore() const countAtom = atom(0) const callback = vi.fn() store.sub(countAtom, callback) const calledTimes = callback.mock.calls.length store.set(countAtom, 0) expect(callback).toHaveBeenCalledTimes(calledTimes) }) it('should not fire subscription if derived atom value is the same', () => { const store = createStore() const countAtom = atom(0) const derivedAtom = atom((get) => get(countAtom) * 0) const callback = vi.fn() store.sub(derivedAtom, callback) const calledTimes = callback.mock.calls.length store.set(countAtom, 1) expect(callback).toHaveBeenCalledTimes(calledTimes) }) it('should unmount with store.get', () => { const store = createDevStore() const countAtom = atom(0) const callback = vi.fn() const unsub = store.sub(countAtom, callback) store.get(countAtom) unsub() const result = Array.from(store.get_mounted_atoms()) expect(result).toEqual([]) }) it('should unmount dependencies with store.get', () => { const store = createDevStore() const countAtom = atom(0) const derivedAtom = atom((get) => get(countAtom) * 2) const callback = vi.fn() const unsub = store.sub(derivedAtom, callback) store.get(derivedAtom) unsub() const result = Array.from(store.get_mounted_atoms()) expect(result).toEqual([]) }) it('should update async atom with delay (#1813)', async () => { const countAtom = atom(0) const delayedAtom = atom(async (get) => { const count = get(countAtom) await sleep(100) return count }) const store = createStore() store.get(delayedAtom) store.set(countAtom, 1) await vi.advanceTimersByTimeAsync(100) await expect(store.get(delayedAtom)).resolves.toBe(1) }) it('should override a promise by setting', async () => { const store = createStore() const countAtom = atom(Promise.resolve(0)) const infinitePending = new Promise(() => {}) store.set(countAtom, infinitePending) const promise1 = store.get(countAtom) expect(promise1).toBe(infinitePending) store.set(countAtom, Promise.resolve(1)) const promise2 = store.get(countAtom) expect(await promise2).toBe(1) }) it('should update async atom with deps after await (#1905)', async () => { const countAtom = atom(0) const delayedAtom = atom(async (get) => { await sleep(100) const count = get(countAtom) return count }) const derivedAtom = atom(async (get) => { const count = await get(delayedAtom) return count }) const store = createStore() let lastValue = store.get(derivedAtom) const unsub = store.sub(derivedAtom, () => { lastValue = store.get(derivedAtom) }) store.set(countAtom, 1) await vi.advanceTimersByTimeAsync(100) expect(await lastValue).toBe(1) store.set(countAtom, 2) await vi.advanceTimersByTimeAsync(100) expect(await lastValue).toBe(2) store.set(countAtom, 3) await vi.advanceTimersByTimeAsync(100) expect(await lastValue).toBe(3) unsub() }) it('should not fire subscription when async atom promise is the same', async () => { const promise = Promise.resolve() const promiseAtom = atom(promise) const derivedGetter = vi.fn((get: Getter) => get(promiseAtom)) const derivedAtom = atom(derivedGetter) const store = createStore() expect(derivedGetter).not.toHaveBeenCalled() const promiseListener = vi.fn() const promiseUnsub = store.sub(promiseAtom, promiseListener) const derivedListener = vi.fn() const derivedUnsub = store.sub(derivedAtom, derivedListener) expect(derivedGetter).toHaveBeenCalledOnce() expect(promiseListener).not.toHaveBeenCalled() expect(derivedListener).not.toHaveBeenCalled() store.get(promiseAtom) store.get(derivedAtom) expect(derivedGetter).toHaveBeenCalledOnce() expect(promiseListener).not.toHaveBeenCalled() expect(derivedListener).not.toHaveBeenCalled() store.set(promiseAtom, promise) expect(derivedGetter).toHaveBeenCalledOnce() expect(promiseListener).not.toHaveBeenCalled() expect(derivedListener).not.toHaveBeenCalled() store.set(promiseAtom, promise) expect(derivedGetter).toHaveBeenCalledOnce() expect(promiseListener).not.toHaveBeenCalled() expect(derivedListener).not.toHaveBeenCalled() promiseUnsub() derivedUnsub() }) it('should notify subscription with tree dependencies (#1956)', () => { const valueAtom = atom(1) const dep1Atom = atom((get) => get(valueAtom) * 2) const dep2Atom = atom((get) => get(valueAtom) + get(dep1Atom)) const dep3Atom = atom((get) => get(dep1Atom)) const cb = vi.fn() const store = createStore() store.sub(dep2Atom, vi.fn()) // this will cause the bug store.sub(dep3Atom, cb) expect(cb).toBeCalledTimes(0) expect(store.get(dep3Atom)).toBe(2) store.set(valueAtom, (c) => c + 1) expect(cb).toBeCalledTimes(1) expect(store.get(dep3Atom)).toBe(4) }) it('should notify subscription with tree dependencies with bail-out', () => { const valueAtom = atom(1) const dep1Atom = atom((get) => get(valueAtom) * 2) const dep2Atom = atom((get) => get(valueAtom) * 0) const dep3Atom = atom((get) => get(dep1Atom) + get(dep2Atom)) const cb = vi.fn() const store = createStore() store.sub(dep1Atom, vi.fn()) store.sub(dep3Atom, cb) expect(cb).toBeCalledTimes(0) expect(store.get(dep3Atom)).toBe(2) store.set(valueAtom, (c) => c + 1) expect(cb).toBeCalledTimes(1) expect(store.get(dep3Atom)).toBe(4) }) it('should bail out with the same value with chained dependency (#2014)', () => { const store = createStore() const objAtom = atom({ count: 1 }) const countAtom = atom((get) => get(objAtom).count) const deriveFn = vi.fn((get: Getter) => get(countAtom)) const derivedAtom = atom(deriveFn) const deriveFurtherFn = vi.fn((get: Getter) => { get(objAtom) // intentional extra dependency return get(derivedAtom) }) const derivedFurtherAtom = atom(deriveFurtherFn) const callback = vi.fn() store.sub(derivedFurtherAtom, callback) expect(store.get(derivedAtom)).toBe(1) expect(store.get(derivedFurtherAtom)).toBe(1) expect(callback).toHaveBeenCalledTimes(0) expect(deriveFn).toHaveBeenCalledTimes(1) expect(deriveFurtherFn).toHaveBeenCalledTimes(1) store.set(objAtom, (obj) => ({ ...obj })) expect(callback).toHaveBeenCalledTimes(0) expect(deriveFn).toHaveBeenCalledTimes(1) expect(deriveFurtherFn).toHaveBeenCalledTimes(2) }) it('should not call read function for unmounted atoms (#2076)', () => { const store = createStore() const countAtom = atom(1) const derive1Fn = vi.fn((get: Getter) => get(countAtom)) const derived1Atom = atom(derive1Fn) const derive2Fn = vi.fn((get: Getter) => get(countAtom)) const derived2Atom = atom(derive2Fn) expect(store.get(derived1Atom)).toBe(1) expect(store.get(derived2Atom)).toBe(1) expect(derive1Fn).toHaveBeenCalledTimes(1) expect(derive2Fn).toHaveBeenCalledTimes(1) store.sub(derived2Atom, vi.fn()) store.set(countAtom, (c) => c + 1) expect(derive1Fn).toHaveBeenCalledTimes(1) expect(derive2Fn).toHaveBeenCalledTimes(2) }) it('should update with conditional dependencies (#2084)', () => { const store = createStore() const f1 = atom(false) const f2 = atom(false) const f3 = atom( (get) => get(f1) && get(f2), (_get, set, val: boolean) => { set(f1, val) set(f2, val) }, ) store.sub(f1, vi.fn()) store.sub(f2, vi.fn()) store.sub(f3, vi.fn()) store.set(f3, true) expect(store.get(f3)).toBe(true) }) it("should recompute dependents' state after onMount (#2098)", () => { const store = createStore() const condAtom = atom(false) const baseAtom = atom(false) baseAtom.onMount = (set) => set(true) const derivedAtom = atom( (get) => get(baseAtom), (_get, set, update: boolean) => set(baseAtom, update), ) const finalAtom = atom( (get) => (get(condAtom) ? get(derivedAtom) : undefined), (_get, set, value: boolean) => set(derivedAtom, value), ) store.sub(finalAtom, () => {}) // mounts finalAtom, but not baseAtom expect(store.get(baseAtom)).toBe(false) expect(store.get(derivedAtom)).toBe(false) expect(store.get(finalAtom)).toBe(undefined) store.set(condAtom, true) // mounts baseAtom expect(store.get(baseAtom)).toBe(true) expect(store.get(derivedAtom)).toBe(true) expect(store.get(finalAtom)).toBe(true) store.set(finalAtom, false) expect(store.get(baseAtom)).toBe(false) expect(store.get(derivedAtom)).toBe(false) expect(store.get(finalAtom)).toBe(false) }) it('should update derived atoms during write (#2107)', () => { const store = createStore() const baseCountAtom = atom(1) const countAtom = atom( (get) => get(baseCountAtom), (get, set, newValue: number) => { set(baseCountAtom, newValue) if (get(countAtom) !== newValue) { throw new Error('mismatch') } }, ) store.sub(countAtom, () => {}) expect(store.get(countAtom)).toBe(1) store.set(countAtom, 2) expect(store.get(countAtom)).toBe(2) }) it('resolves dependencies reliably after a delay (#2192)', async () => { expect.assertions(3) const countAtom = atom(0) let result: number | null = null const asyncAtom = atom(async (get) => { const count = get(countAtom) await sleep(100) return count }) const derivedAtom = atom( async (get, { setSelf }) => { get(countAtom) await sleep(50) result = await get(asyncAtom) if (result === 2) setSelf() // <-- necessary }, () => {}, ) const store = createStore() store.sub(derivedAtom, () => {}) const increment = (c: number) => c + 1 store.set(countAtom, increment) store.set(countAtom, increment) await vi.advanceTimersByTimeAsync(150) expect(result).toBe(2) store.set(countAtom, increment) store.set(countAtom, increment) await vi.advanceTimersByTimeAsync(150) expect(store.get(countAtom)).toBe(4) expect(result).toBe(4) }) it('should not recompute a derived atom value if unchanged (#2168)', () => { const store = createStore() const countAtom = atom(1) const derived1Atom = atom((get) => get(countAtom) * 0) const derive2Fn = vi.fn((get: Getter) => get(derived1Atom)) const derived2Atom = atom(derive2Fn) expect(store.get(derived2Atom)).toBe(0) store.set(countAtom, (c) => c + 1) expect(store.get(derived2Atom)).toBe(0) expect(derive2Fn).toHaveBeenCalledTimes(1) }) it('should mount once with atom creator atom (#2314)', () => { const countAtom = atom(1) countAtom.onMount = vi.fn((setAtom: (v: number) => void) => { setAtom(2) }) const atomCreatorAtom = atom((get) => { const derivedAtom = atom((get) => get(countAtom)) get(derivedAtom) }) const store = createStore() store.sub(atomCreatorAtom, () => {}) expect(countAtom.onMount).toHaveBeenCalledTimes(1) }) it('should flush pending write triggered asynchronously and indirectly (#2451)', async () => { const store = createStore() const anAtom = atom('initial') const callbackFn = vi.fn((_value: string) => {}) const unsub = store.sub(anAtom, () => { callbackFn(store.get(anAtom)) }) const actionAtom = atom(null, async (_get, set) => { await Promise.resolve() // waiting a microtask set(indirectSetAtom) }) const indirectSetAtom = atom(null, (_get, set) => { set(anAtom, 'next') }) // executing the chain reaction await store.set(actionAtom) expect(callbackFn).toHaveBeenCalledExactlyOnceWith('next') unsub() }) describe('async atom with subtle timing', () => { it('case 1', async () => { const store = createStore() const a = atom(1) const b = atom(async (get) => { await sleep(100) return get(a) }) const bValue = store.get(b) store.set(a, 2) const bValue2 = store.get(b) await vi.advanceTimersByTimeAsync(100) expect(await bValue).toBe(2) expect(await bValue2).toBe(2) }) it('case 2', async () => { const store = createStore() const a = atom(1) const b = atom(async (get) => { const aValue = get(a) await sleep(100) return aValue }) const bValue = store.get(b) store.set(a, 2) const bValue2 = store.get(b) await vi.advanceTimersByTimeAsync(100) expect(await bValue).toBe(1) // returns old value expect(await bValue2).toBe(2) }) }) describe('aborting atoms', () => { // We can't use signal.throwIfAborted as it is not available // in earlier versions of TS that this is tested on. const throwIfAborted = (signal: AbortSignal) => { if (signal.aborted) { throw new Error('aborted') } } it('should abort the signal when dependencies change', async () => { const a = atom(1) const callBeforeAbort = vi.fn() const callAfterAbort = vi.fn() const store = createStore() const derivedAtom = atom(async (get, { signal }) => { const aVal = get(a) await sleep(100) callBeforeAbort() throwIfAborted(signal) callAfterAbort() return aVal + 1 }) const promise = store.get(derivedAtom) store.set(a, 3) const promise2 = store.get(derivedAtom) await vi.advanceTimersByTimeAsync(100) await expect(promise).rejects.toThrow('aborted') await expect(promise2).resolves.toEqual(4) expect(callBeforeAbort).toHaveBeenCalledTimes(2) expect(callAfterAbort).toHaveBeenCalledTimes(1) }) it('should abort the signal when dependencies change and the atom is mounted', async () => { const a = atom(1) const callBeforeAbort = vi.fn() const callAfterAbort = vi.fn() const store = createStore() const derivedAtom = atom(async (get, { signal }) => { const aVal = get(a) await sleep(100) callBeforeAbort() throwIfAborted(signal) callAfterAbort() return aVal + 1 }) store.sub(derivedAtom, () => {}) store.set(a, 3) await vi.advanceTimersByTimeAsync(100) expect(callBeforeAbort).toHaveBeenCalledTimes(2) expect(callAfterAbort).toHaveBeenCalledTimes(1) }) it('should not abort the signal when unsubscribed', async () => { const a = atom(1) const callBeforeAbort = vi.fn() const callAfterAbort = vi.fn() const store = createStore() const derivedAtom = atom(async (get, { signal }) => { const aVal = get(a) await sleep(100) callBeforeAbort() throwIfAborted(signal) callAfterAbort() return aVal + 1 }) const unsub = store.sub(derivedAtom, () => {}) unsub() await vi.advanceTimersByTimeAsync(100) expect(await store.get(derivedAtom)).toEqual(2) expect(callBeforeAbort).toHaveBeenCalledTimes(1) expect(callAfterAbort).toHaveBeenCalledTimes(1) }) }) it('Unmount an atom that is no longer dependent within a derived atom (#2658)', () => { const condAtom = atom(true) const baseAtom = atom(0) const onUnmount = vi.fn() baseAtom.onMount = () => onUnmount const derivedAtom = atom((get) => { if (get(condAtom)) get(baseAtom) }) const store = createStore() store.sub(derivedAtom, () => {}) store.set(condAtom, false) expect(onUnmount).toHaveBeenCalledTimes(1) }) it('should update derived atom even if dependencies changed (#2697)', () => { const primitiveAtom = atom(undefined) const derivedAtom = atom((get) => get(primitiveAtom)) const conditionalAtom = atom((get) => { const base = get(primitiveAtom) if (!base) return return get(derivedAtom) }) const store = createStore() const onChangeDerived = vi.fn() store.sub(derivedAtom, onChangeDerived) store.sub(conditionalAtom, () => {}) expect(onChangeDerived).toHaveBeenCalledTimes(0) store.set(primitiveAtom, 1) expect(onChangeDerived).toHaveBeenCalledTimes(1) }) describe('should invoke flushPending only after all atoms are updated (#2804)', () => { const store = createStore() it('should invoke flushPending only after all atoms are updated with set', () => { const a = atom(0) const setResult = [] const w = atom(null, (_get, set, value: number) => { setResult.push('before set') set(a, value) setResult.push('after set') }) store.sub(a, () => { setResult.push('a value changed - ' + store.get(a)) }) setResult.push('before store.set') store.set(w, 1) setResult.push('after store.set') expect(setResult).not.toEqual([ 'before store.set', 'before set', 'a value changed - 1', 'after set', 'after store.set', ]) expect(setResult).toEqual([ 'before store.set', 'before set', 'after set', 'a value changed - 1', 'after store.set', ]) }) it('should invoke flushPending only after all atoms are updated with mount', () => { const mountResult = [] const a = atom(0) const m = atom(null, (_get, set, value: number) => { set(a, value) }) m.onMount = (setSelf) => { mountResult.push('before onMount setSelf') setSelf(1) mountResult.push('after onMount setSelf') } mountResult.push('before store.sub') store.sub(a, () => { mountResult.push('a value changed - ' + store.get(a)) }) store.sub(m, () => {}) mountResult.push('after store.sub') expect(mountResult).not.toEqual([ 'before store.sub', 'before onMount setSelf', 'a value changed - 1', 'after onMount setSelf', 'after store.sub', ]) expect(mountResult).toEqual([ 'before store.sub', 'before onMount setSelf', 'after onMount setSelf', 'a value changed - 1', 'after store.sub', ]) }) it('should flush only after all atoms are updated with unmount', () => { const result: string[] = [] const a = atom(0) const b = atom(null, (_get, set, value: number) => { set(a, value) }) b.onMount = (setAtom) => { return () => { result.push('onUmount: before setAtom') setAtom(1) result.push('onUmount: after setAtom') } } const c = atom(true) const d = atom((get) => get(c) && get(b)) store.sub(a, () => { result.push('a value changed - ' + store.get(a)) }) store.sub(d, () => {}) expect(store.get(d)).toEqual(null) store.set(c, false) expect(result).toEqual([ 'onUmount: before setAtom', 'onUmount: after setAtom', 'a value changed - 1', ]) }) }) describe('should mount and trigger listeners even when an error is thrown', () => { it('in asynchronous read', async () => { const store = createStore() const a = atom(0) a.onMount = vi.fn() const e = atom( () => { throw new Error('error') }, () => {}, ) e.onMount = vi.fn() const b = atom((get) => { setTimeout(() => { get(a) try { get(e) } catch { // expect error } }) }) store.sub(b, () => {}) await vi.advanceTimersByTimeAsync(0) expect(a.onMount).toHaveBeenCalledOnce() expect(e.onMount).toHaveBeenCalledOnce() }) it('in read setSelf', async () => { const store = createStore() const a = atom(0) const e = atom( () => { throw new Error('error') }, () => {}, ) const b = atom( (_, { setSelf }) => { setTimeout(() => { try { setSelf() } catch { // expect error } }) }, (get, set) => { set(a, 1) get(e) }, ) const listener = vi.fn() store.sub(a, listener) store.sub(b, () => {}) await vi.advanceTimersByTimeAsync(0) expect(listener).toHaveBeenCalledOnce() }) it('in read promise on settled', async () => { const store = createStore() const a = atom(0) a.onMount = vi.fn() const e = atom( () => { throw new Error('error') }, () => {}, ) const b = atom(async (get) => { await sleep(0) get(a) get(e) }) store.sub(b, () => {}) await vi.advanceTimersByTimeAsync(0) expect(a.onMount).toHaveBeenCalledOnce() }) it('in asynchronous write', async () => { const store = createStore() const a = atom(0) const e = atom(() => { throw new Error('error') }) const b = atom(null, (get, set) => { set(a, 1) get(e) }) const w = atom(null, async (_get, set) => { setTimeout(() => { try { set(b) } catch { // expect error } }) }) const listener = vi.fn() store.sub(a, listener) store.set(w) await vi.advanceTimersByTimeAsync(0) expect(listener).toHaveBeenCalledOnce() }) it('in synchronous write', () => { const store = createStore() const a = atom(0) const e = atom(() => { throw new Error('error') }) const b = atom(null, (get, set) => { set(a, 1) get(e) }) const listener = vi.fn() store.sub(a, listener) try { store.set(b) } catch { // expect error } expect(listener).toHaveBeenCalledOnce() }) it('in onmount/onunmount asynchronous setAtom', async () => { const store = createStore() const a = atom(0) const e = atom(() => { throw new Error('error') }) const b = atom(null, (get, set) => { set(a, (v) => ++v) get(e) }) b.onMount = (setAtom) => { setTimeout(() => { try { setAtom() } catch { // expect error } }) return () => { setTimeout(() => { try { setAtom() } catch { // expect error } }) } } const listener = vi.fn() store.sub(a, listener) const unsub = store.sub(b, () => {}) await vi.advanceTimersByTimeAsync(0) expect(listener).toHaveBeenCalledOnce() listener.mockClear() unsub() await vi.advanceTimersByTimeAsync(0) expect(listener).toHaveBeenCalledOnce() }) it('in synchronous onmount', () => { const store = createStore() const a = atom(0) const aUnmount = vi.fn() a.onMount = vi.fn(() => aUnmount) const b = atom( (get) => get(a), () => {}, ) b.onMount = () => { throw new Error('error') } try { store.sub(b, () => {}) } catch { // expect error } expect(a.onMount).toHaveBeenCalledOnce() }) it('in synchronous onunmount', () => { const store = createStore() const a = atom(0) const aUnmount = vi.fn() a.onMount = () => aUnmount const b = atom( (get) => get(a), () => {}, ) b.onMount = () => () => { throw new Error('error') } const unsub = store.sub(b, () => {}) try { unsub() } catch { // expect error } expect(aUnmount).toHaveBeenCalledOnce() }) it('in synchronous listener', () => { const store = createStore() const a = atom(0) const e = atom(0) const b = atom(null, (_, set) => { set(a, 1) set(e, 1) }) store.sub(e, () => { throw new Error('error') }) const listener = vi.fn() store.sub(a, listener) try { store.set(b) } catch { // expect error } expect(listener).toHaveBeenCalledOnce() }) }) it('throws falsy errors in onMount, onUnmount, and listeners', () => { const store = createStore() const a = atom(0) a.onMount = () => { throw '' } expect(() => store.sub(a, () => {})).toThrow('') const b = atom(0) b.onMount = () => () => { throw '' } const unsub = store.sub(b, () => {}) expect(() => unsub()).toThrow('') const c = atom(0) store.sub(c, () => { throw '' }) expect(() => store.set(c, 1)).toThrow('') }) it('should use the correct pending on unmount', () => { const store = createStore() const a = atom(0) const b = atom(0, (_, set, update: number) => set(a, update)) b.onMount = (setAtom) => () => setAtom(1) const aListener = vi.fn() store.sub(a, aListener) const unsub = store.sub(b, () => {}) aListener.mockClear() unsub() expect(store.get(a)).toBe(1) expect(aListener).toHaveBeenCalledTimes(1) }) it('should call subscribers after setAtom updates atom value on mount but not on unmount', () => { const store = createStore() const a = atom(0) let unmount a.onMount = vi.fn(((setAtom) => { setAtom(1) unmount = vi.fn(() => { setAtom(2) }) return unmount }) as NonNullable<(typeof a)['onMount']>) const listener = vi.fn() const unsub = store.sub(a, listener) expect(store.get(a)).toBe(1) expect(a.onMount).toHaveBeenCalledTimes(1) expect(listener).toHaveBeenCalledTimes(1) listener.mockClear() unsub() expect(store.get(a)).toBe(2) expect(unmount).toHaveBeenCalledTimes(1) expect(listener).toHaveBeenCalledTimes(0) }) it('processes deep atom a graph beyond maxDepth', () => { function getMaxDepth() { let depth = 0 function d(): number { ++depth try { return d() } catch { return depth } } return d() } const maxDepth = getMaxDepth() const store = createStore() const baseAtom = atom(0) const atoms: [PrimitiveAtom, ...Atom[]] = [baseAtom] Array.from({ length: maxDepth }, (_, i) => { const prevAtom = atoms[i]! const a = atom((get) => get(prevAtom)) atoms.push(a) }) const lastAtom = atoms[maxDepth]! // store.get(lastAtom) // FIXME: This is causing a stack overflow expect(() => store.sub(lastAtom, () => {})).not.toThrow() // store.get(lastAtom) // FIXME: This is causing a stack overflow expect(() => store.set(baseAtom, 1)).not.toThrow() // store.set(lastAtom) // FIXME: This is causing a stack overflow }, 10_000) it('mounted atom should be recomputed eagerly', () => { const result: string[] = [] const a = atom(0) const b = atom((get) => { result.push('bRead') return get(a) }) const store = createStore() store.sub(a, () => { result.push('aCallback') }) store.sub(b, () => { result.push('bCallback') }) expect(result).toEqual(['bRead']) result.splice(0) store.set(a, 1) expect(result).toEqual(['bRead', 'aCallback', 'bCallback']) }) it('should notify subscription even with reading atom in write', () => { const a = atom(1) const b = atom((get) => get(a) * 2) const c = atom((get) => get(b) + 1) const d = atom(null, (get, set) => { set(a, 2) get(b) }) const store = createStore() const callback = vi.fn() store.sub(c, callback) store.set(d) expect(callback).toHaveBeenCalledTimes(1) }) it('should process all atom listeners even if some of them throw errors', () => { const store = createStore() const a = atom(0) const listenerA = vi.fn() const listenerB = vi.fn(() => { throw new Error('error') }) const listenerC = vi.fn() store.sub(a, listenerA) store.sub(a, listenerB) store.sub(a, listenerC) try { store.set(a, 1) } catch { // expect empty } expect(listenerA).toHaveBeenCalledTimes(1) expect(listenerB).toHaveBeenCalledTimes(1) expect(listenerC).toHaveBeenCalledTimes(1) }) it('should call onInit only once per atom', () => { const store = createStore() const a = atom(0) const onInit = vi.fn() a.INTERNAL_onInit = onInit store.get(a) expect(onInit).toHaveBeenCalledTimes(1) expect(onInit).toHaveBeenCalledWith(store) onInit.mockClear() store.get(a) store.set(a, 1) const unsub = store.sub(a, () => {}) unsub() const b = atom((get) => get(a)) store.get(b) store.sub(b, () => {}) expect(onInit).not.toHaveBeenCalled() }) it('should call onInit only once per store', () => { const a = atom(0) const aOnInit = vi.fn() a.INTERNAL_onInit = aOnInit const b = atom(0) const bOnInit = vi.fn() b.INTERNAL_onInit = bOnInit type Store = ReturnType function testInStore(store: Store) { store.get(a) store.get(b) expect(aOnInit).toHaveBeenCalledTimes(1) expect(bOnInit).toHaveBeenCalledTimes(1) aOnInit.mockClear() bOnInit.mockClear() return store } testInStore(createStore()) const store = testInStore(createStore()) testInStore( deriveStore(store, (atomStateMap) => { const initializedAtoms = new WeakSet() return { get: (atom) => { if (!initializedAtoms.has(atom)) { return undefined } return atomStateMap.get(atom) }, set: (atom, atomState) => { initializedAtoms.add(atom) atomStateMap.set(atom, atomState) }, has: (atom) => { if (!initializedAtoms.has(atom)) { return false } return atomStateMap.has(atom) }, delete: (atom) => { initializedAtoms.delete(atom) return atomStateMap.delete(atom) }, } }) as Store, ) }) it('should pass store and atomState to the atom initializer', () => { expect.assertions(1) const store = createStore() const a = atom(null) a.INTERNAL_onInit = (store) => { expect(store).toBe(store) } store.get(a) }) it('recomputes dependents of unmounted atoms', () => { const a = atom(0) const bRead = vi.fn((get: Getter) => { return get(a) }) const b = atom(bRead) const c = atom((get) => get(b)) const w = atom(null, (get, set) => { set(a, 1) get(c) set(a, 2) bRead.mockClear() }) const store = createStore() store.set(w) expect(bRead).not.toHaveBeenCalled() }) it('recomputes all changed atom dependents together', () => { const a = atom([0]) const b = atom([0]) const a0 = atom((get) => get(a)[0]!) const b0 = atom((get) => get(b)[0]!) const a0b0 = atom((get) => [get(a0), get(b0)]) const w = atom(null, (_, set) => { set(a, [0]) set(b, [1]) }) const store = createStore() store.sub(a0b0, () => {}) store.set(w) expect(store.get(a0)).toBe(0) expect(store.get(b0)).toBe(1) expect(store.get(a0b0)).toEqual([0, 1]) }) it('should not inf on subscribe or unsubscribe', () => { const store = createStore() const countAtom = atom(0) const effectAtom = atom( (get) => get(countAtom), (_, set) => set, ) effectAtom.onMount = (setAtom) => { const set = setAtom() set(countAtom, 1) return () => { set(countAtom, 2) } } const unsub = store.sub(effectAtom, () => {}) expect(store.get(countAtom)).toBe(1) unsub() expect(store.get(countAtom)).toBe(2) }) it('supports recursion in an atom subscriber', () => { const a = atom(0) const store = createStore() store.sub(a, () => { if (store.get(a) < 3) { store.set(a, (v) => v + 1) } }) store.set(a, 1) expect(store.get(a)).toBe(3) }) it('allows subscribing to atoms during mount', () => { const store = createStore() const a = atom(0) a.onMount = () => { store.sub(b, () => {}) } const b = atom(0) let bMounted = false b.onMount = () => { bMounted = true } store.sub(a, () => {}) expect(bMounted).toBe(true) }) it('updates with reading derived atoms (#2959)', () => { const store = createStore() const countAtom = atom(0) const countDerivedAtom = atom((get) => get(countAtom)) const countUpAtom = atom(null, (get, set) => { set(countAtom, 1) get(countDerivedAtom) set(countAtom, 2) }) store.sub(countDerivedAtom, () => {}) store.set(countUpAtom) expect(store.get(countDerivedAtom)).toBe(2) }) it('updates dependents when it eagerly recomputes dirty atoms', () => { const countAtom = atom(0) const isActiveAtom = atom(false) const activeCountAtom = atom((get) => get(isActiveAtom) ? get(countAtom) : undefined, ) const activateAction = atom(null, (get, set, value: boolean) => { set(isActiveAtom, value) get(activeCountAtom) }) const store = createStore() store.sub(activeCountAtom, () => {}) store.set(activateAction, true) store.set(countAtom, 1) expect(store.get(activeCountAtom)).toBe(1) }) it('[DEV-ONLY] should warn store mutation during read', () => { const store = createStore() const countAtom = atom(0) const derivedAtom = atom(() => { store.set(countAtom, (c) => c + 1) }) store.get(derivedAtom) expect(console.warn).toHaveBeenCalledWith( 'Detected store mutation during atom read. This is not supported.', ) }) it('should keep reactivity when a derived atom returns a function that calls get (#3240)', () => { const store = createStore() const stableAtom = atom(0) const closureAtom = atom((get) => (x: number) => { const s = get(stableAtom) return x + s }) const changingAtom = atom(0) const upstreamAtom = atom((get) => { const n = get(changingAtom) const fn = get(closureAtom) return fn(n) }) const downstreamAtom = atom((get) => get(upstreamAtom) * 2) const callback = vi.fn() store.sub(downstreamAtom, callback) expect(store.get(upstreamAtom)).toBe(0) expect(store.get(downstreamAtom)).toBe(0) store.set(changingAtom, 1) expect(store.get(upstreamAtom)).toBe(1) expect(store.get(downstreamAtom)).toBe(2) expect(callback).toHaveBeenCalledTimes(1) store.set(changingAtom, 2) expect(store.get(upstreamAtom)).toBe(2) expect(store.get(downstreamAtom)).toBe(4) expect(callback).toHaveBeenCalledTimes(2) }) // Regression (v2.12.1+, commit f5d843c): when a derived atom's read calls store.set // (e.g. via a write-only atom), that atom's subscribers are not notified on dependency // changes — store.get(atom) is correct but the subscription callback never runs. // Repro: counterAtom, write-only queryAtom, dataAtom = get => query(get(counterAtom)). // https://github.com/koutaro-masaki/jotai-atom-in-component-with-usesetatom it('notifies derived-atom subscriber when read calls store.set', () => { const store = createStore() const counterAtom = atom(0) const queryAtom = atom(null, (_get, _set, v: number) => v) const dataAtom = atom((get) => { const v = get(counterAtom) const result = store.set(queryAtom, v * 2) return result }) const dataListener = vi.fn() store.sub(dataAtom, dataListener) expect(store.get(dataAtom)).toBe(0) expect(dataListener.mock.calls.length).toBe(0) store.set(counterAtom, 1) expect(store.get(dataAtom)).toBe(2) expect(dataListener.mock.calls.length).toBe(1) store.set(counterAtom, 2) expect(store.get(dataAtom)).toBe(4) expect(dataListener.mock.calls.length).toBe(2) }) it('notifies subscriber through chain of derived atoms when root calls store.set', () => { const store = createStore() const counterAtom = atom(0) const queryAtom = atom(null, (_get, _set, v: number) => v) const baseAtom = atom((get) => { const v = get(counterAtom) return store.set(queryAtom, v * 2) }) const derivedAtom = atom((get) => get(baseAtom) * 2) const derivedListener = vi.fn() store.sub(derivedAtom, derivedListener) expect(store.get(derivedAtom)).toBe(0) expect(derivedListener.mock.calls.length).toBe(0) store.set(counterAtom, 1) expect(store.get(derivedAtom)).toBe(4) expect(derivedListener.mock.calls.length).toBe(1) }) it('notifies subscriber when nested write uses get to read atom with store.set', () => { const store = createStore() const counterAtom = atom(0) const innerQueryAtom = atom(null, (_get, _set, v: number) => v) const middleAtom = atom((get) => { const v = get(counterAtom) return store.set(innerQueryAtom, v * 3) }) const outerQueryAtom = atom(null, (get, _set, v: number) => { const m = get(middleAtom) return v + m }) const dataAtom = atom((get) => { const v = get(counterAtom) return store.set(outerQueryAtom, v * 2) }) const dataListener = vi.fn() store.sub(dataAtom, dataListener) expect(store.get(dataAtom)).toBe(0) expect(dataListener.mock.calls.length).toBe(0) store.set(counterAtom, 1) expect(store.get(dataAtom)).toBe(5) expect(dataListener.mock.calls.length).toBe(1) store.set(counterAtom, 2) expect(store.get(dataAtom)).toBe(10) expect(dataListener.mock.calls.length).toBe(2) }) it('notifies async derived-atom subscriber when read calls store.set before await', async () => { const store = createStore() const counterAtom = atom(0) const queryAtom = atom(null, (_get, _set, v: number) => v) const dataAtom = atom(async (get) => { const v = get(counterAtom) const result = store.set(queryAtom, v * 2) await sleep(0) return result }) let lastValue: number | Promise | undefined store.sub(dataAtom, () => { lastValue = store.get(dataAtom) }) await vi.advanceTimersByTimeAsync(10) expect(await store.get(dataAtom)).toBe(0) store.set(counterAtom, 1) await vi.advanceTimersByTimeAsync(10) expect(await lastValue).toBe(2) store.set(counterAtom, 2) await vi.advanceTimersByTimeAsync(10) expect(await lastValue).toBe(4) }) it('notifies subscriber normally when store.set is in write function, not read', () => { const store = createStore() const counterAtom = atom(0) const innerQueryAtom = atom(null, (_get, _set, v: number) => v) const queryAtom = atom(null, (_get, _set, v: number) => store.set(innerQueryAtom, v), ) const dataAtom = atom((get) => { const v = get(counterAtom) const result = store.set(queryAtom, v * 2) return result }) const dataListener = vi.fn() store.sub(dataAtom, dataListener) expect(store.get(dataAtom)).toBe(0) store.set(counterAtom, 1) expect(store.get(dataAtom)).toBe(2) expect(dataListener.mock.calls.length).toBe(1) store.set(counterAtom, 2) expect(store.get(dataAtom)).toBe(4) expect(dataListener.mock.calls.length).toBe(2) }) it('store.set before get(dep) causes deep recursion but recovers', () => { const store = createStore() const counterAtom = atom(0) const queryAtom = atom(null, (_get, _set, v: number) => v) const dataAtom = atom((get) => { const result = store.set(queryAtom, 1) const v = get(counterAtom) return result + v }) const dataListener = vi.fn() store.sub(dataAtom, dataListener) expect(store.get(dataAtom)).toBe(1) store.set(counterAtom, 1) expect(store.get(dataAtom)).toBe(2) expect(dataListener.mock.calls.length).toBeGreaterThanOrEqual(1) }) it('notifies subscriber when read calls store.set multiple times', () => { const store = createStore() const counterAtom = atom(0) const query1 = atom(null, (_get, _set, v: number) => v) const query2 = atom(null, (_get, _set, v: number) => v * 10) const dataAtom = atom((get) => { const v = get(counterAtom) const r1 = store.set(query1, v) const r2 = store.set(query2, v) return r1 + r2 }) const dataListener = vi.fn() store.sub(dataAtom, dataListener) expect(store.get(dataAtom)).toBe(0) expect(dataListener.mock.calls.length).toBe(0) store.set(counterAtom, 1) expect(store.get(dataAtom)).toBe(11) expect(dataListener.mock.calls.length).toBe(1) store.set(counterAtom, 2) expect(store.get(dataAtom)).toBe(22) expect(dataListener.mock.calls.length).toBe(2) }) it('does not recompute derived atom redundantly when store.set in read uses return value without setting atoms', () => { const store = createStore() const counterAtom = atom(0) const queryAtom = atom(null, (_get, _set, v: number) => v) let baseReadCount = 0 const baseAtom = atom((get) => { baseReadCount++ const v = get(counterAtom) return store.set(queryAtom, v * 2) }) const baseAtom2 = atom((get) => { const v = get(counterAtom) return store.set(queryAtom, v * 2) }) let derivedReadCount = 0 const derivedAtom = atom((get) => { derivedReadCount++ get(baseAtom) get(baseAtom2) return get(counterAtom) }) const listener = vi.fn() store.sub(derivedAtom, listener) expect(store.get(derivedAtom)).toBe(0) expect(baseReadCount).toBe(1) expect(derivedReadCount).toBe(1) baseReadCount = 0 derivedReadCount = 0 store.set(counterAtom, 1) expect(store.get(derivedAtom)).toBe(1) expect(listener).toHaveBeenCalledTimes(1) expect(baseReadCount).toBe(1) expect(derivedReadCount).not.toBe(3) expect(derivedReadCount).toBe(1) }) ================================================ FILE: tests/vanilla/storedev.test.tsx ================================================ import { describe, expect, it, vi } from 'vitest' import { atom } from 'jotai/vanilla' import type { Atom, WritableAtom } from 'jotai/vanilla' import { INTERNAL_buildStoreRev2 as INTERNAL_buildStore, INTERNAL_initializeStoreHooksRev2 as INTERNAL_initializeStoreHooks, } from 'jotai/vanilla/internals' import type { INTERNAL_AtomState, INTERNAL_Store, } from 'jotai/vanilla/internals' type DevStore = { get_internal_weak_map: () => { get: (atom: Atom) => INTERNAL_AtomState | undefined } get_mounted_atoms: () => Set> restore_atoms: (values: Iterable, unknown]>) => void } const createDevStore = (): INTERNAL_Store & DevStore => { let inRestoreAtom = 0 const storeHooks = INTERNAL_initializeStoreHooks({}) const atomStateMap = new WeakMap() const mountedAtoms = new WeakMap() const store = INTERNAL_buildStore( atomStateMap, mountedAtoms, undefined, undefined, undefined, undefined, storeHooks, undefined, (_store, atom, get, set, ...args) => { if (inRestoreAtom) { return set(atom, ...(args as any)) } return atom.write(get, set, ...(args as any)) }, ) const debugMountedAtoms = new Set>() storeHooks.m.add(undefined, (atom) => { debugMountedAtoms.add(atom) const atomState = atomStateMap.get(atom) // For DevStoreRev4 compatibility ;(atomState as any).m = mountedAtoms.get(atom) }) storeHooks.u.add(undefined, (atom) => { debugMountedAtoms.delete(atom) const atomState = atomStateMap.get(atom) // For DevStoreRev4 compatibility delete (atomState as any).m }) const devStore: DevStore = { // store dev methods (these are tentative and subject to change without notice) get_internal_weak_map: () => atomStateMap, get_mounted_atoms: () => debugMountedAtoms, restore_atoms: (values) => { const restoreAtom: WritableAtom = { read: () => null, write: (_get, set) => { ++inRestoreAtom try { for (const [atom, value] of values) { if ('init' in atom) { set(atom as never, value) } } } finally { --inRestoreAtom } }, } store.set(restoreAtom) }, } return Object.assign(store, devStore) } describe('dev-only methods', () => { it('should get atom value', () => { const store = createDevStore() const countAtom = atom(0) store.set(countAtom, 1) const weakMap = store.get_internal_weak_map() expect(weakMap.get(countAtom)?.v).toEqual(1) }) it('should restore atoms and its dependencies correctly', () => { const store = createDevStore() const countAtom = atom(0) const derivedAtom = atom((get) => get(countAtom) * 2) store.set(countAtom, 1) store.restore_atoms([[countAtom, 2]]) expect(store.get(countAtom)).toBe(2) expect(store.get?.(derivedAtom)).toBe(4) }) it('should restore atoms and call store listeners correctly', () => { const store = createDevStore() const countAtom = atom(0) const derivedAtom = atom((get) => get(countAtom) * 2) const countCb = vi.fn() const derivedCb = vi.fn() store.set(countAtom, 2) const unsubCount = store.sub(countAtom, countCb) const unsubDerived = store.sub(derivedAtom, derivedCb) store.restore_atoms([ [countAtom, 1], [derivedAtom, 2], ]) expect(countCb).toHaveBeenCalled() expect(derivedCb).toHaveBeenCalled() unsubCount() unsubDerived() }) it('should return all the mounted atoms correctly', () => { const store = createDevStore() const countAtom = atom(0) const derivedAtom = atom((get) => get(countAtom) * 2) const unsub = store.sub(derivedAtom, vi.fn()) store.set(countAtom, 1) const result = store.get_mounted_atoms() expect( Array.from(result) .sort((a, b) => Object.keys(a).length - Object.keys(b).length) .map((item) => { const { debugLabel: _, ...rest } = item return rest }), ).toStrictEqual([ { toString: expect.any(Function), read: expect.any(Function) }, { toString: expect.any(Function), init: 0, read: expect.any(Function), write: expect.any(Function), }, ]) unsub() }) it("should return all the mounted atoms correctly after they're unsubscribed", () => { const store = createDevStore() const countAtom = atom(0) const derivedAtom = atom((get) => get(countAtom) * 2) const unsub = store.sub(derivedAtom, vi.fn()) store.set(countAtom, 1) unsub() const result = store.get_mounted_atoms() expect(Array.from(result)).toStrictEqual([]) }) it('should restore atoms with custom write function', () => { const store = createDevStore() const countAtom = atom(0, () => {}) store.restore_atoms([[countAtom, 1]]) expect(store.get(countAtom)).toBe(1) }) }) ================================================ FILE: tests/vanilla/types.test.tsx ================================================ import { expectTypeOf, it } from 'vitest' import { atom } from 'jotai/vanilla' import type { Atom, ExtractAtomArgs, ExtractAtomResult, ExtractAtomValue, PrimitiveAtom, WritableAtom, } from 'jotai/vanilla' it('atom() should return the correct types', () => { // primitive atom const primitiveAtom = atom(0) // NOTE: expectTypeOf is not available in TypeScript 4.0.5 and below, toExtend is not available in TypeScript 4.6.4 and below // [ONLY-TS-4.6.4] [ONLY-TS-4.5.5] [ONLY-TS-4.4.4] [ONLY-TS-4.3.5] [ONLY-TS-4.2.3] [ONLY-TS-4.1.5] [ONLY-TS-4.0.5] [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore expectTypeOf(primitiveAtom).toExtend>() // [ONLY-TS-4.6.4] [ONLY-TS-4.5.5] [ONLY-TS-4.4.4] [ONLY-TS-4.3.5] [ONLY-TS-4.2.3] [ONLY-TS-4.1.5] [ONLY-TS-4.0.5] [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore expectTypeOf(primitiveAtom).not.toExtend>() // primitive atom without initial value const primitiveWithoutInitialAtom = atom() // [ONLY-TS-4.6.4] [ONLY-TS-4.5.5] [ONLY-TS-4.4.4] [ONLY-TS-4.3.5] [ONLY-TS-4.2.3] [ONLY-TS-4.1.5] [ONLY-TS-4.0.5] [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore expectTypeOf(primitiveWithoutInitialAtom).toExtend< PrimitiveAtom >() const undefinedAtom = atom() // [ONLY-TS-4.6.4] [ONLY-TS-4.5.5] [ONLY-TS-4.4.4] [ONLY-TS-4.3.5] [ONLY-TS-4.2.3] [ONLY-TS-4.1.5] [ONLY-TS-4.0.5] [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore expectTypeOf(undefinedAtom).toExtend>() // read-only derived atom const readonlyDerivedAtom = atom((get) => get(primitiveAtom) * 2) // [ONLY-TS-4.6.4] [ONLY-TS-4.5.5] [ONLY-TS-4.4.4] [ONLY-TS-4.3.5] [ONLY-TS-4.2.3] [ONLY-TS-4.1.5] [ONLY-TS-4.0.5] [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore expectTypeOf(readonlyDerivedAtom).toExtend>() // read-write derived atom const readWriteDerivedAtom = atom( (get) => get(primitiveAtom), (get, set, value: number) => { set(primitiveAtom, get(primitiveAtom) + value) }, ) // [ONLY-TS-4.6.4] [ONLY-TS-4.5.5] [ONLY-TS-4.4.4] [ONLY-TS-4.3.5] [ONLY-TS-4.2.3] [ONLY-TS-4.1.5] [ONLY-TS-4.0.5] [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore expectTypeOf(readWriteDerivedAtom).toExtend< WritableAtom >() // write-only derived atom const writeonlyDerivedAtom = atom(null, (get, set) => { set(primitiveAtom, get(primitiveAtom) - 1) }) // [ONLY-TS-4.6.4] [ONLY-TS-4.5.5] [ONLY-TS-4.4.4] [ONLY-TS-4.3.5] [ONLY-TS-4.2.3] [ONLY-TS-4.1.5] [ONLY-TS-4.0.5] [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore expectTypeOf(writeonlyDerivedAtom).toExtend>() }) it('type utils should work', () => { const readWriteAtom = atom( (_get) => 1 as number, async (_get, _set, _value: string) => {}, ) void readWriteAtom type ReadWriteAtom = typeof readWriteAtom // [ONLY-TS-4.0.5] [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore expectTypeOf>().toEqualTypeOf() // [ONLY-TS-4.0.5] [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore expectTypeOf>().toEqualTypeOf<[string]>() // [ONLY-TS-4.0.5] [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore expectTypeOf>().toEqualTypeOf< Promise >() }) it('WritableAtom Result type should be covariant', () => { const writableAtom = atom(null, () => null) // NOTE: expectTypeOf is not available in TypeScript 4.0.5 and below, toExtend is not available in TypeScript 4.6.4 and below // [ONLY-TS-4.6.4] [ONLY-TS-4.5.5] [ONLY-TS-4.4.4] [ONLY-TS-4.3.5] [ONLY-TS-4.2.3] [ONLY-TS-4.1.5] [ONLY-TS-4.0.5] [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore expectTypeOf(writableAtom).toExtend>() }) ================================================ FILE: tests/vanilla/utils/atomFamily.test.ts ================================================ import { expect, it, vi } from 'vitest' import { atom, createStore } from 'jotai/vanilla' import type { Atom } from 'jotai/vanilla' import { atomFamily } from 'jotai/vanilla/utils' it('should create atoms with different params', () => { const store = createStore() const aFamily = atomFamily((param: number) => atom(param)) expect(store.get(aFamily(1))).toEqual(1) expect(store.get(aFamily(2))).toEqual(2) }) it('should remove atoms', () => { const store = createStore() const initializeAtom = vi.fn((param: number) => atom(param)) const aFamily = atomFamily(initializeAtom) expect(store.get(aFamily(1))).toEqual(1) expect(store.get(aFamily(2))).toEqual(2) aFamily.remove(2) initializeAtom.mockClear() expect(store.get(aFamily(1))).toEqual(1) expect(initializeAtom).toHaveBeenCalledTimes(0) expect(store.get(aFamily(2))).toEqual(2) expect(initializeAtom).toHaveBeenCalledTimes(1) }) it('should remove atoms with custom comparator', () => { const store = createStore() const initializeAtom = vi.fn((param: number) => atom(param)) const aFamily = atomFamily(initializeAtom, (a, b) => a === b) expect(store.get(aFamily(1))).toEqual(1) expect(store.get(aFamily(2))).toEqual(2) expect(store.get(aFamily(3))).toEqual(3) aFamily.remove(2) initializeAtom.mockClear() expect(store.get(aFamily(1))).toEqual(1) expect(initializeAtom).toHaveBeenCalledTimes(0) expect(store.get(aFamily(2))).toEqual(2) expect(initializeAtom).toHaveBeenCalledTimes(1) }) it('should remove atoms with custom shouldRemove', () => { const store = createStore() const initializeAtom = vi.fn((param: number) => atom(param)) const aFamily = atomFamily>(initializeAtom) expect(store.get(aFamily(1))).toEqual(1) expect(store.get(aFamily(2))).toEqual(2) expect(store.get(aFamily(3))).toEqual(3) aFamily.setShouldRemove((_createdAt, param) => param % 2 === 0) initializeAtom.mockClear() expect(store.get(aFamily(1))).toEqual(1) expect(initializeAtom).toHaveBeenCalledTimes(0) expect(store.get(aFamily(2))).toEqual(2) expect(initializeAtom).toHaveBeenCalledTimes(1) expect(store.get(aFamily(3))).toEqual(3) expect(initializeAtom).toHaveBeenCalledTimes(1) }) it('should notify listeners', () => { const aFamily = atomFamily((param: number) => atom(param)) const listener = vi.fn(() => {}) type Event = { type: 'CREATE' | 'REMOVE'; param: number; atom: Atom } const unsubscribe = aFamily.unstable_listen(listener) const atom1 = aFamily(1) expect(listener).toHaveBeenCalledTimes(1) const eventCreate = listener.mock.calls[0]?.at(0) as unknown as Event if (!eventCreate) throw new Error('eventCreate is undefined') expect(eventCreate.type).toEqual('CREATE') expect(eventCreate.param).toEqual(1) expect(eventCreate.atom).toEqual(atom1) listener.mockClear() aFamily.remove(1) expect(listener).toHaveBeenCalledTimes(1) const eventRemove = listener.mock.calls[0]?.at(0) as unknown as Event expect(eventRemove.type).toEqual('REMOVE') expect(eventRemove.param).toEqual(1) expect(eventRemove.atom).toEqual(atom1) unsubscribe() listener.mockClear() aFamily(2) expect(listener).toHaveBeenCalledTimes(0) }) it('should return all params', () => { const store = createStore() const aFamily = atomFamily((param: number) => atom(param)) expect(store.get(aFamily(1))).toEqual(1) expect(store.get(aFamily(2))).toEqual(2) expect(store.get(aFamily(3))).toEqual(3) expect(Array.from(aFamily.getParams())).toEqual([1, 2, 3]) }) ================================================ FILE: tests/vanilla/utils/atomWithDefault.test.ts ================================================ import { beforeEach, describe, expect, it, vi } from 'vitest' import { atom, createStore } from 'jotai/vanilla' import { RESET, atomWithDefault } from 'jotai/vanilla/utils' describe('atomWithDefault', () => { beforeEach(() => { vi.clearAllMocks() }) it('should reset to initial value using RESET', () => { const initialValue = 10 const testAtom = atomWithDefault(() => initialValue) const store = createStore() store.set(testAtom, 123) store.set(testAtom, RESET) expect(store.get(testAtom)).toBe(initialValue) }) it('should reset to initial value derived from another atom', () => { const initialValueAtom = atom(10) const testAtom = atomWithDefault((get) => get(initialValueAtom)) const store = createStore() expect(store.get(testAtom)).toBe(10) store.set(testAtom, 123) store.set(initialValueAtom, 20) store.set(testAtom, RESET) expect(store.get(testAtom)).toBe(20) }) it(`should reflect changes to the initial value atom when main atom hasn't been manually changed`, () => { const initialValueAtom = atom(10) const testAtom = atomWithDefault((get) => get(initialValueAtom)) const store = createStore() store.set(initialValueAtom, 20) expect(store.get(testAtom)).toBe(20) }) it(`should reflect changes to the initial value atom when main atom has been manually changed but then RESET`, () => { const initialValueAtom = atom(10) const testAtom = atomWithDefault((get) => get(initialValueAtom)) const store = createStore() store.set(testAtom, 123) // if this RESET were storing 10 rather than EMPTY the next set wouldn't have an effect store.set(testAtom, RESET) store.set(initialValueAtom, 20) expect(store.get(testAtom)).toBe(20) }) it('should update atom with a new value', () => { const initialValue = 10 const testAtom = atomWithDefault(() => initialValue) const store = createStore() store.set(testAtom, 123) store.set(testAtom, 30) expect(store.get(testAtom)).toBe(30) }) it('should update atom using a function', () => { const initialValue = 10 const testAtom = atomWithDefault(() => initialValue) const store = createStore() store.set(testAtom, 123) store.set(testAtom, (prev: number) => prev + 10) expect(store.get(testAtom)).toBe(133) }) it('should reset with a function', () => { const initialValue = 10 const testAtom = atomWithDefault(() => initialValue) const store = createStore() store.set(testAtom, 123) store.set(testAtom, () => RESET) expect(store.get(testAtom)).toBe(initialValue) }) }) ================================================ FILE: tests/vanilla/utils/atomWithLazy.test.ts ================================================ import { expect, it, vi } from 'vitest' import { createStore } from 'jotai/vanilla' import { atomWithLazy } from 'jotai/vanilla/utils' it('initializes on first store get', () => { const storeA = createStore() const storeB = createStore() let externalState = 'first' const initializer = vi.fn(() => externalState) const anAtom = atomWithLazy(initializer) expect(initializer).not.toHaveBeenCalled() expect(storeA.get(anAtom)).toEqual('first') expect(initializer).toHaveBeenCalledOnce() externalState = 'second' expect(storeA.get(anAtom)).toEqual('first') expect(initializer).toHaveBeenCalledOnce() expect(storeB.get(anAtom)).toEqual('second') expect(initializer).toHaveBeenCalledTimes(2) }) it('is writable', () => { const store = createStore() const anAtom = atomWithLazy(() => 0) store.set(anAtom, 123) expect(store.get(anAtom)).toEqual(123) }) it('should work with a set state action', () => { const store = createStore() const anAtom = atomWithLazy(() => 4) store.set(anAtom, (prev: number) => prev * prev) expect(store.get(anAtom)).toEqual(16) }) ================================================ FILE: tests/vanilla/utils/atomWithRefresh.test.ts ================================================ import { describe, expect, it } from 'vitest' import { createStore } from 'jotai/vanilla' import { atomWithRefresh } from 'jotai/vanilla/utils' describe('atomWithRefresh', () => { it('[DEV-ONLY] throws when refresh is called with extra arguments', () => { const atom = atomWithRefresh(() => {}) const store = createStore() const args = ['some arg'] as unknown as [] expect(() => store.set(atom, ...args)).throws() }) }) ================================================ FILE: tests/vanilla/utils/atomWithReset.test.ts ================================================ import { beforeEach, describe, expect, it, vi } from 'vitest' import { createStore } from 'jotai/vanilla' import { RESET, atomWithReset } from 'jotai/vanilla/utils' describe('atomWithReset', () => { let initialValue: number let testAtom: any beforeEach(() => { vi.clearAllMocks() initialValue = 10 testAtom = atomWithReset(initialValue) }) it('should reset to initial value using RESET', () => { const store = createStore() store.set(testAtom, 123) store.set(testAtom, RESET) expect(store.get(testAtom)).toBe(initialValue) }) it('should update atom with a new value', () => { const store = createStore() store.set(testAtom, 123) store.set(testAtom, 30) expect(store.get(testAtom)).toBe(30) }) it('should update atom using a function', () => { const store = createStore() store.set(testAtom, 123) store.set(testAtom, (prev: number) => prev + 10) expect(store.get(testAtom)).toBe(133) }) }) ================================================ FILE: tests/vanilla/utils/loadable.test.ts ================================================ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { atom, createStore } from 'jotai/vanilla' import { loadable } from 'jotai/vanilla/utils' let savedConsoleWarn: any describe('loadable', () => { beforeEach(() => { vi.useFakeTimers() savedConsoleWarn = console.warn console.warn = vi.fn() }) afterEach(() => { vi.useRealTimers() console.warn = savedConsoleWarn }) it('should return fulfilled value of an already resolved async atom', async () => { const store = createStore() const asyncAtom = atom(Promise.resolve('concrete')) expect(store.get(loadable(asyncAtom))).toEqual({ state: 'loading', }) await vi.advanceTimersByTimeAsync(0) expect(store.get(loadable(asyncAtom))).toEqual({ state: 'hasData', data: 'concrete', }) }) it('should get the latest loadable state after the promise resolves', async () => { const store = createStore() const asyncAtom = atom(Promise.resolve()) const loadableAtom = loadable(asyncAtom) expect(store.get(loadableAtom)).toHaveProperty('state', 'loading') await vi.advanceTimersByTimeAsync(0) expect(store.get(loadableAtom)).toHaveProperty('state', 'hasData') }) // https://github.com/pmndrs/jotai/discussions/3208#discussioncomment-15431859 it('[DEV-ONLY] should not call store.set during atom read', async () => { const store = createStore() const example = atom('Hello') store.get(example) const loadableAtom = loadable(example) vi.clearAllMocks() store.get(loadableAtom) expect(console.warn).not.toHaveBeenCalled() }) }) ================================================ FILE: tests/vanilla/utils/types.test.tsx ================================================ import { expectTypeOf, it } from 'vitest' import { atom } from 'jotai/vanilla' import type { Atom, SetStateAction, WritableAtom } from 'jotai/vanilla' import { selectAtom, unwrap } from 'jotai/vanilla/utils' it('selectAtom() should return the correct types', () => { const doubleCount = (x: number) => x * 2 const syncAtom = atom(0) const syncSelectedAtom = selectAtom(syncAtom, doubleCount) // NOTE: expectTypeOf is not available in TypeScript 4.0.5 and below // [ONLY-TS-4.0.5] [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore expectTypeOf(syncSelectedAtom).toEqualTypeOf>() }) it('unwrap() should return the correct types', () => { const getFallbackValue = () => -1 const syncAtom = atom(0) const syncUnwrappedAtom = unwrap(syncAtom, getFallbackValue) // [ONLY-TS-4.0.5] [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore expectTypeOf(syncUnwrappedAtom).toEqualTypeOf< WritableAtom], void> >() const asyncAtom = atom(Promise.resolve(0)) const asyncUnwrappedAtom = unwrap(asyncAtom, getFallbackValue) // [ONLY-TS-4.0.5] [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore expectTypeOf(asyncUnwrappedAtom).toEqualTypeOf< WritableAtom>], void> >() const maybeAsyncAtom = atom(Promise.resolve(0) as number | Promise) const maybeAsyncUnwrappedAtom = unwrap(maybeAsyncAtom, getFallbackValue) // [ONLY-TS-4.0.5] [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore expectTypeOf(maybeAsyncUnwrappedAtom).toEqualTypeOf< WritableAtom>], void> >() }) ================================================ FILE: tests/vanilla/utils/unwrap.test.ts ================================================ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { atom, createStore } from 'jotai/vanilla' import { unwrap } from 'jotai/vanilla/utils' import { sleep } from '../../test-utils' let savedConsoleWarn: any describe('unwrap', () => { beforeEach(() => { vi.useFakeTimers() savedConsoleWarn = console.warn console.warn = vi.fn() }) afterEach(() => { vi.useRealTimers() console.warn = savedConsoleWarn }) it('should unwrap a promise with no fallback function', async () => { const store = createStore() const countAtom = atom(1) const asyncAtom = atom(async (get) => { const count = get(countAtom) await sleep(100) return count * 2 }) const syncAtom = unwrap(asyncAtom) expect(store.get(syncAtom)).toBe(undefined) await vi.advanceTimersByTimeAsync(100) expect(store.get(syncAtom)).toBe(2) store.set(countAtom, 2) expect(store.get(syncAtom)).toBe(undefined) await vi.advanceTimersByTimeAsync(100) expect(store.get(syncAtom)).toBe(4) store.set(countAtom, 3) expect(store.get(syncAtom)).toBe(undefined) await vi.advanceTimersByTimeAsync(100) expect(store.get(syncAtom)).toBe(6) }) it('should unwrap a promise with fallback function without prev', async () => { const store = createStore() const countAtom = atom(1) const asyncAtom = atom(async (get) => { const count = get(countAtom) await sleep(100) return count * 2 }) const syncAtom = unwrap(asyncAtom, () => -1) expect(store.get(syncAtom)).toBe(-1) await vi.advanceTimersByTimeAsync(100) expect(store.get(syncAtom)).toBe(2) store.set(countAtom, 2) expect(store.get(syncAtom)).toBe(-1) await vi.advanceTimersByTimeAsync(100) expect(store.get(syncAtom)).toBe(4) store.set(countAtom, 3) expect(store.get(syncAtom)).toBe(-1) await vi.advanceTimersByTimeAsync(100) expect(store.get(syncAtom)).toBe(6) }) it('should unwrap a promise with fallback function with prev', async () => { const store = createStore() const countAtom = atom(1) const asyncAtom = atom(async (get) => { const count = get(countAtom) await sleep(100) return count * 2 }) const syncAtom = unwrap(asyncAtom, (prev?: number) => prev ?? 0) expect(store.get(syncAtom)).toBe(0) await vi.advanceTimersByTimeAsync(100) expect(store.get(syncAtom)).toBe(2) store.set(countAtom, 2) expect(store.get(syncAtom)).toBe(2) await vi.advanceTimersByTimeAsync(100) expect(store.get(syncAtom)).toBe(4) store.set(countAtom, 3) expect(store.get(syncAtom)).toBe(4) await vi.advanceTimersByTimeAsync(100) expect(store.get(syncAtom)).toBe(6) store.set(countAtom, 4) expect(store.get(syncAtom)).toBe(6) store.set(countAtom, 5) expect(store.get(syncAtom)).not.toBe(0) // expect 6 or 8 await vi.advanceTimersByTimeAsync(100) expect(store.get(syncAtom)).toBe(10) }) it('should unwrap a sync atom which is noop', () => { const store = createStore() const countAtom = atom(1) const syncAtom = unwrap(countAtom) expect(store.get(syncAtom)).toBe(1) store.set(countAtom, 2) expect(store.get(syncAtom)).toBe(2) store.set(countAtom, 3) expect(store.get(syncAtom)).toBe(3) }) it('should unwrap an async writable atom', async () => { const store = createStore() const asyncAtom = atom(Promise.resolve(1)) const syncAtom = unwrap(asyncAtom, (prev?: number) => prev ?? 0) expect(store.get(syncAtom)).toBe(0) await vi.advanceTimersByTimeAsync(0) expect(store.get(syncAtom)).toBe(1) store.set(syncAtom, Promise.resolve(2)) expect(store.get(syncAtom)).toBe(1) await vi.advanceTimersByTimeAsync(0) expect(store.get(syncAtom)).toBe(2) store.set(syncAtom, Promise.resolve(3)) expect(store.get(syncAtom)).toBe(2) await vi.advanceTimersByTimeAsync(0) expect(store.get(syncAtom)).toBe(3) }) it('should unwrap to a fulfilled value of an already resolved async atom', async () => { const store = createStore() const asyncAtom = atom(Promise.resolve('concrete')) expect(store.get(unwrap(asyncAtom))).toEqual(undefined) await vi.advanceTimersByTimeAsync(0) expect(store.get(unwrap(asyncAtom))).toEqual('concrete') }) it('should get a fulfilled value after the promise resolves', async () => { const store = createStore() const asyncAtom = atom(Promise.resolve('concrete')) const syncAtom = unwrap(asyncAtom) expect(store.get(syncAtom)).toEqual(undefined) await vi.advanceTimersByTimeAsync(0) expect(store.get(syncAtom)).toEqual('concrete') }) it('should throw an error if underlying promise is rejected', async () => { const store = createStore() const asyncAtom = atom(Promise.reject('error')) const syncAtom = unwrap(asyncAtom) store.sub(syncAtom, () => {}) await vi.advanceTimersByTimeAsync(0) expect(() => store.get(syncAtom)).toThrow('error') store.set(asyncAtom, Promise.resolve(3)) await vi.advanceTimersByTimeAsync(0) expect(store.get(syncAtom)).toBe(3) }) it('should update dependents with the value of the unwrapped atom when the promise resolves', async () => { const store = createStore() const asyncTarget = atom(() => Promise.resolve('value')) const target = unwrap(asyncTarget) const results: string[] = [] const derived = atom(async (get) => { await Promise.resolve() results.push('effect ' + get(target)) }) store.sub(derived, () => {}) await vi.advanceTimersByTimeAsync(0) expect(results).toEqual(['effect undefined', 'effect value']) }) // https://github.com/pmndrs/jotai/discussions/3208#discussioncomment-15431859 it('[DEV-ONLY] should not call store.set during atom read', async () => { const store = createStore() const example = atom('Hello') store.get(example) const unwrapAtom = unwrap(example) vi.clearAllMocks() store.get(unwrapAtom) expect(console.warn).not.toHaveBeenCalled() }) }) ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "target": "esnext", "strict": true, "jsx": "react-jsx", "esModuleInterop": true, "module": "esnext", "moduleResolution": "bundler", "skipLibCheck": true /* FIXME remove this once vite fixes it */, "allowImportingTsExtensions": true, "noUncheckedIndexedAccess": true, "exactOptionalPropertyTypes": true, "verbatimModuleSyntax": true, "declaration": true, "isolatedDeclarations": true, "types": ["@testing-library/jest-dom"], "noEmit": true, "baseUrl": ".", "paths": { "jotai": ["./src/index.ts"], "jotai/*": ["./src/*.ts"] } }, "include": ["src/**/*", "tests/**/*", "benchmarks/**/*"], "exclude": ["node_modules", "dist"] } ================================================ FILE: vitest.config.mts ================================================ import { existsSync } from 'node:fs' import { resolve } from 'node:path' import react from '@vitejs/plugin-react' import { defineConfig } from 'vitest/config' export default defineConfig({ resolve: { alias: [ { find: /^jotai$/, replacement: resolve('./src/index.ts') }, { find: /^jotai(.*)$/, replacement: resolve('./src/$1.ts') }, ], }, plugins: [ react({ babel: { plugins: existsSync('./dist/babel/plugin-debug-label.js') ? [ // FIXME Can we read from ./src instead of ./dist? './dist/babel/plugin-debug-label.js', ] : [], }, }), ], test: { name: 'jotai', // Keeping globals to true triggers React Testing Library's auto cleanup // https://vitest.dev/guide/migration.html globals: true, environment: 'jsdom', dir: 'tests', reporters: process.env.GITHUB_ACTIONS ? ['default', 'github-actions'] : ['default'], setupFiles: ['tests/setup.ts'], coverage: { reporter: ['text', 'json', 'html', 'text-summary'], reportsDirectory: './coverage/', provider: 'v8', include: ['src/**'], }, onConsoleLog(log) { if (log.includes('DOMException')) return false }, }, }) ================================================ FILE: website/.babelrc ================================================ { "presets": [ [ "babel-preset-gatsby", { "reactRuntime": "automatic", "targets": { "browsers": [ ">0.25%", "not dead", "not ie <=11", "not ie_mob <=11", "not op_mini all" ] } } ] ], "plugins": ["jotai/babel/plugin-react-refresh"] } ================================================ FILE: website/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # nyc test coverage .nyc_output # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (http://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # Typescript v1 declaration files typings/ # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # dotenv environment variable files .env* # gatsby files .cache/ public # Mac files .DS_Store # Yarn yarn-error.log .pnp/ .pnp.js # Yarn Integrity file .yarn-integrity ================================================ FILE: website/README.md ================================================ ## Getting Started Install the dependencies: ```bash pnpm install --ignore-workspace ``` Make a cache directory: ```bash mkdir .cache ``` Run the development server: ```bash pnpm run dev ``` Open [http://localhost:9000](http://localhost:9000) with your browser to see the result. ================================================ FILE: website/api/contact.js ================================================ import * as postmark from 'postmark' const client = new postmark.ServerClient(process.env.POSTMARK_API_TOKEN) export default async function handler(request, response) { const body = request.body if (!body.name || !body.email || !body.message) { return response.status(400).json({ data: 'Invalid' }) } const subject = `Message from ${body.name} (${body.email}) via jotai.org` const message = ` Name: ${body.name}\r\n Email: ${body.email}\r\n Message: ${body.message} ` try { await client.sendEmail({ From: 'noreply@jotai.org', To: process.env.EMAIL_RECIPIENTS, Subject: subject, ReplyTo: body.email, TextBody: message, }) response.status(200).json({ status: 'Sent' }) } catch (error) { response.status(500).json({ status: 'Not sent' }) } } ================================================ FILE: website/gatsby-browser.js ================================================ import './src/styles/index.css' export { wrapRootElement, wrapPageElement } from './gatsby-shared.js' ================================================ FILE: website/gatsby-config.js ================================================ /* eslint-disable @typescript-eslint/no-var-requires */ require('dotenv').config() const kebabCase = require('just-kebab-case') const getAnchor = (value) => { return typeof value === 'string' ? kebabCase(value.toLowerCase().replaceAll("'", '')) : '' } const DOCS_QUERY = ` query { allMdx { nodes { slug meta: frontmatter { title description keywords } headings(depth: h2) { value } excerpt rawBody } } } ` const queries = [ { query: DOCS_QUERY, transformer: ({ data }) => { const results = [] data.allMdx.nodes.forEach((item) => { const transformedNode = { objectID: item.slug, slug: item.slug, title: item.meta.title, description: item.meta.description, keywords: item.meta?.keywords?.split(',') ?? [], excerpt: item.excerpt, headings: item.headings.map((heading) => heading.value).join(' '), body: item.rawBody.replace(/(<([^>]+)>)/gi, ''), level: 1, } if (item.slug !== 'introduction') { item.headings .map((heading) => heading.value) .forEach((heading) => { const transformedNode = { objectID: `${item.slug}#${getAnchor(heading)}`, slug: `${item.slug}#${getAnchor(heading)}`, title: heading, description: '', keywords: [], excerpt: '', headings: [], body: '', level: 2, } results.push(transformedNode) }) } results.push(transformedNode) }) return results }, indexName: 'Docs', settings: { searchableAttributes: [ 'slug', 'title', 'description', 'keywords', 'excerpt', 'headings', 'body', ], indexLanguages: ['en'], }, mergeSettings: false, }, ] module.exports = { siteMetadata: { title: `Jotai, primitive and flexible state management for React`, description: `Jotai takes a bottom-up approach to global React state management with an atomic model inspired by Recoil. One can build state by combining atoms and renders are optimized based on atom dependency. This solves the extra re-render issue of React context and eliminates the need for memoization.`, siteUrl: `https://jotai.org`, shortName: `Jotai`, }, plugins: [ `gatsby-plugin-pnpm`, { resolve: `gatsby-source-filesystem`, options: { name: `docs`, path: `../docs`, }, }, { resolve: `gatsby-plugin-mdx`, options: { extensions: [`.md`, `.mdx`], }, }, `gatsby-plugin-postcss`, { resolve: `gatsby-plugin-algolia`, options: { appId: process.env.GATSBY_ALGOLIA_APP_ID, apiKey: process.env.ALGOLIA_ADMIN_KEY, queries, skipIndexing: process.env.ALGOLIA_SKIP_INDEXING === 'true', }, }, `gatsby-plugin-sitemap`, { resolve: `gatsby-plugin-google-gtag`, options: { trackingIds: ['G-WWJ8XD0QP0'], gtagConfig: { anonymize_ip: true, cookie_expires: 0, }, pluginConfig: { head: false, respectDNT: true, }, }, }, ], flags: { DEV_SSR: false, LAZY_IMAGES: true, PRESERVE_FILE_DOWNLOAD_CACHE: true, PARALLEL_SOURCING: true, }, graphqlTypegen: false, jsxRuntime: 'automatic', polyfill: false, trailingSlash: 'never', } ================================================ FILE: website/gatsby-node.js ================================================ exports.createPages = ({ actions }) => { const { createRedirect } = actions actions.setWebpackConfig({ module: { rules: [ { test: require.resolve(`@gatsbyjs/reach-router/index`), type: `javascript/auto`, use: [ { loader: require.resolve(`./reach-router`), }, ], }, ], }, }) createRedirect({ fromPath: `/docs/introduction`, toPath: `/docs`, isPermanent: false, }) createRedirect({ fromPath: `/docs/api/babel`, toPath: `/docs/tools/babel`, isPermanent: false, }) createRedirect({ fromPath: `/docs/api/core`, toPath: `/docs/core/atom`, isPermanent: false, }) createRedirect({ fromPath: `/docs/api/devtools`, toPath: `/docs/tools/devtools`, isPermanent: false, }) createRedirect({ fromPath: `/docs/api/devtools`, toPath: `/docs/tools/devtools`, isPermanent: false, }) createRedirect({ fromPath: `/docs/api/swc`, toPath: `/docs/tools/swc`, isPermanent: false, }) createRedirect({ fromPath: `/docs/api/utils`, toPath: `/docs/tools/introduction`, isPermanent: false, }) createRedirect({ fromPath: `/docs/utils/atom-family`, toPath: `/docs/utilities/family`, isPermanent: false, }) createRedirect({ fromPath: `/docs/utils/atom-with-default`, toPath: `/docs/utilities/resettable`, isPermanent: false, }) createRedirect({ fromPath: `/docs/utils/atom-with-hash`, toPath: `/docs/extensions/location`, isPermanent: false, }) createRedirect({ fromPath: `/docs/utils/atom-with-observable`, toPath: `/docs/utilities/async`, isPermanent: false, }) createRedirect({ fromPath: `/docs/utils/atom-with-reducer`, toPath: `/docs/utilities/reducer`, isPermanent: false, }) createRedirect({ fromPath: `/docs/utils/atom-with-reset`, toPath: `/docs/utilities/resettable`, isPermanent: false, }) createRedirect({ fromPath: `/docs/utils/atom-with-storage`, toPath: `/docs/utilities/storage`, isPermanent: false, }) createRedirect({ fromPath: `/docs/utils/freeze-atom-creator`, toPath: `/docs/tools/devtools`, isPermanent: false, }) createRedirect({ fromPath: `/docs/utils/freeze-atom`, toPath: `/docs/tools/devtools`, isPermanent: false, }) createRedirect({ fromPath: `/docs/utils/loadable`, toPath: `/docs/utilities/async`, isPermanent: false, }) createRedirect({ fromPath: `/docs/utils/reset`, toPath: `/docs/utilities/resettable`, isPermanent: false, }) createRedirect({ fromPath: `/docs/utils/select-atom`, toPath: `/docs/utilities/select`, isPermanent: false, }) createRedirect({ fromPath: `/docs/utils/split-atom`, toPath: `/docs/utilities/split`, isPermanent: false, }) createRedirect({ fromPath: `/docs/utils/use-atom-callback`, toPath: `/docs/utilities/callback`, isPermanent: false, }) createRedirect({ fromPath: `/docs/utils/use-atom-value`, toPath: `/docs/core/use-atom`, isPermanent: false, }) createRedirect({ fromPath: `/docs/utils/use-hydrate-atoms`, toPath: `/docs/utilities/ssr`, isPermanent: false, }) createRedirect({ fromPath: `/docs/utils/use-reducer-atom`, toPath: `/docs/utilities/reducer`, isPermanent: false, }) createRedirect({ fromPath: `/docs/utils/use-reset-atom`, toPath: `/docs/utilities/resttable`, isPermanent: false, }) createRedirect({ fromPath: `/docs/utils/use-update-atom`, toPath: `/docs/core/use-atom`, isPermanent: false, }) createRedirect({ fromPath: `/docs/integrations/cache`, toPath: `/docs/extensions/cache`, isPermanent: false, }) createRedirect({ fromPath: `/docs/integrations/effect`, toPath: `/docs/extensions/effect`, isPermanent: false, }) createRedirect({ fromPath: `/docs/integrations/immer`, toPath: `/docs/extensions/immer`, isPermanent: false, }) createRedirect({ fromPath: `/docs/integrations/location`, toPath: `/docs/extensions/location`, isPermanent: false, }) createRedirect({ fromPath: `/docs/integrations/optics`, toPath: `/docs/extensions/optics`, isPermanent: false, }) createRedirect({ fromPath: `/docs/integrations/query`, toPath: `/docs/extensions/query`, isPermanent: false, }) createRedirect({ fromPath: `/docs/integrations/redux`, toPath: `/docs/extensions/redux`, isPermanent: false, }) createRedirect({ fromPath: `/docs/integrations/relay`, toPath: `/docs/extensions/relay`, isPermanent: false, }) createRedirect({ fromPath: `/docs/integrations/scope`, toPath: `/docs/extensions/scope`, isPermanent: false, }) createRedirect({ fromPath: `/docs/integrations/trpc`, toPath: `/docs/extensions/trpc`, isPermanent: false, }) createRedirect({ fromPath: `/docs/integrations/urql`, toPath: `/docs/extensions/urql`, isPermanent: false, }) createRedirect({ fromPath: `/docs/integrations/valtio`, toPath: `/docs/extensions/valtio`, isPermanent: false, }) createRedirect({ fromPath: `/docs/integrations/xstate`, toPath: `/docs/extensions/xstate`, isPermanent: false, }) createRedirect({ fromPath: `/docs/integrations/zustand`, toPath: `/docs/extensions/zustand`, isPermanent: false, }) } ================================================ FILE: website/gatsby-shared.js ================================================ import { MDXProvider } from '@mdx-js/react' import { Provider as JotaiProvider, createStore } from 'jotai' import { countAtom, menuAtom, searchAtom, textAtom } from './src/atoms/index.js' import { CodeSandbox } from './src/components/code-sandbox.js' import { Code } from './src/components/code.js' import { InlineCode } from './src/components/inline-code.js' import { Layout } from './src/components/layout.js' import { A, H2, H3, H4, H5 } from './src/components/mdx.js' import { Stackblitz } from './src/components/stackblitz.js' import { TOC } from './src/components/toc.js' const store = createStore() store.set(countAtom, 0) store.set(menuAtom, false) store.set(searchAtom, false) store.set(textAtom, 'hello') const components = { code: Code, inlineCode: InlineCode, CodeSandbox, Stackblitz, TOC, h2: H2, h3: H3, h4: H4, h5: H5, a: A, } export const wrapRootElement = ({ element }) => ( {element} ) export const wrapPageElement = ({ element, props }) => { return {element} } ================================================ FILE: website/gatsby-ssr.js ================================================ export { wrapRootElement, wrapPageElement } from './gatsby-shared.js' export const onRenderBody = ({ setHtmlAttributes, setPreBodyComponents }) => { setHtmlAttributes({ lang: 'en' }) setPreBodyComponents([