├── .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 │ ├── 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 │ │ ├── assets │ │ │ └── jotai-mascot.png │ │ ├── index.css │ │ ├── index.tsx │ │ └── vite-env.d.ts │ ├── tsconfig.json │ └── vite.config.ts ├── text_length │ ├── README.md │ ├── index.html │ ├── package.json │ ├── public │ │ ├── castle.jpg │ │ ├── cover.svg │ │ ├── index.html │ │ └── snippet.png │ ├── 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 ├── img ├── jotai-course-banner.jpg ├── jotai-header-dark.png ├── jotai-header-light.png ├── jotai-mascot.png ├── jotai-opengraph.png └── title.svg ├── package.json ├── pnpm-lock.yaml ├── rollup.config.mjs ├── src ├── babel │ ├── plugin-debug-label.ts │ ├── plugin-react-refresh.ts │ ├── preset.ts │ └── utils.ts ├── index.ts ├── react.ts ├── react │ ├── Provider.ts │ ├── useAtom.ts │ ├── useAtomValue.ts │ ├── useSetAtom.ts │ ├── utils.ts │ └── utils │ │ ├── useAtomCallback.ts │ │ ├── useHydrateAtoms.ts │ │ ├── useReducerAtom.ts │ │ └── useResetAtom.ts ├── types.d.ts ├── utils.ts ├── vanilla.ts └── vanilla │ ├── atom.ts │ ├── internals.ts │ ├── store.ts │ ├── typeUtils.ts │ ├── utils.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 ├── 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 └── 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 │ ├── 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 ├── api └── contact.js ├── gatsby-browser.js ├── gatsby-config.js ├── gatsby-node.js ├── gatsby-shared.js ├── gatsby-ssr.js ├── jsconfig.json ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public └── robots.txt ├── 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 ├── favicon.svg ├── fonts │ ├── inter-italic-var.woff2 │ ├── inter-var.woff2 │ └── meslo.woff2 ├── robots.txt └── search-by-algolia.svg ├── tailwind.config.js └── vercel.json /.codesandbox/ci.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": ["dist"], 3 | "sandboxes": [ 4 | "new", 5 | "react-typescript-react-ts", 6 | "simple-react-browserify-x9yni", 7 | "simple-snowpack-react-o1gmx", 8 | "next-js-uo1h0", 9 | "next-js-with-custom-babel-config-komw9", 10 | "react-with-custom-babel-config-z1ebx" 11 | ], 12 | "node": "18" 13 | } 14 | -------------------------------------------------------------------------------- /.github/DISCUSSION_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | labels: ['bug'] 2 | body: 3 | - type: markdown 4 | attributes: 5 | value: If you don't have a reproduction link, please choose a different category. 6 | - type: textarea 7 | attributes: 8 | label: Bug Description 9 | description: Describe the bug you encountered 10 | validations: 11 | required: true 12 | - type: input 13 | attributes: 14 | label: Reproduction Link 15 | 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. 16 | validations: 17 | required: true 18 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [dai-shi] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: jotai # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: ['https://daishi.gumroad.com/l/learn-jotai'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Assigned issue 3 | about: This is to create a new issue that already has an assignee. Please open a new discussion otherwise. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Bug Reports 4 | url: https://github.com/pmndrs/jotai/discussions/new?category=bug-report 5 | about: Please post bug reports here. 6 | - name: Questions 7 | url: https://github.com/pmndrs/jotai/discussions/new?category=q-a 8 | about: Please post questions here. 9 | - name: Other Discussions 10 | url: https://github.com/pmndrs/jotai/discussions/new/choose 11 | about: Please post ideas and general discussions here. 12 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Related Bug Reports or Discussions 2 | 3 | Fixes # 4 | 5 | ## Summary 6 | 7 | ## Check List 8 | 9 | - [ ] `pnpm run fix` for formatting and linting code and docs 10 | -------------------------------------------------------------------------------- /.github/workflows/compressed-size.yml: -------------------------------------------------------------------------------- 1 | name: Compressed Size 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | compressed_size: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: pnpm/action-setup@v4 11 | - uses: actions/setup-node@v4 12 | with: 13 | node-version: 'lts/*' 14 | cache: 'pnpm' 15 | - uses: preactjs/compressed-size-action@v2 16 | with: 17 | pattern: './dist/**/*.{js,mjs}' 18 | -------------------------------------------------------------------------------- /.github/workflows/ecosystem-ci.yml: -------------------------------------------------------------------------------- 1 | name: Ecosystem CI 2 | 3 | on: 4 | issue_comment: 5 | types: [created] 6 | 7 | jobs: 8 | trigger: 9 | runs-on: ubuntu-latest 10 | if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body, '/ecosystem-ci run') }} 11 | steps: 12 | - uses: actions/checkout@v4 13 | with: 14 | repository: 'jotaijs/jotai-ecosystem-ci' 15 | - uses: pnpm/action-setup@v4 16 | - uses: actions/setup-node@v4 17 | with: 18 | node-version: 22 19 | cache: 'pnpm' 20 | - run: pnpm install 21 | - name: Get Short SHA 22 | id: short_sha 23 | run: | 24 | api="https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.issue.number }}" 25 | sha=$(curl -s -H "Authorization: token $GITHUB_TOKEN" $api | jq -r '.head.sha' | cut -c1-8) 26 | echo "x=$sha" >> $GITHUB_OUTPUT 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | - name: Run Ecosystem CI 30 | id: run_command 31 | run: | 32 | echo "x<> $GITHUB_OUTPUT 33 | pnpm run ecosystem-ci | grep -A999 -- '---- Jotai Ecosystem CI Results ----' >> $GITHUB_OUTPUT 34 | echo "EOF" >> $GITHUB_OUTPUT 35 | env: 36 | JOTAI_PKG: https://pkg.csb.dev/pmndrs/jotai/commit/${{ steps.short_sha.outputs.x }}/jotai 37 | - uses: peter-evans/create-or-update-comment@v4 38 | with: 39 | issue-number: ${{ github.event.issue.number }} 40 | body: | 41 | ## Ecosystem CI Output 42 | ``` 43 | ${{ steps.run_command.outputs.x }} 44 | ``` 45 | -------------------------------------------------------------------------------- /.github/workflows/livecodes-post-comment.yml: -------------------------------------------------------------------------------- 1 | name: LiveCodes Post Comment 2 | 3 | on: 4 | workflow_run: 5 | workflows: [LiveCodes Preview] 6 | types: 7 | - completed 8 | 9 | jobs: 10 | upload: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | pull-requests: write 14 | if: > 15 | github.event.workflow_run.event == 'pull_request' && 16 | github.event.workflow_run.conclusion == 'success' 17 | steps: 18 | - uses: live-codes/pr-comment-from-artifact@v1 19 | with: 20 | GITHUB_TOKEN: ${{ github.token }} 21 | -------------------------------------------------------------------------------- /.github/workflows/livecodes-preview.yml: -------------------------------------------------------------------------------- 1 | name: LiveCodes Preview 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | build_and_prepare: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: pnpm/action-setup@v4 11 | - uses: actions/setup-node@v4 12 | with: 13 | node-version: 'lts/*' 14 | cache: 'pnpm' 15 | - uses: live-codes/preview-in-livecodes@v1 16 | with: 17 | install-command: pnpm install 18 | build-command: pnpm run build 19 | base-url: 'https://{{LC::REF}}.preview-in-livecodes-demo.pages.dev' 20 | -------------------------------------------------------------------------------- /.github/workflows/preview-release.yml: -------------------------------------------------------------------------------- 1 | name: Preview Release 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | preview_release: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: pnpm/action-setup@v4 11 | - uses: actions/setup-node@v4 12 | with: 13 | node-version: 'lts/*' 14 | cache: 'pnpm' 15 | - run: pnpm install 16 | - run: pnpm run build 17 | - run: pnpm dlx pkg-pr-new publish './dist' --compact --template './examples/*' 18 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: pnpm/action-setup@v4 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version: 'lts/*' 16 | registry-url: 'https://registry.npmjs.org' 17 | cache: 'pnpm' 18 | - run: pnpm install 19 | - run: pnpm run build 20 | - run: npm publish 21 | env: 22 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 23 | working-directory: dist 24 | -------------------------------------------------------------------------------- /.github/workflows/test-multiple-versions.yml: -------------------------------------------------------------------------------- 1 | name: Test Multiple Versions 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | types: [opened, synchronize] 8 | 9 | jobs: 10 | test_multiple_versions: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | react: 16 | - 16.14.0 17 | - 17.0.0 18 | - 18.0.0 19 | - 18.1.0 20 | - 18.2.0 21 | - 18.3.1 22 | - 19.0.0 23 | - 19.1.0 24 | - 19.2.0-canary-c0464aed-20250523 25 | - 0.0.0-experimental-c0464aed-20250523 26 | steps: 27 | - uses: actions/checkout@v4 28 | - uses: pnpm/action-setup@v4 29 | - uses: actions/setup-node@v4 30 | with: 31 | node-version: 'lts/*' 32 | cache: 'pnpm' 33 | - run: pnpm install 34 | - name: Install legacy testing-library 35 | if: ${{ startsWith(matrix.react, '16.') || startsWith(matrix.react, '17.') }} 36 | run: | 37 | pnpm add -D @testing-library/react@12.1.4 38 | - name: Patch for React 16 39 | if: ${{ startsWith(matrix.react, '16.') }} 40 | run: | 41 | sed -i~ '1s/^/import React from "react";/' tests/*/*.tsx tests/*/*/*.tsx 42 | sed -i~ 's/"jsx": "react-jsx"/"jsx": "react"/' tsconfig.json 43 | sed -i~ 's/import\.meta\.env[?]\.MODE/"DEVELOPMENT".toLowerCase()/' src/*.ts src/*/*.ts src/*/*/*.ts 44 | - name: Test Build # we need to build for babel tests 45 | run: pnpm run build 46 | - name: Test ${{ matrix.react }} 47 | run: | 48 | pnpm add -D react@${{ matrix.react }} react-dom@${{ matrix.react }} 49 | pnpm run test:spec 50 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | types: [opened, synchronize] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: pnpm/action-setup@v4 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version: 'lts/*' 18 | cache: 'pnpm' 19 | - run: pnpm install 20 | - run: pnpm run test:format 21 | - run: pnpm run test:types 22 | - run: pnpm run test:lint 23 | - run: pnpm run test:spec 24 | - run: pnpm run build # we don't have any other workflows to test build 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | .pnp 4 | .pnp.js 5 | 6 | # testing 7 | coverage 8 | 9 | # development 10 | .devcontainer 11 | .vscode 12 | 13 | # production 14 | dist 15 | build 16 | 17 | # dotenv environment variables file 18 | .env 19 | .env.local 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | 24 | # logs 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | # misc 30 | .DS_Store 31 | .idea 32 | 33 | # examples 34 | examples/**/*/package-lock.json 35 | examples/**/*/yarn.lock 36 | examples/**/*/pnpm-lock.yaml 37 | examples/**/*/bun.lockb 38 | -------------------------------------------------------------------------------- /.livecodes/react.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "React demo", 3 | "activeEditor": "script", 4 | "markup": { 5 | "language": "html", 6 | "content": "
" 7 | }, 8 | "style": { 9 | "language": "css", 10 | "content": ".App {\n font-family: sans-serif;\n text-align: center;\n}\n" 11 | }, 12 | "script": { 13 | "language": "jsx", 14 | "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" 15 | }, 16 | "customSettings": { 17 | "jotai commit sha": "{{LC::SHORT_SHA}}", 18 | "imports": { 19 | "jotai": "{{LC::TO_DATA_URL(./dist/esm/index.mjs)}}", 20 | "jotai/vanilla": "{{LC::TO_DATA_URL(./dist/esm/vanilla.mjs)}}", 21 | "jotai/utils": "{{LC::TO_DATA_URL(./dist/esm/utils.mjs)}}", 22 | "jotai/react": "{{LC::TO_DATA_URL(./dist/esm/react.mjs)}}", 23 | "jotai/vanilla/utils": "{{LC::TO_DATA_URL(./dist/esm/vanilla/utils.mjs)}}", 24 | "jotai/react/utils": "{{LC::TO_DATA_URL(./dist/esm/react/utils.mjs)}}" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | pnpm-lock.yaml 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Poimandres 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /babel.config.mjs: -------------------------------------------------------------------------------- 1 | export default (api, targets) => { 2 | // https://babeljs.io/docs/en/config-files#config-function-api 3 | const isTestEnv = api.env('test') 4 | 5 | return { 6 | babelrc: false, 7 | ignore: ['./node_modules'], 8 | presets: [ 9 | [ 10 | '@babel/preset-env', 11 | { 12 | loose: true, 13 | modules: isTestEnv ? 'commonjs' : false, 14 | targets: isTestEnv ? { node: 'current' } : targets, 15 | }, 16 | ], 17 | ], 18 | plugins: [ 19 | [ 20 | '@babel/plugin-transform-react-jsx', 21 | { 22 | runtime: 'automatic', 23 | }, 24 | ], 25 | ['@babel/plugin-transform-typescript', { isTSX: true }], 26 | ], 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /benchmarks/.gitignore: -------------------------------------------------------------------------------- 1 | /*.json 2 | /*.html 3 | -------------------------------------------------------------------------------- /benchmarks/simple-read.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env npx tsx 2 | 3 | import { add, complete, cycle, save, suite } from 'benny' 4 | import { atom } from '../src/vanilla/atom.ts' 5 | import type { PrimitiveAtom } from '../src/vanilla/atom.ts' 6 | import { createStore } from '../src/vanilla/store.ts' 7 | 8 | const createStateWithAtoms = (n: number) => { 9 | let targetAtom: PrimitiveAtom | undefined 10 | const store = createStore() 11 | for (let i = 0; i < n; ++i) { 12 | const a = atom(i) 13 | if (!targetAtom) { 14 | targetAtom = a 15 | } 16 | store.set(a, i) 17 | } 18 | if (!targetAtom) { 19 | throw new Error() 20 | } 21 | return [store, targetAtom] as const 22 | } 23 | 24 | const main = async () => { 25 | await suite( 26 | 'simple-read', 27 | ...[2, 3, 4, 5, 6].map((n) => 28 | add(`atoms=${10 ** n}`, () => { 29 | const [store, targetAtom] = createStateWithAtoms(10 ** n) 30 | return () => store.get(targetAtom) 31 | }), 32 | ), 33 | cycle(), 34 | complete(), 35 | save({ 36 | folder: __dirname, 37 | file: 'simple-read', 38 | format: 'json', 39 | }), 40 | save({ 41 | folder: __dirname, 42 | file: 'simple-read', 43 | format: 'chart.html', 44 | }), 45 | ) 46 | } 47 | 48 | main() 49 | -------------------------------------------------------------------------------- /benchmarks/simple-write.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env npx tsx 2 | 3 | import { add, complete, cycle, save, suite } from 'benny' 4 | import { atom } from '../src/vanilla/atom.ts' 5 | import type { PrimitiveAtom } from '../src/vanilla/atom.ts' 6 | import { createStore } from '../src/vanilla/store.ts' 7 | 8 | const createStateWithAtoms = (n: number) => { 9 | let targetAtom: PrimitiveAtom | undefined 10 | const store = createStore() 11 | for (let i = 0; i < n; ++i) { 12 | const a = atom(i) 13 | if (!targetAtom) { 14 | targetAtom = a 15 | } 16 | store.set(a, i) 17 | } 18 | if (!targetAtom) { 19 | throw new Error() 20 | } 21 | return [store, targetAtom] as const 22 | } 23 | 24 | const main = async () => { 25 | await suite( 26 | 'simple-write', 27 | ...[2, 3, 4, 5, 6].map((n) => 28 | add(`atoms=${10 ** n}`, () => { 29 | const [store, targetAtom] = createStateWithAtoms(10 ** n) 30 | return () => store.set(targetAtom, (c) => c + 1) 31 | }), 32 | ), 33 | cycle(), 34 | complete(), 35 | save({ 36 | folder: __dirname, 37 | file: 'simple-write', 38 | format: 'json', 39 | }), 40 | save({ 41 | folder: __dirname, 42 | file: 'simple-write', 43 | format: 'chart.html', 44 | }), 45 | ) 46 | } 47 | 48 | main() 49 | -------------------------------------------------------------------------------- /benchmarks/subscribe-write.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env npx tsx 2 | 3 | import { add, complete, cycle, save, suite } from 'benny' 4 | import { atom } from '../src/vanilla/atom.ts' 5 | import type { PrimitiveAtom } from '../src/vanilla/atom.ts' 6 | import { createStore } from '../src/vanilla/store.ts' 7 | 8 | const cleanupFns = new Set<() => void>() 9 | const cleanup = () => { 10 | cleanupFns.forEach((fn) => fn()) 11 | cleanupFns.clear() 12 | } 13 | 14 | const createStateWithAtoms = (n: number) => { 15 | let targetAtom: PrimitiveAtom | undefined 16 | const store = createStore() 17 | for (let i = 0; i < n; ++i) { 18 | const a = atom(i) 19 | if (!targetAtom) { 20 | targetAtom = a 21 | } 22 | store.get(a) 23 | const unsub = store.sub(a, () => { 24 | store.get(a) 25 | }) 26 | cleanupFns.add(unsub) 27 | } 28 | if (!targetAtom) { 29 | throw new Error() 30 | } 31 | return [store, targetAtom] as const 32 | } 33 | 34 | const main = async () => { 35 | await suite( 36 | 'subscribe-write', 37 | ...[2, 3, 4, 5, 6].map((n) => 38 | add(`atoms=${10 ** n}`, () => { 39 | cleanup() 40 | const [store, targetAtom] = createStateWithAtoms(10 ** n) 41 | return () => store.set(targetAtom, (c) => c + 1) 42 | }), 43 | ), 44 | cycle(), 45 | complete(), 46 | save({ 47 | folder: __dirname, 48 | file: 'subscribe-write', 49 | format: 'json', 50 | }), 51 | save({ 52 | folder: __dirname, 53 | file: 'subscribe-write', 54 | format: 'chart.html', 55 | }), 56 | ) 57 | } 58 | 59 | main() 60 | -------------------------------------------------------------------------------- /docs/basics/concepts.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Concepts 3 | nav: 7.01 4 | --- 5 | 6 | Jotai is a library that will make you return to the basics of React development & keep everything simple. 7 | 8 | ### From scratch 9 | 10 | Before trying to compare Jotai with what we may have known previously, let's just dive straight into something very simple. 11 | 12 | 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. 13 | 14 | Now, the Jotai world also has its small entities, atoms, and they also have their state. Composing atoms will create your app state! 15 | 16 | Jotai considers anything to be an atom, so you may say: `Huh, I need objects and arrays, filter them and then sort them out`. 17 | And here's the beauty of it, Jotai gracefully lets you create dumb atoms derived from even more dumb atoms. 18 | 19 | If, for example, I have a page with 2 tabs: online friends and offline friends. 20 | I will have 2 atoms simply derived from a common, dumber source. 21 | 22 | ```js 23 | const dumbAtom = atom([{ name: 'Friend 1', online: false }]) 24 | const onlineAtom = atom((get) => get(dumbAtom).filter((item) => item.online)) 25 | const offlineAtom = atom((get) => get(dumbAtom).filter((item) => !item.online)) 26 | ``` 27 | 28 | And you could keep going on complexity forever. 29 | 30 | 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. 31 | -------------------------------------------------------------------------------- /docs/basics/showcase.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Showcase 3 | nav: 7.03 4 | --- 5 | 6 | - 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) 7 | 8 | Count the length and show the uppercase of any text. 9 | 10 | - 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) 11 | 12 | Demonstrate a news article with Jotai, hit next to see more articles. 13 | 14 | - 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) 15 | 16 | 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. 17 | 18 | - 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) 19 | 20 | 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`. 21 | 22 | - 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) 23 | 24 | An UTC time electronic clock implementation using Next.js and Jotai. 25 | 26 | - 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) 27 | 28 | A game of tic tac toe implemented with Jotai. 29 | 30 | - 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) 31 | 32 | A simple demo to use Jotai with react-three-fiber 33 | -------------------------------------------------------------------------------- /docs/core/provider.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Provider 3 | description: This doc describes core `jotai` bundle. 4 | nav: 2.04 5 | keywords: provider,usestore,ssr 6 | --- 7 | 8 | ## Provider 9 | 10 | The `Provider` component is to provide state for a component sub tree. 11 | Multiple Providers can be used for multiple subtrees, and they can even be nested. 12 | This works just like React Context. 13 | 14 | If an atom is used in a tree without a Provider, 15 | it will use the default state. This is so-called provider-less mode. 16 | 17 | Providers are useful for three reasons: 18 | 19 | 1. To provide a different state for each sub tree. 20 | 2. To accept initial values of atoms. 21 | 3. To clear all atoms by remounting. 22 | 23 | ```jsx 24 | const SubTree = () => ( 25 | 26 | 27 | 28 | ) 29 | ``` 30 | 31 | ### Signatures 32 | 33 | ```ts 34 | const Provider: React.FC<{ 35 | store?: Store 36 | }> 37 | ``` 38 | 39 | 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`. 40 | 41 | ```jsx 42 | const Root = () => ( 43 | 44 | 45 | 46 | ) 47 | ``` 48 | 49 | ### `store` prop 50 | 51 | A Provider accepts an optional prop `store` that you can use for the Provider subtree. 52 | 53 | #### Example 54 | 55 | ```jsx 56 | const myStore = createStore() 57 | 58 | const Root = () => ( 59 | 60 | 61 | 62 | ) 63 | ``` 64 | 65 | ## useStore 66 | 67 | This hook returns a store within the component tree. 68 | 69 | ```jsx 70 | const Component = () => { 71 | const store = useStore() 72 | // ... 73 | } 74 | ``` 75 | -------------------------------------------------------------------------------- /docs/core/store.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Store 3 | description: This doc describes core `jotai` bundle. 4 | nav: 2.03 5 | keywords: store,createstore,getdefaultstore,defaultstore 6 | --- 7 | 8 | ## createStore 9 | 10 | This function is to create a new empty store. 11 | The store can be used to pass in `Provider`. 12 | 13 | The store has three methods: `get` for getting atom values, 14 | `set` for setting atom values, and `sub` for subscribing to atom changes. 15 | 16 | ```jsx 17 | const myStore = createStore() 18 | 19 | const countAtom = atom(0) 20 | myStore.set(countAtom, 1) 21 | const unsub = myStore.sub(countAtom, () => { 22 | console.log('countAtom value is changed to', myStore.get(countAtom)) 23 | }) 24 | // unsub() to unsubscribe 25 | 26 | const Root = () => ( 27 | 28 | 29 | 30 | ) 31 | ``` 32 | 33 | ## getDefaultStore 34 | 35 | This function returns a default store that is used in provider-less mode. 36 | 37 | ```js 38 | const defaultStore = getDefaultStore() 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/extensions/optics.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Optics 3 | description: This doc describes Optics-ts extension. 4 | nav: 4.09 5 | keywords: optics 6 | --- 7 | 8 | ### Install 9 | 10 | You have to install `optics-ts` and `jotai-optics` to use this feature. 11 | 12 | ``` 13 | npm install optics-ts jotai-optics 14 | ``` 15 | 16 | ## focusAtom 17 | 18 | `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, 19 | and when the derived atom is updated, the derivee is notified of the update, and the equivalent update is done on the derivee. 20 | 21 | See this: 22 | 23 | ```js 24 | const baseAtom = atom({ a: 5 }) // PrimitiveAtom<{a: number}> 25 | const derivedAtom = focusAtom(baseAtom, (optic) => optic.prop('a')) // PrimitiveAtom 26 | ``` 27 | 28 | 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 29 | 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`. 30 | 31 | The example below is simple, but it's a starting point. `focusAtom` supports many kinds of optics, including `Lens`, `Prism`, `Isomorphism`. 32 | 33 | To see more advanced optics, please see the example at: https://github.com/akheron/optics-ts 34 | 35 | ### Example 36 | 37 | ```jsx 38 | import { atom } from 'jotai' 39 | import { focusAtom } from 'jotai-optics' 40 | 41 | const objectAtom = atom({ a: 5, b: 10 }) 42 | const aAtom = focusAtom(objectAtom, (optic) => optic.prop('a')) 43 | const bAtom = focusAtom(objectAtom, (optic) => optic.prop('b')) 44 | 45 | const Controls = () => { 46 | const [a, setA] = useAtom(aAtom) 47 | const [b, setB] = useAtom(bAtom) 48 | return ( 49 |
50 | Value of a: {a} 51 | Value of b: {b} 52 | 53 | 54 |
55 | ) 56 | } 57 | ``` 58 | 59 | #### Stackblitz 60 | 61 | 62 | -------------------------------------------------------------------------------- /docs/extensions/redux.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Redux 3 | description: This doc describes Redux extension. 4 | nav: 4.98 5 | keywords: redux 6 | published: false 7 | --- 8 | 9 | Jotai's state resides in React, but sometimes it would be nice 10 | to interact with the world outside React. 11 | 12 | Redux provides a store interface that can be used to store some values 13 | and sync with atoms in Jotai. 14 | 15 | ### Install 16 | 17 | You have to install `redux` and `jotai-redux` to use this feature. 18 | 19 | ``` 20 | npm install redux jotai-redux 21 | ``` 22 | 23 | ## atomWithStore 24 | 25 | `atomWithStore` creates a new atom with redux store. 26 | It's two-way binding and you can change the value from both ends. 27 | 28 | ```jsx 29 | import { useAtom } from 'jotai' 30 | import { atomWithStore } from 'jotai-redux' 31 | import { createStore } from 'redux' 32 | 33 | const initialState = { count: 0 } 34 | const reducer = (state = initialState, action: { type: 'INC' }) => { 35 | if (action.type === 'INC') { 36 | return { ...state, count: state.count + 1 } 37 | } 38 | return state 39 | } 40 | const store = createStore(reducer) 41 | const storeAtom = atomWithStore(store) 42 | 43 | const Counter = () => { 44 | const [state, dispatch] = useAtom(storeAtom) 45 | 46 | return ( 47 | <> 48 | count: {state.count} 49 | 50 | 51 | ) 52 | } 53 | ``` 54 | 55 | ### Examples 56 | 57 | 58 | -------------------------------------------------------------------------------- /docs/extensions/trpc.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: tRPC 3 | description: This doc describes tRPC extension. 4 | nav: 4.01 5 | keywords: rpc,trpc,typescript,t3 6 | --- 7 | 8 | You can use Jotai with [tRPC](https://trpc.io). 9 | 10 | ### Install 11 | 12 | You have to install `jotai-trpc`, `@trpc/client` and `@trpc/server` to use the extension. 13 | 14 | ``` 15 | npm install jotai-trpc @trpc/client @trpc/server 16 | ``` 17 | 18 | ### Usage 19 | 20 | ```ts 21 | import { createTRPCJotai } from 'jotai-trpc' 22 | 23 | const trpc = createTRPCJotai({ 24 | links: [ 25 | httpLink({ 26 | url: myUrl, 27 | }), 28 | ], 29 | }) 30 | 31 | const idAtom = atom('foo') 32 | const queryAtom = trpc.bar.baz.atomWithQuery((get) => get(idAtom)) 33 | ``` 34 | 35 | ### atomWithQuery 36 | 37 | `...atomWithQuery` creates a new atom with "query". It internally uses [Vanilla Client](https://trpc.io/docs/vanilla)'s `...query` procedure. 38 | 39 | ```tsx 40 | import { atom, useAtom } from 'jotai' 41 | import { httpLink } from '@trpc/client' 42 | import { createTRPCJotai } from 'jotai-trpc' 43 | import { trpcPokemonUrl } from 'trpc-pokemon' 44 | import type { PokemonRouter } from 'trpc-pokemon' 45 | 46 | const trpc = createTRPCJotai({ 47 | links: [ 48 | httpLink({ 49 | url: trpcPokemonUrl, 50 | }), 51 | ], 52 | }) 53 | 54 | const NAMES = [ 55 | 'bulbasaur', 56 | 'ivysaur', 57 | 'venusaur', 58 | 'charmander', 59 | 'charmeleon', 60 | 'charizard', 61 | 'squirtle', 62 | 'wartortle', 63 | 'blastoise', 64 | ] 65 | 66 | const nameAtom = atom(NAMES[0]) 67 | 68 | const pokemonAtom = trpc.pokemon.byId.atomWithQuery((get) => get(nameAtom)) 69 | 70 | const Pokemon = () => { 71 | const [data, refresh] = useAtom(pokemonAtom) 72 | return ( 73 |
74 |
ID: {data.id}
75 |
Height: {data.height}
76 |
Weight: {data.weight}
77 | 78 |
79 | ) 80 | } 81 | ``` 82 | 83 | #### Examples 84 | 85 | 86 | 87 | ### atomWithMutation 88 | 89 | `...atomWithMutation` creates a new atom with "mutate". It internally uses [Vanilla Client](https://trpc.io/docs/vanilla)'s `...mutate` procedure. 90 | 91 | FIXME: add code example and codesandbox 92 | 93 | ### atomWithSubscription 94 | 95 | `...atomWithSubscription` creates a new atom with "subscribe". It internally uses [Vanilla Client](https://trpc.io/docs/vanilla)'s `...subscribe` procedure. 96 | 97 | FIXME: add code example and codesandbox 98 | -------------------------------------------------------------------------------- /docs/extensions/zustand.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Zustand 3 | description: This doc describes Zustand extension. 4 | nav: 4.98 5 | keywords: zustand 6 | published: false 7 | --- 8 | 9 | Jotai's state resides in React, but sometimes it would be nice 10 | to interact with the world outside React. 11 | 12 | Zustand provides a store interface that can be used to hold some values 13 | and sync with atoms in Jotai. 14 | 15 | This only uses the vanilla api of zustand. 16 | 17 | ### Install 18 | 19 | You have to install `zustand` and `jotai-zustand` to use this feature. 20 | 21 | ``` 22 | npm install zustand jotai-zustand 23 | ``` 24 | 25 | ## atomWithStore 26 | 27 | `atomWithStore` creates a new atom with zustand store. 28 | It's two-way binding and you can change the value from both ends. 29 | 30 | ```jsx 31 | import { useAtom } from 'jotai' 32 | import { atomWithStore } from 'jotai-zustand' 33 | import create from 'zustand/vanilla' 34 | 35 | const store = create(() => ({ count: 0 })) 36 | const stateAtom = atomWithStore(store) 37 | const Counter = () => { 38 | const [state, setState] = useAtom(stateAtom) 39 | 40 | return ( 41 | <> 42 | count: {state.count} 43 | 48 | 49 | ) 50 | } 51 | ``` 52 | 53 | ### Examples 54 | 55 | 56 | -------------------------------------------------------------------------------- /docs/guides/react-native.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: React Native 3 | description: Using Jotai in React Native 4 | nav: 8.06 5 | keywords: native,ios,android 6 | --- 7 | 8 | Jotai atoms can be used in React Native applications with absolutely no changes. 9 | Our goal is to always be 100% compatible with React-Native. 10 | 11 | ### Persistence 12 | 13 | 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). 14 | 15 | ### Performance 16 | 17 | 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. 18 | 19 | Jotai atomical 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. 20 | 21 | And always remember that renders have to be fast, split calculation logic to async actions. 22 | -------------------------------------------------------------------------------- /docs/guides/remix.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Remix 3 | description: How to use Jotai with Remix 4 | nav: 8.05 5 | keywords: remix 6 | status: draft 7 | --- 8 | 9 | ### Hydration 10 | 11 | Jotai has support for hydration of atoms with `useHydrateAtoms`. The documentation for the hook can be seen [here](../utilities/ssr.mdx). 12 | -------------------------------------------------------------------------------- /docs/guides/resettable.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Resettable 3 | description: How to use resettable atoms 4 | nav: 8.99 5 | published: false 6 | --- 7 | 8 | The Jotai core doesn't support resettable atoms. 9 | But you can create those with helper functions from `jotai/utils`. 10 | 11 | ### Primitive resettable atom with atomWithReset / useResetAtom 12 | 13 | ```jsx 14 | import { useAtom } from 'jotai' 15 | import { atomWithReset, useResetAtom } from 'jotai/utils' 16 | 17 | const todoListAtom = atomWithReset([ 18 | { description: 'Add a todo', checked: false }, 19 | ]) 20 | 21 | const TodoList = () => { 22 | const [todoList, setTodoList] = useAtom(todoListAtom) 23 | const resetTodoList = useResetAtom(todoListAtom) 24 | 25 | return ( 26 | <> 27 |
    28 | {todoList.map((todo) => ( 29 |
  • {todo.description}
  • 30 | ))} 31 |
32 | 33 | 46 | 47 | 48 | ) 49 | } 50 | ``` 51 | 52 | ### Examples 53 | 54 | 55 | 56 | ### Derived atom with RESET symbol 57 | 58 | ```jsx 59 | import { atom, useAtom, useSetAtom } from 'jotai' 60 | import { atomWithReset, useResetAtom, RESET } from 'jotai/utils' 61 | 62 | const dollarsAtom = atomWithReset(0) 63 | const centsAtom = atom( 64 | (get) => get(dollarsAtom) * 100, 65 | (get, set, newValue: number | typeof RESET) => 66 | set(dollarsAtom, newValue === RESET ? newValue : newValue / 100) 67 | ) 68 | 69 | const ResetExample = () => { 70 | const [dollars] = useAtom(dollarsAtom) 71 | const setCents = useSetAtom(centsAtom) 72 | const resetCents = useResetAtom(centsAtom) 73 | 74 | return ( 75 | <> 76 |

Current balance ${dollars}

77 | 78 | 79 | 80 | 81 | ) 82 | } 83 | ``` 84 | 85 | ### Examples 86 | 87 | 88 | -------------------------------------------------------------------------------- /docs/guides/using-store-outside-react.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Using store outside React 3 | description: Using store outside React 4 | nav: 8.98 5 | keywords: state, outside, react 6 | published: false 7 | --- 8 | 9 | Jotai's state resides in React, but sometimes it would be nice 10 | to interact with the world outside React. 11 | 12 | ## createStore 13 | 14 | [`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. 15 | 16 | ```jsx 17 | import { atom, useAtomValue, createStore, Provider } from 'jotai' 18 | 19 | const timeAtom = atom(0) 20 | const store = createStore() 21 | 22 | store.set(timeAtom, (prev) => prev + 1) // Update atom's value 23 | store.get(timeAtom) // Read atom's value 24 | 25 | function Component() { 26 | const time = useAtomValue(timeAtom) // Inside React 27 | return ( 28 |
29 |

{time}

30 |
31 | ) 32 | } 33 | 34 | export default function App() { 35 | return ( 36 | 37 | 38 | 39 | ) 40 | } 41 | ``` 42 | 43 | ### Examples 44 | 45 | 46 | -------------------------------------------------------------------------------- /docs/guides/vite.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Vite 3 | description: How to use Jotai with Vite 4 | nav: 8.99 5 | keywords: vite 6 | published: false 7 | --- 8 | 9 | You can use the plugins from the `jotai/babel` bundle to enhance your developer experience when using Vite and Jotai. 10 | 11 | In your `vite.config.ts`: 12 | 13 | ```js 14 | import { defineConfig } from 'vite' 15 | import react from '@vitejs/plugin-react' 16 | import jotaiDebugLabel from 'jotai/babel/plugin-debug-label' 17 | import jotaiReactRefresh from 'jotai/babel/plugin-react-refresh' 18 | 19 | // https://vitejs.dev/config/ 20 | export default defineConfig({ 21 | plugins: [ 22 | react({ babel: { plugins: [jotaiDebugLabel, jotaiReactRefresh] } }), 23 | ], 24 | // ... The rest of your configuration 25 | }) 26 | ``` 27 | 28 | There's a template below to try it yourself. 29 | 30 | ### Examples 31 | 32 | #### Vite Template 33 | 34 | 35 | -------------------------------------------------------------------------------- /docs/guides/waku.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Waku 3 | description: How to use Jotai with Waku 4 | nav: 8.04 5 | keywords: waku 6 | status: draft 7 | --- 8 | 9 | ### Hydration 10 | 11 | Jotai has support for hydration of atoms with `useHydrateAtoms`. The documentation for the hook can be seen [here](../utils/ssr.mdx). 12 | -------------------------------------------------------------------------------- /docs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Documentation 3 | description: Table of contents 4 | nav: 0 5 | --- 6 | 7 | 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. 8 | 9 | ## Features 10 | 11 | - Minimal core API (2kb) 12 | - Many utilities and extensions 13 | - TypeScript oriented 14 | - Works with Next.js, Waku, Remix, and React Native 15 | 16 | ## Core 17 | 18 | Jotai has a very minimal API, exposing only a few exports from the main `jotai` bundle. They are split into four categories below. 19 | 20 | 21 | 22 | ## Utilities 23 | 24 | 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. 25 | 26 | 27 | 28 | ## Extensions 29 | 30 | Jotai has many officially maintained extensions including `atomWithQuery` for React Query and `atomWithMachine` for XState, among many others. 31 | 32 | 33 | 34 | ## Third-party 35 | 36 | Beyond the official extensions, there are many third-party community packages as well. 37 | 38 | 39 | 40 | ## Tools 41 | 42 | 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. 43 | 44 | 45 | 46 | ## Basics 47 | 48 | Learn the basic concepts of the library, discover how it compares with others, and see usage examples. 49 | 50 | 51 | 52 | ## Guides 53 | 54 | Guides can help with use common cases such as TypeScript, React frameworks, and basic patterns. 55 | 56 | 57 | 58 | ## Recipes 59 | 60 | Recipes can help with more advanced patterns. 61 | 62 | 63 | -------------------------------------------------------------------------------- /docs/recipes/atom-with-broadcast.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: atomWithBroadcast 3 | nav: 9.09 4 | keywords: creators,broadcast 5 | --- 6 | 7 | > `atomWithBroadcast` creates an atom. The atom will be shared between 8 | > browser tabs and frames, similar to `atomWithStorage` but with the 9 | > initialization limitation. 10 | 11 | This can be useful when you want states to interact with each other without the use of localStorage. 12 | 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. 13 | 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. 14 | 15 | ```tsx 16 | import { atom, SetStateAction } from 'jotai' 17 | 18 | export function atomWithBroadcast(key: string, initialValue: Value) { 19 | const baseAtom = atom(initialValue) 20 | const listeners = new Set<(event: MessageEvent) => void>() 21 | const channel = new BroadcastChannel(key) 22 | 23 | channel.onmessage = (event) => { 24 | listeners.forEach((l) => l(event)) 25 | } 26 | 27 | const broadcastAtom = atom( 28 | (get) => get(baseAtom), 29 | (get, set, update: { isEvent: boolean; value: SetStateAction }) => { 30 | set(baseAtom, update.value) 31 | 32 | if (!update.isEvent) { 33 | channel.postMessage(get(baseAtom)) 34 | } 35 | }, 36 | ) 37 | 38 | broadcastAtom.onMount = (setAtom) => { 39 | const listener = (event: MessageEvent) => { 40 | setAtom({ isEvent: true, value: event.data }) 41 | } 42 | 43 | listeners.add(listener) 44 | 45 | return () => { 46 | listeners.delete(listener) 47 | } 48 | } 49 | 50 | const returnedAtom = atom( 51 | (get) => get(broadcastAtom), 52 | (_get, set, update: SetStateAction) => { 53 | set(broadcastAtom, { isEvent: false, value: update }) 54 | }, 55 | ) 56 | 57 | return returnedAtom 58 | } 59 | 60 | const broadAtom = atomWithBroadcast('count', 0) 61 | 62 | const ListOfThings = () => { 63 | const [count, setCount] = useAtom(broadAtom) 64 | 65 | return ( 66 |
67 | {count} 68 | 69 |
70 | ) 71 | } 72 | ``` 73 | 74 | 75 | -------------------------------------------------------------------------------- /docs/recipes/atom-with-compare.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: atomWithCompare 3 | nav: 9.05 4 | keywords: creators,compare 5 | --- 6 | 7 | > `atomWithCompare` creates atom that triggers updates when custom compare function `areEqual(prev, next)` is false. 8 | 9 | This can help you avoid unwanted re-renders by ignoring state changes that don't matter to your application. 10 | 11 | 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. 12 | 13 | ```ts 14 | import { atomWithReducer } from 'jotai/utils' 15 | 16 | export function atomWithCompare( 17 | initialValue: Value, 18 | areEqual: (prev: Value, next: Value) => boolean, 19 | ) { 20 | return atomWithReducer(initialValue, (prev: Value, next: Value) => { 21 | if (areEqual(prev, next)) { 22 | return prev 23 | } 24 | 25 | return next 26 | }) 27 | } 28 | ``` 29 | 30 | Here's how you'd use it to make an atom that ignores updates that are shallow-equal: 31 | 32 | ```ts 33 | import { atomWithCompare } from 'XXX' 34 | import { shallowEquals } from 'YYY' 35 | import { CSSProperties } from 'react' 36 | 37 | const styleAtom = atomWithCompare( 38 | { backgroundColor: 'blue' }, 39 | shallowEquals, 40 | ) 41 | ``` 42 | 43 | In a component: 44 | 45 | ```jsx 46 | const StylePreview = () => { 47 | const [styles, setStyles] = useAtom(styleAtom) 48 | 49 | return ( 50 |
51 |
Style preview
52 | 53 | {/* Clicking this button twice will only trigger one render */} 54 | 57 | 58 | {/* Clicking this button twice will only trigger one render */} 59 | 62 |
63 | ) 64 | } 65 | ``` 66 | -------------------------------------------------------------------------------- /docs/recipes/atom-with-refresh.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: atomWithRefresh 3 | nav: 9.06 4 | keywords: creators,refresh 5 | --- 6 | 7 | `atomWithRefresh` has been provided by `jotai/utils` since v2.7.0. 8 | [Jump to the doc](../utilities/resettable.mdx#atomwithrefresh) 9 | -------------------------------------------------------------------------------- /docs/recipes/atom-with-toggle-and-storage.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: atomWithToggleAndStorage 3 | nav: 9.05 4 | keywords: creators,storage 5 | --- 6 | 7 | > `atomWithToggleAndStorage` is like `atomWithToggle` but also persist the state anytime it changes in given storage using [`atomWithStorage`](../utilities/storage.mdx). 8 | 9 | Here is the source: 10 | 11 | ```ts 12 | import { WritableAtom, atom } from 'jotai' 13 | import { atomWithStorage } from 'jotai/utils' 14 | 15 | export function atomWithToggleAndStorage( 16 | key: string, 17 | initialValue?: boolean, 18 | storage?: any, 19 | ): WritableAtom { 20 | const anAtom = atomWithStorage(key, initialValue, storage) 21 | const derivedAtom = atom( 22 | (get) => get(anAtom), 23 | (get, set, nextValue?: boolean) => { 24 | const update = nextValue ?? !get(anAtom) 25 | void set(anAtom, update) 26 | }, 27 | ) 28 | 29 | return derivedAtom as WritableAtom 30 | } 31 | ``` 32 | 33 | And how it's used: 34 | 35 | ```js 36 | import { atomWithToggleAndStorage } from 'XXX' 37 | 38 | // will have an initial value set to false & get stored in localStorage under the key "isActive" 39 | const isActiveAtom = atomWithToggleAndStorage('isActive') 40 | ``` 41 | 42 | The usage in a component is also the same as `atomWithToggle`. 43 | -------------------------------------------------------------------------------- /docs/recipes/atom-with-toggle.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: atomWithToggle 3 | nav: 9.04 4 | keywords: creators,toggle 5 | --- 6 | 7 | > `atomWithToggle` creates a new atom with a boolean as initial state & a setter function to toggle it. 8 | 9 | This avoids the boilerplate of having to set up another atom just to update the state of the first. 10 | 11 | ```ts 12 | import { WritableAtom, atom } from 'jotai' 13 | 14 | export function atomWithToggle( 15 | initialValue?: boolean, 16 | ): WritableAtom { 17 | const anAtom = atom(initialValue, (get, set, nextValue?: boolean) => { 18 | const update = nextValue ?? !get(anAtom) 19 | set(anAtom, update) 20 | }) 21 | 22 | return anAtom as WritableAtom 23 | } 24 | ``` 25 | 26 | An optional initial state can be provided as the first argument. 27 | 28 | 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. 29 | 30 | Here is how it's used. 31 | 32 | ```js 33 | import { atomWithToggle } from 'XXX' 34 | 35 | // will have an initial value set to true 36 | const isActiveAtom = atomWithToggle(true) 37 | ``` 38 | 39 | And in a component: 40 | 41 | ```jsx 42 | const Toggle = () => { 43 | const [isActive, toggle] = useAtom(isActiveAtom) 44 | 45 | return ( 46 | <> 47 | 50 | 51 | 52 | 53 | ) 54 | } 55 | ``` 56 | -------------------------------------------------------------------------------- /docs/recipes/custom-useatom-hooks.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Custom useAtom hooks 3 | nav: 9.02 4 | keywords: custom,hook 5 | --- 6 | 7 | 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. 8 | 9 | ### utils 10 | 11 | #### useSelectAtom 12 | 13 | ```js 14 | import { useAtomValue } from 'jotai' 15 | import { selectAtom } from 'jotai/utils' 16 | 17 | export function useSelectAtom(anAtom, selector) { 18 | const selectorAtom = selectAtom( 19 | anAtom, 20 | selector, 21 | // Alternatively, you can customize `equalityFn` to determine when it will rerender 22 | // Check selectAtom's signature for details. 23 | ) 24 | return useAtomValue(selectorAtom) 25 | } 26 | 27 | // how to use it 28 | function useN(n) { 29 | const selector = useCallback((v) => v[n], [n]) 30 | return useSelectAtom(arrayAtom, selector) 31 | } 32 | ``` 33 | 34 | Please note that in this case `keyFn` must be stable, either define outside render or wrap with `useCallback`. 35 | 36 | #### useFreezeAtom 37 | 38 | ```js 39 | import { useAtom } from 'jotai' 40 | import { freezeAtom } from 'jotai/utils' 41 | 42 | export function useFreezeAtom(anAtom) { 43 | return useAtom(freezeAtom(anAtom)) 44 | } 45 | ``` 46 | 47 | #### useSplitAtom 48 | 49 | ```js 50 | import { useAtom } from 'jotai' 51 | import { splitAtom } from 'jotai/utils' 52 | 53 | export function useSplitAtom(anAtom) { 54 | return useAtom(splitAtom(anAtom)) 55 | } 56 | ``` 57 | 58 | ### extensions 59 | 60 | #### useFocusAtom 61 | 62 | ```js 63 | import { useAtom } from 'jotai' 64 | import { focusAtom } from 'jotai-optics' 65 | 66 | /* if an atom is created here, please use `useMemo(() => atom(initValue), [initValue])` instead. */ 67 | export function useFocusAtom(anAtom, keyFn) { 68 | return useAtom(focusAtom(anAtom, keyFn)) 69 | } 70 | 71 | // how to use it 72 | useFocusAtom(anAtom) { 73 | useMemo(() => atom(initValue), [initValue]), 74 | useCallback((optic) => optic.prop('key'), []) 75 | } 76 | ``` 77 | 78 | #### Stackblitz 79 | 80 | 81 | 82 | Please note that in this case `keyFn` must be stable, either define outside render or wrap with `useCallback`. 83 | -------------------------------------------------------------------------------- /docs/recipes/use-atom-effect.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: useAtomEffect 3 | nav: 9.03 4 | keywords: effect, atom effect, side effect, side-effect, sideeffect, hook, useAtomEffect 5 | --- 6 | 7 | > `useAtomEffect` runs side effects in response to changes in atoms or props using [atomEffect](../extensions/effect.mdx). 8 | 9 | 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. 10 | 11 | ⚠️ 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. 12 | 13 | ```ts 14 | import { useMemoOne as useStableMemo } from 'use-memo-one' 15 | import { useAtomValue } from 'jotai/react' 16 | import { atomEffect } from 'jotai-effect' 17 | 18 | type EffectFn = Parameters[0] 19 | 20 | export function useAtomEffect(effectFn: EffectFn) { 21 | useAtomValue(useStableMemo(() => atomEffect(effectFn), [effectFn])) 22 | } 23 | ``` 24 | 25 | ### Example Usage 26 | 27 | ```tsx 28 | import { useCallbackOne as useStableCallback } from 'use-memo-one' 29 | import { atom, useAtom } from 'jotai' 30 | import { atomFamily } from 'jotai/utils' 31 | import { useAtomEffect } from './useAtomEffect' 32 | 33 | const channelSubscriptionAtomFamily = atomFamily( 34 | (channelId: string) => { 35 | return atom(new Channel(channelId)) 36 | }, 37 | ) 38 | const messagesAtom = atom([]) 39 | 40 | function Messages({ channelId }: { channelId: string }) { 41 | const [messages] = useAtom(messagesAtom) 42 | useAtomEffect( 43 | useStableCallback( 44 | (get, set) => { 45 | const channel = get(channelSubscriptionAtomFamily(channelId)) 46 | const unsubscribe = channel.subscribe((message) => { 47 | set(messagesAtom, (prev) => [...prev, message]) 48 | }) 49 | return unsubscribe 50 | }, 51 | [channelId], 52 | ), 53 | ) 54 | return ( 55 | <> 56 |

You have {messages.length} messages

57 |
58 | {messages.map((message) => ( 59 |
{message.text}
60 | ))} 61 | 62 | ) 63 | } 64 | ``` 65 | -------------------------------------------------------------------------------- /docs/recipes/use-reducer-atom.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: useReducerAtom 3 | nav: 9.11 4 | keywords: reducer, hook, useReducerAtom 5 | --- 6 | 7 | `useReducerAtom` is a custom hook to apply a reducer to a primitive atom. 8 | 9 | It's useful to change the update behavior temporarily. 10 | Also, consider [atomWithReducer](../utilities/reducer.mdx) 11 | for the atom-level solution. 12 | 13 | ```ts 14 | import { useCallback } from 'react' 15 | import { useAtom } from 'jotai' 16 | import type { PrimitiveAtom } from 'jotai' 17 | 18 | export function useReducerAtom( 19 | anAtom: PrimitiveAtom, 20 | reducer: (v: Value, a: Action) => Value, 21 | ) { 22 | const [state, setState] = useAtom(anAtom) 23 | const dispatch = useCallback( 24 | (action: Action) => setState((prev) => reducer(prev, action)), 25 | [setState, reducer], 26 | ) 27 | return [state, dispatch] as const 28 | } 29 | ``` 30 | 31 | ### Example Usage 32 | 33 | ```jsx 34 | import { atom } from 'jotai' 35 | 36 | const countReducer = (prev, action) => { 37 | if (action.type === 'inc') return prev + 1 38 | if (action.type === 'dec') return prev - 1 39 | throw new Error('unknown action type') 40 | } 41 | 42 | const countAtom = atom(0) 43 | 44 | const Counter = () => { 45 | const [count, dispatch] = useReducerAtom(countAtom, countReducer) 46 | return ( 47 |
48 | {count} 49 | 50 | 51 |
52 | ) 53 | } 54 | ``` 55 | 56 | 57 | -------------------------------------------------------------------------------- /docs/third-party/history.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: History 3 | description: A Jōtai utility package for state history 4 | nav: 4.04 5 | keywords: history, undo, redo, track changes 6 | --- 7 | 8 | [jotai-history](https://github.com/jotaijs/jotai-history) is a utility package for tracking state history in Jotai. 9 | 10 | ## Installation 11 | 12 | ``` 13 | npm install jotai-history 14 | ``` 15 | 16 | ## `withHistory` 17 | 18 | ```js 19 | import { withHistory } from 'jotai-history' 20 | 21 | const targetAtom = atom(0) 22 | const limit = 2 23 | const historyAtom = withHistory(targetAtom, limit) 24 | 25 | function Component() { 26 | const [current, previous] = useAtomValue(historyAtom) 27 | ... 28 | } 29 | ``` 30 | 31 | ### Description 32 | 33 | `withHistory` creates an atom that tracks the history of states for a given `targetAtom`. The most recent `limit` states are retained. 34 | 35 | ### Action Symbols 36 | 37 | - **RESET** 38 | Clears the entire history, removing all previous states (including the undo/redo stack). 39 | 40 | ```js 41 | import { RESET } from 'jotai-history' 42 | 43 | ... 44 | 45 | function Component() { 46 | const setHistoryAtom = useSetAtom(historyAtom) 47 | ... 48 | setHistoryAtom(RESET) 49 | } 50 | ``` 51 | 52 | - **UNDO** and **REDO** 53 | Moves the `targetAtom` backward or forward in its history. 54 | 55 | ```js 56 | import { REDO, UNDO } from 'jotai-history' 57 | 58 | ... 59 | 60 | function Component() { 61 | const setHistoryAtom = useSetAtom(historyAtom) 62 | ... 63 | setHistoryAtom(UNDO) 64 | setHistoryAtom(REDO) 65 | } 66 | ``` 67 | 68 | ### Indicators 69 | 70 | - **canUndo** and **canRedo** 71 | Booleans indicating whether undo or redo actions are currently possible. These can be used to disable buttons or conditionally trigger actions. 72 | 73 | ```jsx 74 | ... 75 | 76 | function Component() { 77 | const history = useAtomValue(historyAtom) 78 | 79 | return ( 80 | <> 81 | 82 | 83 | 84 | ) 85 | } 86 | ``` 87 | 88 | 89 | 90 | ## Memory Management 91 | 92 | > 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. 93 | -------------------------------------------------------------------------------- /docs/utilities/callback.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Callback 3 | nav: 3.99 4 | keywords: callback 5 | published: false 6 | --- 7 | 8 | ## useAtomCallback 9 | 10 | Ref: https://github.com/pmndrs/jotai/issues/60 11 | 12 | ### Usage 13 | 14 | ```ts 15 | useAtomCallback( 16 | callback: (get: Getter, set: Setter, ...arg: Args) => Result, 17 | options?: Options 18 | ): (...args: Args) => Result 19 | ``` 20 | 21 | This hook is for interacting with atoms imperatively. 22 | It takes a callback function that works like atom write function, 23 | and returns a function that returns an atom value. 24 | 25 | The callback to pass in the hook must be stable (should be wrapped with useCallback). 26 | 27 | ### Examples 28 | 29 | ```jsx 30 | import { useEffect, useState, useCallback } from 'react' 31 | import { Provider, atom, useAtom } from 'jotai' 32 | import { useAtomCallback } from 'jotai/utils' 33 | 34 | const countAtom = atom(0) 35 | 36 | const Counter = () => { 37 | const [count, setCount] = useAtom(countAtom) 38 | return ( 39 | <> 40 | {count} 41 | 42 | ) 43 | } 44 | 45 | const Monitor = () => { 46 | const [count, setCount] = useState(0) 47 | const readCount = useAtomCallback( 48 | useCallback((get) => { 49 | const currCount = get(countAtom) 50 | setCount(currCount) 51 | return currCount 52 | }, []), 53 | ) 54 | useEffect(() => { 55 | const timer = setInterval(async () => { 56 | console.log(readCount()) 57 | }, 1000) 58 | return () => { 59 | clearInterval(timer) 60 | } 61 | }, [readCount]) 62 | return
current count: {count}
63 | } 64 | ``` 65 | 66 | ### Stackblitz 67 | 68 | 69 | -------------------------------------------------------------------------------- /docs/utilities/reducer.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Reducer 3 | nav: 3.99 4 | keywords: reducer,action,dispatch 5 | published: false 6 | --- 7 | 8 | ## atomWithReducer 9 | 10 | Ref: https://github.com/pmndrs/jotai/issues/38 11 | 12 | ```js 13 | import { atomWithReducer } from 'jotai/utils' 14 | 15 | const countReducer = (prev, action) => { 16 | if (action.type === 'inc') return prev + 1 17 | if (action.type === 'dec') return prev - 1 18 | throw new Error('unknown action type') 19 | } 20 | 21 | const countReducerAtom = atomWithReducer(0, countReducer) 22 | ``` 23 | 24 | ### Stackblitz 25 | 26 | 27 | 28 | ## useReducerAtom 29 | 30 | See [useReducerAtom](../recipes/use-reducer-atom.mdx) recipe. 31 | -------------------------------------------------------------------------------- /examples/hacker_news/README.md: -------------------------------------------------------------------------------- 1 | # 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) 2 | 3 | ## Description 4 | 5 | Demonstrate a news articles with jotai, hit next to see more articles. 6 | 7 | ## Set up locally 8 | 9 | ```bash 10 | git clone https://github.com/pmndrs/jotai 11 | 12 | # install project dependencies & build the library 13 | cd jotai && pnpm install 14 | 15 | # move to the examples folder & install dependencies 16 | cd examples/hacker_news && pnpm install 17 | 18 | # start the dev server 19 | pnpm dev 20 | ``` 21 | 22 | ## Set up on `StackBlitz` 23 | 24 | Link: https://stackblitz.com/github/pmndrs/jotai/tree/main/examples/hacker_news 25 | -------------------------------------------------------------------------------- /examples/hacker_news/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Jotai Examples | Hacker News 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/hacker_news/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hacker_news", 3 | "version": "2.0.0", 4 | "description": "Demonstrate a news articles with jotai, hit next to see more articles.", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "serve": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@react-spring/web": "^9.2.3", 13 | "html-react-parser": "^1.2.6", 14 | "jotai": "^2.10.4", 15 | "react": "^18.2.0", 16 | "react-dom": "^18.2.0" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "^18.2.0", 20 | "@types/react-dom": "^18.2.0", 21 | "@vitejs/plugin-react": "^4.3.4", 22 | "typescript": "^5.0.0", 23 | "vite": "^6.0.5" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/hacker_news/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 16 | 25 | Jotai Examples | Hacker News 26 | 27 | 28 | 29 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /examples/hacker_news/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from 'react' 2 | import { a, useSpring } from '@react-spring/web' 3 | import Parser from 'html-react-parser' 4 | import { Provider, atom, useAtom, useSetAtom } from 'jotai' 5 | 6 | type PostData = { 7 | by: string 8 | descendants?: number 9 | id: number 10 | kids?: number[] 11 | parent: number 12 | score?: number 13 | text?: string 14 | time: number 15 | title?: string 16 | type: 'comment' | 'story' 17 | url?: string 18 | } 19 | 20 | const postId = atom(9001) 21 | const postData = atom(async (get) => { 22 | const id = get(postId) 23 | const response = await fetch( 24 | `https://hacker-news.firebaseio.com/v0/item/${id}.json`, 25 | ) 26 | const data: PostData = await response.json() 27 | return data 28 | }) 29 | 30 | function Id() { 31 | const [id] = useAtom(postId) 32 | const props = useSpring({ from: { id }, id, reset: true }) 33 | return {props.id.to(Math.round)} 34 | } 35 | 36 | function Next() { 37 | // Use `useSetAtom` to avoid re-render 38 | // const [, setPostId] = useAtom(postId) 39 | const setPostId = useSetAtom(postId) 40 | return ( 41 | 44 | ) 45 | } 46 | 47 | function PostTitle() { 48 | const [{ by, text, time, title, url }] = useAtom(postData) 49 | return ( 50 | <> 51 |

{by}

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

{title}

} 54 | {url && {url}} 55 | {text &&
{Parser(text)}
} 56 | 57 | ) 58 | } 59 | 60 | export default function App() { 61 | return ( 62 | 63 | 64 |
65 | Loading...}> 66 | 67 | 68 |
69 | 70 |
71 | ) 72 | } 73 | -------------------------------------------------------------------------------- /examples/hacker_news/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import './styles.css' 4 | import App from './App' 5 | 6 | const rootElement = document.getElementById('root') 7 | createRoot(rootElement!).render( 8 | 9 | 10 | , 11 | ) 12 | -------------------------------------------------------------------------------- /examples/hacker_news/src/styles.css: -------------------------------------------------------------------------------- 1 | @import url('https://rsms.me/inter/inter.css'); 2 | 3 | * { 4 | box-sizing: border-box; 5 | outline: none !important; 6 | } 7 | 8 | html, 9 | body, 10 | #root { 11 | width: 100%; 12 | height: 100%; 13 | margin: 0; 14 | padding: 0; 15 | } 16 | 17 | body { 18 | background: white; 19 | color: black; 20 | font-family: 'Inter', sans-serif; 21 | -webkit-font-smoothing: antialiased; 22 | -moz-osx-font-smoothing: grayscale; 23 | } 24 | 25 | #root { 26 | display: grid; 27 | grid-template-columns: auto 1fr auto; 28 | } 29 | 30 | h1 { 31 | writing-mode: tb-rl; 32 | font-variant-numeric: tabular-nums; 33 | font-weight: 700; 34 | font-size: 10em; 35 | letter-spacing: -10px; 36 | text-align: left; 37 | margin: 0; 38 | padding: 50px 0px 0px 20px; 39 | } 40 | 41 | h2 { 42 | margin-bottom: 0.2em; 43 | } 44 | 45 | h4 { 46 | font-weight: 500; 47 | } 48 | 49 | h6 { 50 | margin-top: 0; 51 | } 52 | 53 | #root > div { 54 | padding: 50px 20px; 55 | overflow: hidden; 56 | word-wrap: break-word; 57 | position: relative; 58 | } 59 | 60 | #root > div > div { 61 | position: absolute; 62 | } 63 | 64 | p { 65 | color: #474747; 66 | } 67 | 68 | button { 69 | text-decoration: none; 70 | background: transparent; 71 | border: none; 72 | cursor: pointer; 73 | font-family: 'Inter', sans-serif; 74 | font-weight: 200; 75 | font-size: 6em; 76 | padding: 0px 30px 20px 0px; 77 | display: flex; 78 | align-items: flex-end; 79 | color: inherit; 80 | } 81 | 82 | button:focus { 83 | outline: 0; 84 | } 85 | 86 | a { 87 | color: inherit; 88 | } 89 | -------------------------------------------------------------------------------- /examples/hacker_news/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/hacker_news/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "strict": true, 5 | "esModuleInterop": true, 6 | "allowSyntheticDefaultImports": true, 7 | "moduleResolution": "node", 8 | "lib": ["dom", "dom.iterable", "esnext"], 9 | "jsx": "react-jsx", 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "module": "esnext", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true 18 | }, 19 | "include": ["./src/**/*"], 20 | "exclude": ["node_modules"] 21 | } 22 | -------------------------------------------------------------------------------- /examples/hacker_news/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | export default defineConfig({ 5 | plugins: [react()], 6 | }) 7 | -------------------------------------------------------------------------------- /examples/hello/README.md: -------------------------------------------------------------------------------- 1 | # 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) 2 | 3 | ## Set up locally 4 | 5 | ```bash 6 | git clone https://github.com/pmndrs/jotai 7 | 8 | # install project dependencies & build the library 9 | cd jotai && pnpm install 10 | 11 | # move to the examples folder & install dependencies 12 | cd examples/hello && pnpm install 13 | 14 | # start the dev server 15 | pnpm dev 16 | ``` 17 | 18 | ## Set up on `StackBlitz` 19 | 20 | Link: https://stackblitz.com/github/pmndrs/jotai/tree/main/examples/hello 21 | -------------------------------------------------------------------------------- /examples/hello/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | Jotai Examples | Hello 13 | 14 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/hello/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello", 3 | "version": "2.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "serve": "vite preview" 10 | }, 11 | "dependencies": { 12 | "jotai": "^2.10.4", 13 | "prismjs": "^1.23.0", 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0", 16 | "react-prism": "4.3.2" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "^18.2.0", 20 | "@types/react-dom": "^18.2.0", 21 | "@vitejs/plugin-react": "^4.3.4", 22 | "typescript": "^5.0.0", 23 | "vite": "^6.0.5" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/hello/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | Jotai Examples | Hello 15 | 16 | 17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /examples/hello/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { atom, useAtom } from 'jotai' 2 | // @ts-ignore 3 | import PrismCode from 'react-prism' 4 | import 'prismjs' 5 | import 'prismjs/components/prism-jsx.min' 6 | 7 | const textAtom = atom('hello') 8 | const uppercaseAtom = atom((get) => get(textAtom).toUpperCase()) 9 | 10 | const Input = () => { 11 | const [text, setText] = useAtom(textAtom) 12 | return ( 13 | setText(e.target.value)} 17 | /> 18 | ) 19 | } 20 | 21 | const Uppercase = () => { 22 | const [uppercase] = useAtom(uppercaseAtom) 23 | return <>{uppercase} 24 | } 25 | 26 | const code = `import { atom, useAtom } from 'jotai' 27 | 28 | // Create your atoms and derivatives 29 | const textAtom = atom('hello') 30 | const uppercaseAtom = atom((get) => get(textAtom).toUpperCase()) 31 | 32 | // Use them anywhere in your app 33 | const Input = () => { 34 | const [text, setText] = useAtom(textAtom) 35 | return setText(e.target.value)} /> 36 | } 37 | 38 | const Uppercase = () => { 39 | const [uppercase] = useAtom(uppercaseAtom) 40 | return
Uppercase: {uppercase}
41 | } 42 | 43 | // Now you have the components 44 | const MyApp = () => ( 45 |
46 | 47 | 48 |
49 | ) 50 | ` 51 | 52 | const App = () => ( 53 |
54 |

A simple example:

55 |
56 |
57 |
58 | 59 |
60 | 61 |
62 |
63 |
64 |
65 | 66 |
67 |
68 |
69 | ) 70 | 71 | export default App 72 | -------------------------------------------------------------------------------- /examples/hello/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client' 2 | import App from './App' 3 | import './prism.css' 4 | import './style.css' 5 | 6 | const root = document.getElementById('root') 7 | 8 | createRoot(root!).render( 9 |
10 |
11 |

12 | Jōtai 13 |

14 |

20 |
Primitive and flexible state management for React.
21 |
状態
22 |

23 |
24 | 25 |
, 26 | ) 27 | -------------------------------------------------------------------------------- /examples/hello/src/style.css: -------------------------------------------------------------------------------- 1 | @import url('https://rsms.me/inter/inter.css'); 2 | html { 3 | font-family: 'Inter', sans-serif; 4 | } 5 | 6 | @supports (font-variation-settings: normal) { 7 | html { 8 | font-family: 'Inter var', sans-serif; 9 | } 10 | } 11 | 12 | * { 13 | box-sizing: border-box; 14 | } 15 | 16 | ::selection { 17 | background: #212121; 18 | color: white; 19 | } 20 | 21 | html, 22 | body { 23 | overflow-x: hidden; 24 | } 25 | 26 | pre { 27 | font-size: 0.8em; 28 | margin-left: -2.5rem !important; 29 | margin-right: -2.5rem !important; 30 | width: calc(100% + 5rem); 31 | padding: 3em !important; 32 | border: 1px solid #eee !important; 33 | border-radius: 4px; 34 | } 35 | 36 | .src a * { 37 | opacity: 0.5; 38 | display: inline-block; 39 | margin: 10px 5px; 40 | } 41 | 42 | @media screen and (min-width: 800px) { 43 | pre { 44 | width: 100% !important; 45 | margin: 0 !important; 46 | } 47 | } 48 | 49 | pre > span:nth-child(11), 50 | pre > span:nth-child(17), 51 | pre > span:nth-child(20), 52 | pre > span:nth-child(23), 53 | pre > span:nth-child(44), 54 | pre > span:nth-child(61), 55 | pre > span:nth-child(78), 56 | pre > span:nth-child(79) > span > span.class-name, 57 | pre > span:nth-child(85) > span > span.class-name { 58 | color: #ff7bab !important; 59 | } 60 | -------------------------------------------------------------------------------- /examples/hello/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/hello/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "strict": true, 5 | "esModuleInterop": true, 6 | "allowSyntheticDefaultImports": true, 7 | "moduleResolution": "node", 8 | "lib": ["dom", "dom.iterable", "esnext"], 9 | "jsx": "react-jsx", 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "module": "esnext", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true 18 | }, 19 | "include": ["./src/**/*"], 20 | "exclude": ["node_modules"] 21 | } 22 | -------------------------------------------------------------------------------- /examples/hello/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | export default defineConfig({ 5 | plugins: [react()], 6 | }) 7 | -------------------------------------------------------------------------------- /examples/mega-form/README.md: -------------------------------------------------------------------------------- 1 | # 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) 2 | 3 | ## Set up locally 4 | 5 | ```bash 6 | git clone https://github.com/pmndrs/jotai 7 | 8 | # install project dependencies & build the library 9 | cd jotai && pnpm install 10 | 11 | # move to the examples folder & install dependencies 12 | cd examples/mega-form && pnpm install 13 | 14 | # start the dev server 15 | pnpm dev 16 | ``` 17 | 18 | ## Set up on `StackBlitz` 19 | 20 | Link: https://stackblitz.com/github/pmndrs/jotai/tree/main/examples/mega-form 21 | -------------------------------------------------------------------------------- /examples/mega-form/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Jotai Examples | Mega Form 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/mega-form/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mega-form", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "tsc && vite build", 8 | "serve": "vite preview" 9 | }, 10 | "dependencies": { 11 | "fp-ts": "^2.9.5", 12 | "io-ts": "^2.2.15", 13 | "jotai": "^2.10.4", 14 | "jotai-optics": "^0.4.0", 15 | "optics-ts": "^2.0.0", 16 | "react": "^18.2.0", 17 | "react-dom": "^18.2.0" 18 | }, 19 | "devDependencies": { 20 | "@types/react": "^18.2.0", 21 | "@types/react-dom": "^18.2.0", 22 | "@vitejs/plugin-react": "^4.3.4", 23 | "typescript": "^5.0.0", 24 | "vite": "^6.0.5" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/mega-form/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Jotai Examples | Mega Form 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/mega-form/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client' 2 | import App from './App' 3 | import './style.css' 4 | 5 | const rootElement = document.getElementById('root') 6 | createRoot(rootElement!).render() 7 | -------------------------------------------------------------------------------- /examples/mega-form/src/initialValue.ts: -------------------------------------------------------------------------------- 1 | const initialValue: Record> = { 2 | form1: { task: 'Eat some food', checked: 'yeah' }, 3 | form2: { task: 'Eat some food', checked: 'yeah' }, 4 | form3: { task: 'Eat some food', checked: 'yeah' }, 5 | form4: { task: 'Eat some food', checked: 'yeah' }, 6 | form5: { task: 'Eat some food', checked: 'yeah' }, 7 | form6: { task: 'Eat some food', checked: 'yeah' }, 8 | form7: { task: 'Eat some food', checked: 'yeah' }, 9 | form8: { task: 'Eat some food', checked: 'yeah' }, 10 | form12: { task: 'Eat some food', checked: 'yeah' }, 11 | form22: { task: 'Eat some food', checked: 'yeah' }, 12 | form32: { task: 'Eat some food', checked: 'yeah' }, 13 | form42: { task: 'Eat some food', checked: 'yeah' }, 14 | form52: { task: 'Eat some food', checked: 'yeah' }, 15 | form62: { task: 'Eat some food', checked: 'yeah' }, 16 | form72: { task: 'Eat some food', checked: 'yeah' }, 17 | form82: { task: 'Eat some food', checked: 'yeah' }, 18 | form14: { task: 'Eat some food', checked: 'yeah' }, 19 | form24: { task: 'Eat some food', checked: 'yeah' }, 20 | form34: { task: 'Eat some food', checked: 'yeah' }, 21 | form44: { task: 'Eat some food', checked: 'yeah' }, 22 | form54: { task: 'Eat some food', checked: 'yeah' }, 23 | form64: { task: 'Eat some food', checked: 'yeah' }, 24 | form74: { task: 'Eat some food', checked: 'yeah' }, 25 | form84: { task: 'Eat some food', checked: 'yeah' }, 26 | form15: { task: 'Eat some food', checked: 'yeah' }, 27 | form25: { task: 'Eat some food', checked: 'yeah' }, 28 | form35: { task: 'Eat some food', checked: 'yeah' }, 29 | form45: { task: 'Eat some food', checked: 'yeah' }, 30 | form55: { task: 'Eat some food', checked: 'yeah' }, 31 | form65: { task: 'Eat some food', checked: 'yeah' }, 32 | form75: { task: 'Eat some food', checked: 'yeah' }, 33 | form85: { task: 'Eat some food', checked: 'yeah' }, 34 | } 35 | 36 | export default initialValue 37 | -------------------------------------------------------------------------------- /examples/mega-form/src/style.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | position: relative; 4 | width: 100%; 5 | height: 100%; 6 | } 7 | 8 | body { 9 | color: #333; 10 | margin: 0; 11 | padding: 8px; 12 | box-sizing: border-box; 13 | font-family: 14 | -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, 15 | Cantarell, 'Helvetica Neue', sans-serif; 16 | } 17 | 18 | a { 19 | color: rgb(0, 100, 200); 20 | text-decoration: none; 21 | } 22 | 23 | a:hover { 24 | text-decoration: underline; 25 | } 26 | 27 | a:visited { 28 | color: rgb(0, 80, 160); 29 | } 30 | 31 | label { 32 | display: block; 33 | } 34 | 35 | input, 36 | button, 37 | select, 38 | textarea { 39 | font-family: inherit; 40 | font-size: inherit; 41 | -webkit-padding: 0.4em 0; 42 | padding: 0.4em; 43 | margin: 0 0 0.5em 0; 44 | box-sizing: border-box; 45 | border: 1px solid #ccc; 46 | border-radius: 2px; 47 | } 48 | 49 | input:disabled { 50 | color: #ccc; 51 | } 52 | 53 | button { 54 | color: #333; 55 | background-color: #f4f4f4; 56 | outline: none; 57 | } 58 | 59 | button:disabled { 60 | color: #999; 61 | } 62 | 63 | button:not(:disabled):active { 64 | background-color: #ddd; 65 | } 66 | 67 | button:focus { 68 | border-color: #666; 69 | } 70 | -------------------------------------------------------------------------------- /examples/mega-form/src/useAtomSlice.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | import { useAtom } from 'jotai' 3 | import type { PrimitiveAtom } from 'jotai' 4 | import { splitAtom } from 'jotai/utils' 5 | 6 | const useAtomSlice = (arrAtom: PrimitiveAtom) => { 7 | const [atoms, dispatch] = useAtom( 8 | useMemo(() => splitAtom(arrAtom), [arrAtom]), 9 | ) 10 | return useMemo( 11 | () => 12 | atoms.map( 13 | (itemAtom) => 14 | [ 15 | itemAtom, 16 | () => dispatch({ type: 'remove', atom: itemAtom }), 17 | ] as const, 18 | ), 19 | [atoms, dispatch], 20 | ) 21 | } 22 | 23 | export default useAtomSlice 24 | -------------------------------------------------------------------------------- /examples/mega-form/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/mega-form/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "strict": true, 5 | "esModuleInterop": true, 6 | "allowSyntheticDefaultImports": true, 7 | "moduleResolution": "node", 8 | "lib": ["dom", "dom.iterable", "esnext"], 9 | "jsx": "react-jsx", 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "module": "esnext", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true 18 | }, 19 | "include": ["./src/**/*"], 20 | "exclude": ["node_modules"] 21 | } 22 | -------------------------------------------------------------------------------- /examples/mega-form/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | export default defineConfig({ 5 | plugins: [react()], 6 | }) 7 | -------------------------------------------------------------------------------- /examples/starter/README.md: -------------------------------------------------------------------------------- 1 | # 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) 2 | 3 | ## Set up locally 4 | 5 | ```bash 6 | git clone https://github.com/pmndrs/jotai 7 | 8 | # install project dependencies & build the library 9 | cd jotai && pnpm install 10 | 11 | # move to the examples folder & install dependencies 12 | cd examples/starter && pnpm install 13 | 14 | # start the dev server 15 | pnpm dev 16 | ``` 17 | 18 | ## Set up on `StackBlitz` 19 | 20 | Link: https://stackblitz.com/github/pmndrs/jotai/tree/main/examples/starter 21 | -------------------------------------------------------------------------------- /examples/starter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Jotai Examples | Starter 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/starter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "starter", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "jotai": "^2.10.4", 13 | "react": "^18.3.1", 14 | "react-dom": "^18.3.1" 15 | }, 16 | "devDependencies": { 17 | "@types/react": "^18.2.0", 18 | "@types/react-dom": "^18.2.0", 19 | "@vitejs/plugin-react": "^4.3.4", 20 | "typescript": "^5.0.0", 21 | "vite": "^6.0.5" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/starter/src/assets/jotai-mascot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/jotai/f0e3d953d6f75e7bdd0a0d18360c471bc64f4058/examples/starter/src/assets/jotai-mascot.png -------------------------------------------------------------------------------- /examples/starter/src/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | #root { 4 | height: 100%; 5 | } 6 | 7 | #root { 8 | display: flex; 9 | place-items: center; 10 | justify-content: center; 11 | 12 | color: #fff; 13 | background-color: hsl(0, 0%, 4%); 14 | } 15 | -------------------------------------------------------------------------------- /examples/starter/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import { atom, useAtom } from 'jotai' 4 | 5 | import mascot from './assets/jotai-mascot.png' 6 | 7 | import './index.css' 8 | 9 | const countAtom = atom(0) 10 | 11 | const Counter = () => { 12 | const [count, setCount] = useAtom(countAtom) 13 | const inc = () => setCount((c) => c + 1) 14 | 15 | return ( 16 | <> 17 | {count} 18 | 24 | 25 | ) 26 | } 27 | 28 | function App() { 29 | return ( 30 |
31 | 32 | Jotai mascot 40 | 41 | 42 |

Jotai Starter

43 | 44 | 45 |
46 | ) 47 | } 48 | 49 | createRoot(document.getElementById('root')!).render( 50 | 51 | 52 | , 53 | ) 54 | -------------------------------------------------------------------------------- /examples/starter/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/starter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "strict": true, 5 | "esModuleInterop": true, 6 | "allowSyntheticDefaultImports": true, 7 | "moduleResolution": "node", 8 | "lib": ["dom", "dom.iterable", "esnext"], 9 | "jsx": "react-jsx", 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "module": "esnext", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true 18 | }, 19 | "include": ["vite.config.ts", "./src/**/*"], 20 | "exclude": ["node_modules"] 21 | } 22 | -------------------------------------------------------------------------------- /examples/starter/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react' 2 | import { defineConfig } from 'vite' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /examples/text_length/README.md: -------------------------------------------------------------------------------- 1 | # 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) 2 | 3 | ## Description 4 | 5 | Count the length and show the uppercase of any text. 6 | 7 | ## Set up locally 8 | 9 | ```bash 10 | git clone https://github.com/pmndrs/jotai 11 | 12 | # install project dependencies & build the library 13 | cd jotai && pnpm install 14 | 15 | # move to the examples folder & install dependencies 16 | cd examples/text_length && pnpm install 17 | 18 | # start the dev server 19 | pnpm dev 20 | ``` 21 | 22 | ## Set up on `StackBlitz` 23 | 24 | Link: https://stackblitz.com/github/pmndrs/jotai/tree/main/examples/text_length 25 | -------------------------------------------------------------------------------- /examples/text_length/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Jotai Examples | Text Length 7 | 18 | 19 | 20 |
21 |
22 |
REACT SPRING
23 |
24 |
25 | GitHub 28 |
29 | 30 |
31 |
Code
32 |
Castle
33 |
34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /examples/text_length/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "text_length", 3 | "version": "2.0.0", 4 | "description": "Count the length and show the uppercase of any text", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "serve": "vite preview" 10 | }, 11 | "dependencies": { 12 | "jotai": "^2.10.4", 13 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0" 15 | }, 16 | "devDependencies": { 17 | "@types/react": "^18.2.0", 18 | "@types/react-dom": "^18.2.0", 19 | "@vitejs/plugin-react": "^4.3.4", 20 | "typescript": "^5.0.0", 21 | "vite": "^6.0.5" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/text_length/public/castle.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/jotai/f0e3d953d6f75e7bdd0a0d18360c471bc64f4058/examples/text_length/public/castle.jpg -------------------------------------------------------------------------------- /examples/text_length/public/snippet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/jotai/f0e3d953d6f75e7bdd0a0d18360c471bc64f4058/examples/text_length/public/snippet.png -------------------------------------------------------------------------------- /examples/text_length/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Provider, atom, useAtom } from 'jotai' 2 | 3 | const textAtom = atom('hello') 4 | const textLenAtom = atom((get) => get(textAtom).length) 5 | const uppercaseAtom = atom((get) => get(textAtom).toUpperCase()) 6 | 7 | const Input = () => { 8 | const [text, setText] = useAtom(textAtom) 9 | return setText(e.target.value)} /> 10 | } 11 | 12 | const CharCount = () => { 13 | const [len] = useAtom(textLenAtom) 14 | return
Length: {len}
15 | } 16 | 17 | const Uppercase = () => { 18 | const [uppercase] = useAtom(uppercaseAtom) 19 | return
Uppercase: {uppercase}
20 | } 21 | 22 | const App = () => ( 23 | 24 | 25 | 26 | 27 | 28 | ) 29 | 30 | export default App 31 | -------------------------------------------------------------------------------- /examples/text_length/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import App from './App' 4 | 5 | const rootElement = document.getElementById('root') 6 | createRoot(rootElement!).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /examples/text_length/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/text_length/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "strict": true, 5 | "esModuleInterop": true, 6 | "allowSyntheticDefaultImports": true, 7 | "moduleResolution": "node", 8 | "lib": ["dom", "dom.iterable", "esnext"], 9 | "jsx": "react-jsx", 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "module": "esnext", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true 18 | }, 19 | "include": ["./src/**/*"], 20 | "exclude": ["node_modules"] 21 | } 22 | -------------------------------------------------------------------------------- /examples/text_length/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | export default defineConfig({ 5 | plugins: [react()], 6 | }) 7 | -------------------------------------------------------------------------------- /examples/todos/README.md: -------------------------------------------------------------------------------- 1 | # 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) 2 | 3 | ## Description 4 | 5 | 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. 6 | 7 | ## Set up locally 8 | 9 | ```bash 10 | git clone https://github.com/pmndrs/jotai 11 | 12 | # install project dependencies & build the library 13 | cd jotai && pnpm install 14 | 15 | # move to the examples folder & install dependencies 16 | cd examples/todos && pnpm install 17 | 18 | # start the dev server 19 | pnpm dev 20 | ``` 21 | 22 | ## Set up on `StackBlitz` 23 | 24 | Link: https://stackblitz.com/github/pmndrs/jotai/tree/main/examples/todos 25 | -------------------------------------------------------------------------------- /examples/todos/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Jotai Examples | Todos 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/todos/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todos", 3 | "version": "2.0.0", 4 | "description": "Record your todo list by typing them into this app", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "serve": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@ant-design/icons": "^5.5.2", 13 | "@react-spring/web": "^9.2.3", 14 | "antd": "^4.16.2", 15 | "jotai": "^2.10.4", 16 | "react": "^18.2.0", 17 | "react-dom": "^18.2.0" 18 | }, 19 | "devDependencies": { 20 | "@types/react": "^18.2.0", 21 | "@types/react-dom": "^18.2.0", 22 | "@vitejs/plugin-react": "^4.3.4", 23 | "typescript": "^5.0.0", 24 | "vite": "^6.0.5" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/todos/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 16 | 25 | Jotai Examples | Todos 26 | 27 | 28 | 29 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /examples/todos/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client' 2 | import 'antd/dist/antd.css' 3 | import './styles.css' 4 | import App from './App' 5 | 6 | const rootElement = document.getElementById('root') 7 | createRoot(rootElement!).render() 8 | -------------------------------------------------------------------------------- /examples/todos/src/styles.css: -------------------------------------------------------------------------------- 1 | @import url('https://rsms.me/inter/inter.css'); 2 | 3 | * { 4 | box-sizing: border-box; 5 | } 6 | 7 | html, 8 | body { 9 | width: 100%; 10 | height: 100%; 11 | } 12 | 13 | body { 14 | margin-top: 5em; 15 | display: flex; 16 | align-items: flex-start; 17 | justify-content: center; 18 | background: #fdfdfd; 19 | font-family: 'Inter', sans-serif !important; 20 | -webkit-font-smoothing: antialiased; 21 | -moz-osx-font-smoothing: grayscale; 22 | filter: saturate(0); 23 | } 24 | 25 | #root { 26 | width: 50ch; 27 | display: flex; 28 | flex-direction: column; 29 | gap: 1em; 30 | } 31 | 32 | input:not([type='checkbox']) { 33 | width: 100%; 34 | border: none; 35 | box-shadow: 0px 15px 30px rgba(0, 0, 0, 0.05); 36 | padding: 10px 20px; 37 | margin-top: 2em; 38 | margin-bottom: 4em; 39 | background: white; 40 | } 41 | 42 | input:focus { 43 | outline: none; 44 | } 45 | 46 | .anticon-close { 47 | width: 32px !important; 48 | cursor: pointer; 49 | color: #c0c0c0; 50 | } 51 | 52 | .anticon-close:hover { 53 | color: #272730; 54 | } 55 | 56 | .item { 57 | position: relative; 58 | display: flex; 59 | width: 100%; 60 | align-items: center; 61 | justify-content: space-between; 62 | gap: 20px; 63 | overflow: hidden; 64 | } 65 | 66 | .item > span { 67 | display: inline-block; 68 | width: 100%; 69 | } 70 | 71 | h1 { 72 | font-size: 10em; 73 | font-weight: 800; 74 | margin: 0; 75 | padding: 0; 76 | letter-spacing: -5px; 77 | color: black; 78 | white-space: nowrap; 79 | } 80 | -------------------------------------------------------------------------------- /examples/todos/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/todos/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "strict": true, 5 | "esModuleInterop": true, 6 | "allowSyntheticDefaultImports": true, 7 | "moduleResolution": "node", 8 | "lib": ["dom", "dom.iterable", "esnext"], 9 | "jsx": "react-jsx" 10 | }, 11 | "include": ["./src/**/*"], 12 | "exclude": ["node_modules"] 13 | } 14 | -------------------------------------------------------------------------------- /examples/todos/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | export default defineConfig({ 5 | plugins: [react()], 6 | }) 7 | -------------------------------------------------------------------------------- /examples/todos_with_atomFamily/README.md: -------------------------------------------------------------------------------- 1 | # 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) 2 | 3 | ## Description 4 | 5 | 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`. 6 | 7 | ## Set up locally 8 | 9 | ```bash 10 | git clone https://github.com/pmndrs/jotai 11 | 12 | # install project dependencies & build the library 13 | cd jotai && pnpm install 14 | 15 | # move to the examples folder & install dependencies 16 | cd examples/todos_with_atomFamily && pnpm install 17 | 18 | # start the dev server 19 | pnpm dev 20 | ``` 21 | 22 | ## Set up on `StackBlitz` 23 | 24 | Link: https://stackblitz.com/github/pmndrs/jotai/tree/main/examples/todos_with_atomFamily 25 | -------------------------------------------------------------------------------- /examples/todos_with_atomFamily/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Jotai Examples | Todos with atomFamily 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/todos_with_atomFamily/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todos_with_atomFamily", 3 | "version": "2.0.0", 4 | "description": "Implement a todo list using atomFamily and localStorage", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "serve": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@ant-design/icons": "^5.5.2", 13 | "@react-spring/web": "^9.2.3", 14 | "antd": "^4.16.2", 15 | "jotai": "^2.10.4", 16 | "nanoid": "^3.1.23", 17 | "react": "^18.2.0", 18 | "react-dom": "^18.2.0" 19 | }, 20 | "devDependencies": { 21 | "@types/react": "^18.2.0", 22 | "@types/react-dom": "^18.2.0", 23 | "@vitejs/plugin-react": "^4.3.4", 24 | "typescript": "^5.0.0", 25 | "vite": "^6.0.5" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/todos_with_atomFamily/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 16 | 25 | Jotai Examples | Todos with atomFamily 26 | 27 | 28 | 29 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /examples/todos_with_atomFamily/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client' 2 | import 'antd/dist/antd.css' 3 | import './styles.css' 4 | import App from './App' 5 | 6 | const rootElement = document.getElementById('root') 7 | createRoot(rootElement!).render() 8 | -------------------------------------------------------------------------------- /examples/todos_with_atomFamily/src/styles.css: -------------------------------------------------------------------------------- 1 | @import url('https://rsms.me/inter/inter.css'); 2 | 3 | * { 4 | box-sizing: border-box; 5 | } 6 | 7 | html, 8 | body { 9 | width: 100%; 10 | height: 100%; 11 | } 12 | 13 | body { 14 | margin-top: 5em; 15 | display: flex; 16 | align-items: flex-start; 17 | justify-content: center; 18 | background: #fdfdfd; 19 | font-family: 'Inter', sans-serif !important; 20 | -webkit-font-smoothing: antialiased; 21 | -moz-osx-font-smoothing: grayscale; 22 | filter: saturate(0); 23 | } 24 | 25 | #root { 26 | width: 50ch; 27 | display: flex; 28 | flex-direction: column; 29 | gap: 1em; 30 | } 31 | 32 | input:not([type='checkbox']) { 33 | width: 100%; 34 | border: none; 35 | box-shadow: 0px 15px 30px rgba(0, 0, 0, 0.05); 36 | padding: 10px 20px; 37 | margin-top: 2em; 38 | margin-bottom: 4em; 39 | background: white; 40 | } 41 | 42 | input:focus { 43 | outline: none; 44 | } 45 | 46 | .anticon-close { 47 | width: 32px !important; 48 | cursor: pointer; 49 | color: #c0c0c0; 50 | } 51 | 52 | .anticon-close:hover { 53 | color: #272730; 54 | } 55 | 56 | .item { 57 | position: relative; 58 | display: flex; 59 | width: 100%; 60 | align-items: center; 61 | justify-content: space-between; 62 | gap: 20px; 63 | overflow: hidden; 64 | } 65 | 66 | .item > span { 67 | display: inline-block; 68 | width: 100%; 69 | } 70 | 71 | h1 { 72 | font-size: 10em; 73 | font-weight: 800; 74 | margin: 0; 75 | padding: 0; 76 | letter-spacing: -5px; 77 | color: black; 78 | white-space: nowrap; 79 | } 80 | -------------------------------------------------------------------------------- /examples/todos_with_atomFamily/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/todos_with_atomFamily/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "strict": true, 5 | "esModuleInterop": true, 6 | "allowSyntheticDefaultImports": true, 7 | "moduleResolution": "node", 8 | "lib": ["dom", "dom.iterable", "esnext"], 9 | "jsx": "react-jsx", 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "module": "esnext", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true 18 | }, 19 | "include": ["./src/**/*"], 20 | "exclude": ["node_modules"] 21 | } 22 | -------------------------------------------------------------------------------- /examples/todos_with_atomFamily/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | export default defineConfig({ 5 | plugins: [react()], 6 | }) 7 | -------------------------------------------------------------------------------- /img/jotai-course-banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/jotai/f0e3d953d6f75e7bdd0a0d18360c471bc64f4058/img/jotai-course-banner.jpg -------------------------------------------------------------------------------- /img/jotai-header-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/jotai/f0e3d953d6f75e7bdd0a0d18360c471bc64f4058/img/jotai-header-dark.png -------------------------------------------------------------------------------- /img/jotai-header-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/jotai/f0e3d953d6f75e7bdd0a0d18360c471bc64f4058/img/jotai-header-light.png -------------------------------------------------------------------------------- /img/jotai-mascot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/jotai/f0e3d953d6f75e7bdd0a0d18360c471bc64f4058/img/jotai-mascot.png -------------------------------------------------------------------------------- /img/jotai-opengraph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/jotai/f0e3d953d6f75e7bdd0a0d18360c471bc64f4058/img/jotai-opengraph.png -------------------------------------------------------------------------------- /src/babel/plugin-debug-label.ts: -------------------------------------------------------------------------------- 1 | import babel from '@babel/core' 2 | import type { PluginObj } from '@babel/core' 3 | import _templateBuilder from '@babel/template' 4 | import { isAtom } from './utils.ts' 5 | import type { PluginOptions } from './utils.ts' 6 | 7 | const templateBuilder = (_templateBuilder as any).default || _templateBuilder 8 | 9 | export default function debugLabelPlugin( 10 | { types: t }: typeof babel, 11 | options?: PluginOptions, 12 | ): PluginObj { 13 | return { 14 | visitor: { 15 | ExportDefaultDeclaration(nodePath, state) { 16 | const { node } = nodePath 17 | if ( 18 | t.isCallExpression(node.declaration) && 19 | isAtom(t, node.declaration.callee, options?.customAtomNames) 20 | ) { 21 | const filename = (state.filename || 'unknown').replace(/\.\w+$/, '') 22 | 23 | let displayName = filename.split('/').pop()! 24 | 25 | // ./{module name}/index.js 26 | if (displayName === 'index') { 27 | displayName = 28 | filename.slice(0, -'/index'.length).split('/').pop() || 'unknown' 29 | } 30 | // Relies on visiting the variable declaration to add the debugLabel 31 | const buildExport = templateBuilder(` 32 | const %%atomIdentifier%% = %%atom%%; 33 | export default %%atomIdentifier%% 34 | `) 35 | const ast = buildExport({ 36 | atomIdentifier: t.identifier(displayName), 37 | atom: node.declaration, 38 | }) 39 | nodePath.replaceWithMultiple(ast as babel.Node[]) 40 | } 41 | }, 42 | VariableDeclarator(path) { 43 | if ( 44 | t.isIdentifier(path.node.id) && 45 | t.isCallExpression(path.node.init) && 46 | isAtom(t, path.node.init.callee, options?.customAtomNames) 47 | ) { 48 | path.parentPath.insertAfter( 49 | t.expressionStatement( 50 | t.assignmentExpression( 51 | '=', 52 | t.memberExpression( 53 | t.identifier(path.node.id.name), 54 | t.identifier('debugLabel'), 55 | ), 56 | t.stringLiteral(path.node.id.name), 57 | ), 58 | ), 59 | ) 60 | } 61 | }, 62 | }, 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/babel/preset.ts: -------------------------------------------------------------------------------- 1 | import babel from '@babel/core' 2 | import pluginDebugLabel from './plugin-debug-label.ts' 3 | import pluginReactRefresh from './plugin-react-refresh.ts' 4 | import type { PluginOptions } from './utils.ts' 5 | 6 | export default function jotaiPreset( 7 | _: typeof babel, 8 | options?: PluginOptions, 9 | ): { plugins: babel.PluginItem[] } { 10 | return { 11 | plugins: [ 12 | [pluginDebugLabel, options], 13 | [pluginReactRefresh, options], 14 | ], 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/babel/utils.ts: -------------------------------------------------------------------------------- 1 | import { types } from '@babel/core' 2 | 3 | export interface PluginOptions { 4 | customAtomNames?: string[] 5 | } 6 | 7 | export function isAtom( 8 | t: typeof types, 9 | callee: babel.types.Expression | babel.types.V8IntrinsicIdentifier, 10 | customAtomNames: PluginOptions['customAtomNames'] = [], 11 | ): boolean { 12 | const atomNames = [...atomFunctionNames, ...customAtomNames] 13 | if (t.isIdentifier(callee) && atomNames.includes(callee.name)) { 14 | return true 15 | } 16 | 17 | if (t.isMemberExpression(callee)) { 18 | const { property } = callee 19 | if (t.isIdentifier(property) && atomNames.includes(property.name)) { 20 | return true 21 | } 22 | } 23 | return false 24 | } 25 | 26 | const atomFunctionNames = [ 27 | // Core 28 | 'atom', 29 | 'atomFamily', 30 | 'atomWithDefault', 31 | 'atomWithObservable', 32 | 'atomWithReducer', 33 | 'atomWithReset', 34 | 'atomWithStorage', 35 | 'freezeAtom', 36 | 'loadable', 37 | 'selectAtom', 38 | 'splitAtom', 39 | 'unwrap', 40 | // jotai-xstate 41 | 'atomWithMachine', 42 | // jotai-immer 43 | 'atomWithImmer', 44 | // jotai-valtio 45 | 'atomWithProxy', 46 | // jotai-trpc + jotai-relay 47 | 'atomWithQuery', 48 | 'atomWithMutation', 49 | 'atomWithSubscription', 50 | // jotai-redux + jotai-zustand 51 | 'atomWithStore', 52 | // jotai-location 53 | 'atomWithHash', 54 | 'atomWithLocation', 55 | // jotai-optics 56 | 'focusAtom', 57 | // jotai-form 58 | 'atomWithValidate', 59 | 'validateAtoms', 60 | // jotai-cache 61 | 'atomWithCache', 62 | // jotai-recoil 63 | 'atomWithRecoilValue', 64 | ] 65 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './vanilla.ts' 2 | export * from './react.ts' 3 | -------------------------------------------------------------------------------- /src/react.ts: -------------------------------------------------------------------------------- 1 | export { Provider, useStore } from './react/Provider.ts' 2 | export { useAtomValue } from './react/useAtomValue.ts' 3 | export { useSetAtom } from './react/useSetAtom.ts' 4 | export { useAtom } from './react/useAtom.ts' 5 | -------------------------------------------------------------------------------- /src/react/Provider.ts: -------------------------------------------------------------------------------- 1 | import { createContext, createElement, useContext, useRef } from 'react' 2 | import type { FunctionComponent, ReactElement, ReactNode } from 'react' 3 | import { createStore, getDefaultStore } from '../vanilla.ts' 4 | 5 | type Store = ReturnType 6 | 7 | type StoreContextType = ReturnType> 8 | const StoreContext: StoreContextType = createContext( 9 | undefined, 10 | ) 11 | 12 | type Options = { 13 | store?: Store 14 | } 15 | 16 | export function useStore(options?: Options): Store { 17 | const store = useContext(StoreContext) 18 | return options?.store || store || getDefaultStore() 19 | } 20 | 21 | export function Provider({ 22 | children, 23 | store, 24 | }: { 25 | children?: ReactNode 26 | store?: Store 27 | }): ReactElement< 28 | { value: Store | undefined }, 29 | FunctionComponent<{ value: Store | undefined }> 30 | > { 31 | const storeRef = useRef(undefined) 32 | if (!store && !storeRef.current) { 33 | storeRef.current = createStore() 34 | } 35 | return createElement( 36 | StoreContext.Provider, 37 | { 38 | value: store || storeRef.current, 39 | }, 40 | children, 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /src/react/useAtom.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Atom, 3 | ExtractAtomArgs, 4 | ExtractAtomResult, 5 | ExtractAtomValue, 6 | PrimitiveAtom, 7 | SetStateAction, 8 | WritableAtom, 9 | } from '../vanilla.ts' 10 | import { useAtomValue } from './useAtomValue.ts' 11 | import { useSetAtom } from './useSetAtom.ts' 12 | 13 | type SetAtom = (...args: Args) => Result 14 | 15 | type Options = Parameters[1] 16 | 17 | export function useAtom( 18 | atom: WritableAtom, 19 | options?: Options, 20 | ): [Awaited, SetAtom] 21 | 22 | export function useAtom( 23 | atom: PrimitiveAtom, 24 | options?: Options, 25 | ): [Awaited, SetAtom<[SetStateAction], void>] 26 | 27 | export function useAtom( 28 | atom: Atom, 29 | options?: Options, 30 | ): [Awaited, never] 31 | 32 | export function useAtom< 33 | AtomType extends WritableAtom, 34 | >( 35 | atom: AtomType, 36 | options?: Options, 37 | ): [ 38 | Awaited>, 39 | SetAtom, ExtractAtomResult>, 40 | ] 41 | 42 | export function useAtom>( 43 | atom: AtomType, 44 | options?: Options, 45 | ): [Awaited>, never] 46 | 47 | export function useAtom( 48 | atom: Atom | WritableAtom, 49 | options?: Options, 50 | ) { 51 | return [ 52 | useAtomValue(atom, options), 53 | // We do wrong type assertion here, which results in throwing an error. 54 | useSetAtom(atom as WritableAtom, options), 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /src/react/useSetAtom.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react' 2 | import type { 3 | ExtractAtomArgs, 4 | ExtractAtomResult, 5 | WritableAtom, 6 | } from '../vanilla.ts' 7 | import { useStore } from './Provider.ts' 8 | 9 | type SetAtom = (...args: Args) => Result 10 | type Options = Parameters[0] 11 | 12 | export function useSetAtom( 13 | atom: WritableAtom, 14 | options?: Options, 15 | ): SetAtom 16 | 17 | export function useSetAtom< 18 | AtomType extends WritableAtom, 19 | >( 20 | atom: AtomType, 21 | options?: Options, 22 | ): SetAtom, ExtractAtomResult> 23 | 24 | export function useSetAtom( 25 | atom: WritableAtom, 26 | options?: Options, 27 | ) { 28 | const store = useStore(options) 29 | const setAtom = useCallback( 30 | (...args: Args) => { 31 | if (import.meta.env?.MODE !== 'production' && !('write' in atom)) { 32 | // useAtom can pass non writable atom with wrong type assertion, 33 | // so we should check here. 34 | throw new Error('not writable atom') 35 | } 36 | return store.set(atom, ...args) 37 | }, 38 | [store, atom], 39 | ) 40 | return setAtom 41 | } 42 | -------------------------------------------------------------------------------- /src/react/utils.ts: -------------------------------------------------------------------------------- 1 | export { useResetAtom } from './utils/useResetAtom.ts' 2 | export { useReducerAtom } from './utils/useReducerAtom.ts' 3 | export { useAtomCallback } from './utils/useAtomCallback.ts' 4 | export { useHydrateAtoms } from './utils/useHydrateAtoms.ts' 5 | -------------------------------------------------------------------------------- /src/react/utils/useAtomCallback.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | import { useSetAtom } from '../../react.ts' 3 | import { atom } from '../../vanilla.ts' 4 | import type { Getter, Setter } from '../../vanilla.ts' 5 | 6 | type Options = Parameters[1] 7 | 8 | export function useAtomCallback( 9 | callback: (get: Getter, set: Setter, ...arg: Args) => Result, 10 | options?: Options, 11 | ): (...args: Args) => Result { 12 | const anAtom = useMemo( 13 | () => atom(null, (get, set, ...args: Args) => callback(get, set, ...args)), 14 | [callback], 15 | ) 16 | return useSetAtom(anAtom, options) 17 | } 18 | -------------------------------------------------------------------------------- /src/react/utils/useHydrateAtoms.ts: -------------------------------------------------------------------------------- 1 | import { useStore } from '../../react.ts' 2 | import type { WritableAtom } from '../../vanilla.ts' 3 | 4 | type Store = ReturnType 5 | type Options = Parameters[0] & { 6 | dangerouslyForceHydrate?: boolean 7 | } 8 | type AnyWritableAtom = WritableAtom 9 | 10 | type InferAtomTuples = { 11 | [K in keyof T]: T[K] extends readonly [infer A, unknown] 12 | ? A extends WritableAtom 13 | ? readonly [A, Args[0]] 14 | : T[K] 15 | : never 16 | } 17 | 18 | // For internal use only 19 | // This can be changed without notice. 20 | export type INTERNAL_InferAtomTuples = InferAtomTuples 21 | 22 | const hydratedMap: WeakMap> = new WeakMap() 23 | 24 | export function useHydrateAtoms< 25 | T extends (readonly [AnyWritableAtom, unknown])[], 26 | >(values: InferAtomTuples, options?: Options): void 27 | 28 | export function useHydrateAtoms>( 29 | values: T, 30 | options?: Options, 31 | ): void 32 | 33 | export function useHydrateAtoms< 34 | T extends Iterable, 35 | >(values: InferAtomTuples, options?: Options): void 36 | 37 | export function useHydrateAtoms< 38 | T extends Iterable, 39 | >(values: T, options?: Options) { 40 | const store = useStore(options) 41 | 42 | const hydratedSet = getHydratedSet(store) 43 | for (const [atom, value] of values) { 44 | if (!hydratedSet.has(atom) || options?.dangerouslyForceHydrate) { 45 | hydratedSet.add(atom) 46 | store.set(atom, value as never) 47 | } 48 | } 49 | } 50 | 51 | const getHydratedSet = (store: Store) => { 52 | let hydratedSet = hydratedMap.get(store) 53 | if (!hydratedSet) { 54 | hydratedSet = new WeakSet() 55 | hydratedMap.set(store, hydratedSet) 56 | } 57 | return hydratedSet 58 | } 59 | -------------------------------------------------------------------------------- /src/react/utils/useReducerAtom.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react' 2 | import { useAtom } from '../../react.ts' 3 | import type { PrimitiveAtom } from '../../vanilla.ts' 4 | 5 | type Options = Parameters[1] 6 | 7 | /** 8 | * @deprecated please use a recipe instead 9 | * https://github.com/pmndrs/jotai/pull/2467 10 | */ 11 | export function useReducerAtom( 12 | anAtom: PrimitiveAtom, 13 | reducer: (v: Value, a?: Action) => Value, 14 | options?: Options, 15 | ): [Value, (action?: Action) => void] 16 | 17 | /** 18 | * @deprecated please use a recipe instead 19 | * https://github.com/pmndrs/jotai/pull/2467 20 | */ 21 | export function useReducerAtom( 22 | anAtom: PrimitiveAtom, 23 | reducer: (v: Value, a: Action) => Value, 24 | options?: Options, 25 | ): [Value, (action: Action) => void] 26 | 27 | export function useReducerAtom( 28 | anAtom: PrimitiveAtom, 29 | reducer: (v: Value, a: Action) => Value, 30 | options?: Options, 31 | ) { 32 | if (import.meta.env?.MODE !== 'production') { 33 | console.warn( 34 | '[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', 35 | ) 36 | } 37 | const [state, setState] = useAtom(anAtom, options) 38 | const dispatch = useCallback( 39 | (action: Action) => { 40 | setState((prev) => reducer(prev, action)) 41 | }, 42 | [setState, reducer], 43 | ) 44 | return [state, dispatch] 45 | } 46 | -------------------------------------------------------------------------------- /src/react/utils/useResetAtom.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react' 2 | import { useSetAtom } from '../../react.ts' 3 | import { RESET } from '../../vanilla/utils.ts' 4 | import type { WritableAtom } from '../../vanilla.ts' 5 | 6 | type Options = Parameters[1] 7 | 8 | export function useResetAtom( 9 | anAtom: WritableAtom, 10 | options?: Options, 11 | ): () => T { 12 | const setAtom = useSetAtom(anAtom, options) 13 | const resetAtom = useCallback(() => setAtom(RESET), [setAtom]) 14 | return resetAtom 15 | } 16 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | declare interface ImportMeta { 2 | env?: { 3 | MODE: string 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export * from './vanilla/utils.ts' 2 | export * from './react/utils.ts' 3 | -------------------------------------------------------------------------------- /src/vanilla.ts: -------------------------------------------------------------------------------- 1 | export { atom } from './vanilla/atom.ts' 2 | export type { Atom, WritableAtom, PrimitiveAtom } from './vanilla/atom.ts' 3 | 4 | export { 5 | createStore, 6 | getDefaultStore, 7 | INTERNAL_overrideCreateStore, 8 | } from './vanilla/store.ts' 9 | 10 | export type { 11 | Getter, 12 | Setter, 13 | ExtractAtomValue, 14 | ExtractAtomArgs, 15 | ExtractAtomResult, 16 | SetStateAction, 17 | } from './vanilla/typeUtils.ts' 18 | -------------------------------------------------------------------------------- /src/vanilla/typeUtils.ts: -------------------------------------------------------------------------------- 1 | import type { Atom, PrimitiveAtom, WritableAtom } from './atom.ts' 2 | 3 | export type Getter = Parameters['read']>[0] 4 | export type Setter = Parameters< 5 | WritableAtom['write'] 6 | >[1] 7 | 8 | export type ExtractAtomValue = 9 | AtomType extends Atom ? Value : never 10 | 11 | export type ExtractAtomArgs = 12 | AtomType extends WritableAtom 13 | ? Args 14 | : never 15 | 16 | export type ExtractAtomResult = 17 | AtomType extends WritableAtom 18 | ? Result 19 | : never 20 | 21 | export type SetStateAction = ExtractAtomArgs>[0] 22 | -------------------------------------------------------------------------------- /src/vanilla/utils.ts: -------------------------------------------------------------------------------- 1 | export { RESET } from './utils/constants.ts' 2 | export { atomWithReset } from './utils/atomWithReset.ts' 3 | export { atomWithReducer } from './utils/atomWithReducer.ts' 4 | export { atomFamily } from './utils/atomFamily.ts' 5 | export { selectAtom } from './utils/selectAtom.ts' 6 | export { freezeAtom, freezeAtomCreator } from './utils/freezeAtom.ts' 7 | export { splitAtom } from './utils/splitAtom.ts' 8 | export { atomWithDefault } from './utils/atomWithDefault.ts' 9 | export { 10 | atomWithStorage, 11 | createJSONStorage, 12 | withStorageValidator as unstable_withStorageValidator, 13 | } from './utils/atomWithStorage.ts' 14 | export { atomWithObservable } from './utils/atomWithObservable.ts' 15 | export { loadable } from './utils/loadable.ts' 16 | export { unwrap } from './utils/unwrap.ts' 17 | export { atomWithRefresh } from './utils/atomWithRefresh.ts' 18 | export { atomWithLazy } from './utils/atomWithLazy.ts' 19 | -------------------------------------------------------------------------------- /src/vanilla/utils/atomWithDefault.ts: -------------------------------------------------------------------------------- 1 | import { atom } from '../../vanilla.ts' 2 | import type { SetStateAction, WritableAtom } from '../../vanilla.ts' 3 | import { RESET } from './constants.ts' 4 | 5 | type Read = WritableAtom< 6 | Value, 7 | Args, 8 | Result 9 | >['read'] 10 | 11 | export function atomWithDefault( 12 | getDefault: Read | typeof RESET], void>, 13 | ): WritableAtom | typeof RESET], void> { 14 | const EMPTY = Symbol() 15 | const overwrittenAtom = atom(EMPTY) 16 | 17 | if (import.meta.env?.MODE !== 'production') { 18 | overwrittenAtom.debugPrivate = true 19 | } 20 | 21 | const anAtom: WritableAtom< 22 | Value, 23 | [SetStateAction | typeof RESET], 24 | void 25 | > = atom( 26 | (get, options) => { 27 | const overwritten = get(overwrittenAtom) 28 | if (overwritten !== EMPTY) { 29 | return overwritten 30 | } 31 | return getDefault(get, options) 32 | }, 33 | (get, set, update) => { 34 | if (update === RESET) { 35 | set(overwrittenAtom, EMPTY) 36 | } else if (typeof update === 'function') { 37 | const prevValue = get(anAtom) 38 | set(overwrittenAtom, (update as (prev: Value) => Value)(prevValue)) 39 | } else { 40 | set(overwrittenAtom, update) 41 | } 42 | }, 43 | ) 44 | return anAtom 45 | } 46 | -------------------------------------------------------------------------------- /src/vanilla/utils/atomWithLazy.ts: -------------------------------------------------------------------------------- 1 | import { atom } from '../../vanilla.ts' 2 | import type { PrimitiveAtom } from '../../vanilla.ts' 3 | 4 | export function atomWithLazy( 5 | makeInitial: () => Value, 6 | ): PrimitiveAtom { 7 | const a = atom(undefined as unknown as Value) 8 | delete (a as { init?: Value }).init 9 | Object.defineProperty(a, 'init', { 10 | get() { 11 | return makeInitial() 12 | }, 13 | }) 14 | return a 15 | } 16 | -------------------------------------------------------------------------------- /src/vanilla/utils/atomWithReducer.ts: -------------------------------------------------------------------------------- 1 | import { atom } from '../../vanilla.ts' 2 | import type { WritableAtom } from '../../vanilla.ts' 3 | 4 | export function atomWithReducer( 5 | initialValue: Value, 6 | reducer: (value: Value, action?: Action) => Value, 7 | ): WritableAtom 8 | 9 | export function atomWithReducer( 10 | initialValue: Value, 11 | reducer: (value: Value, action: Action) => Value, 12 | ): WritableAtom 13 | 14 | export function atomWithReducer( 15 | initialValue: Value, 16 | reducer: (value: Value, action: Action) => Value, 17 | ) { 18 | return atom(initialValue, function (this: never, get, set, action: Action) { 19 | set(this, reducer(get(this), action)) 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /src/vanilla/utils/atomWithRefresh.ts: -------------------------------------------------------------------------------- 1 | import { atom } from '../../vanilla.ts' 2 | import type { WritableAtom } from '../../vanilla.ts' 3 | 4 | type Read = WritableAtom< 5 | Value, 6 | Args, 7 | Result 8 | >['read'] 9 | type Write = WritableAtom< 10 | Value, 11 | Args, 12 | Result 13 | >['write'] 14 | 15 | export function atomWithRefresh( 16 | read: Read, 17 | write: Write, 18 | ): WritableAtom 19 | 20 | export function atomWithRefresh( 21 | read: Read, 22 | ): WritableAtom 23 | 24 | export function atomWithRefresh( 25 | read: Read, 26 | write?: Write, 27 | ) { 28 | const refreshAtom = atom(0) 29 | if (import.meta.env?.MODE !== 'production') { 30 | refreshAtom.debugPrivate = true 31 | } 32 | return atom( 33 | (get, options) => { 34 | get(refreshAtom) 35 | return read(get, options as never) 36 | }, 37 | (get, set, ...args: Args) => { 38 | if (args.length === 0) { 39 | set(refreshAtom, (c) => c + 1) 40 | } else if (write) { 41 | return write(get, set, ...args) 42 | } else if (import.meta.env?.MODE !== 'production') { 43 | throw new Error('refresh must be called without arguments') 44 | } 45 | }, 46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /src/vanilla/utils/atomWithReset.ts: -------------------------------------------------------------------------------- 1 | import { atom } from '../../vanilla.ts' 2 | import type { WritableAtom } from '../../vanilla.ts' 3 | import { RESET } from './constants.ts' 4 | 5 | type SetStateActionWithReset = 6 | | Value 7 | | typeof RESET 8 | | ((prev: Value) => Value | typeof RESET) 9 | 10 | // This is an internal type and not part of public API. 11 | // Do not depend on it as it can change without notice. 12 | type WithInitialValue = { 13 | init: Value 14 | } 15 | 16 | export function atomWithReset( 17 | initialValue: Value, 18 | ): WritableAtom], void> & 19 | WithInitialValue { 20 | type Update = SetStateActionWithReset 21 | const anAtom = atom( 22 | initialValue, 23 | (get, set, update) => { 24 | const nextValue = 25 | typeof update === 'function' 26 | ? (update as (prev: Value) => Value | typeof RESET)(get(anAtom)) 27 | : update 28 | 29 | set(anAtom, nextValue === RESET ? initialValue : nextValue) 30 | }, 31 | ) 32 | return anAtom as WritableAtom & WithInitialValue 33 | } 34 | -------------------------------------------------------------------------------- /src/vanilla/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const RESET: unique symbol = Symbol( 2 | import.meta.env?.MODE !== 'production' ? 'RESET' : '', 3 | ) 4 | -------------------------------------------------------------------------------- /src/vanilla/utils/freezeAtom.ts: -------------------------------------------------------------------------------- 1 | import type { Atom, WritableAtom } from '../../vanilla.ts' 2 | 3 | const frozenAtoms = new WeakSet>() 4 | 5 | const deepFreeze = (value: T): T => { 6 | if (typeof value !== 'object' || value === null) { 7 | return value 8 | } 9 | Object.freeze(value) 10 | const propNames = Object.getOwnPropertyNames(value) 11 | for (const name of propNames) { 12 | deepFreeze((value as never)[name]) 13 | } 14 | return value 15 | } 16 | 17 | export function freezeAtom>( 18 | anAtom: AtomType, 19 | ): AtomType 20 | 21 | export function freezeAtom( 22 | anAtom: WritableAtom, 23 | ): WritableAtom { 24 | if (frozenAtoms.has(anAtom)) { 25 | return anAtom 26 | } 27 | frozenAtoms.add(anAtom) 28 | 29 | const origRead = anAtom.read 30 | anAtom.read = function (get, options) { 31 | return deepFreeze(origRead.call(this, get, options)) 32 | } 33 | if ('write' in anAtom) { 34 | const origWrite = anAtom.write 35 | anAtom.write = function (get, set, ...args) { 36 | return origWrite.call( 37 | this, 38 | get, 39 | (...setArgs) => { 40 | if (setArgs[0] === anAtom) { 41 | setArgs[1] = deepFreeze(setArgs[1]) 42 | } 43 | 44 | return set(...setArgs) 45 | }, 46 | ...args, 47 | ) 48 | } 49 | } 50 | return anAtom 51 | } 52 | 53 | /** 54 | * @deprecated Define it on users end 55 | */ 56 | export function freezeAtomCreator< 57 | CreateAtom extends (...args: unknown[]) => Atom, 58 | >(createAtom: CreateAtom): CreateAtom { 59 | if (import.meta.env?.MODE !== 'production') { 60 | console.warn( 61 | '[DEPRECATED] freezeAtomCreator is deprecated, define it on users end', 62 | ) 63 | } 64 | return ((...args: unknown[]) => freezeAtom(createAtom(...args))) as never 65 | } 66 | -------------------------------------------------------------------------------- /src/vanilla/utils/loadable.ts: -------------------------------------------------------------------------------- 1 | import { atom } from '../../vanilla.ts' 2 | import type { Atom } from '../../vanilla.ts' 3 | 4 | const cache1 = new WeakMap() 5 | const memo1 = (create: () => T, dep1: object): T => 6 | (cache1.has(dep1) ? cache1 : cache1.set(dep1, create())).get(dep1) 7 | 8 | const isPromiseLike = (p: unknown): p is PromiseLike> => 9 | typeof (p as any)?.then === 'function' 10 | 11 | export type Loadable = 12 | | { state: 'loading' } 13 | | { state: 'hasError'; error: unknown } 14 | | { state: 'hasData'; data: Awaited } 15 | 16 | const LOADING: Loadable = { state: 'loading' } 17 | 18 | export function loadable(anAtom: Atom): Atom> { 19 | return memo1(() => { 20 | const loadableCache = new WeakMap< 21 | PromiseLike>, 22 | Loadable 23 | >() 24 | const refreshAtom = atom(0) 25 | 26 | if (import.meta.env?.MODE !== 'production') { 27 | refreshAtom.debugPrivate = true 28 | } 29 | 30 | const derivedAtom = atom( 31 | (get, { setSelf }) => { 32 | get(refreshAtom) 33 | let value: Value 34 | try { 35 | value = get(anAtom) 36 | } catch (error) { 37 | return { state: 'hasError', error } as Loadable 38 | } 39 | if (!isPromiseLike(value)) { 40 | return { state: 'hasData', data: value } as Loadable 41 | } 42 | const promise = value 43 | const cached1 = loadableCache.get(promise) 44 | if (cached1) { 45 | return cached1 46 | } 47 | promise.then( 48 | (data) => { 49 | loadableCache.set(promise, { state: 'hasData', data }) 50 | setSelf() 51 | }, 52 | (error) => { 53 | loadableCache.set(promise, { state: 'hasError', error }) 54 | setSelf() 55 | }, 56 | ) 57 | 58 | const cached2 = loadableCache.get(promise) 59 | if (cached2) { 60 | return cached2 61 | } 62 | loadableCache.set(promise, LOADING as Loadable) 63 | return LOADING as Loadable 64 | }, 65 | (_get, set) => { 66 | set(refreshAtom, (c) => c + 1) 67 | }, 68 | ) 69 | 70 | if (import.meta.env?.MODE !== 'production') { 71 | derivedAtom.debugPrivate = true 72 | } 73 | 74 | return atom((get) => get(derivedAtom)) 75 | }, anAtom) 76 | } 77 | -------------------------------------------------------------------------------- /src/vanilla/utils/selectAtom.ts: -------------------------------------------------------------------------------- 1 | import { atom } from '../../vanilla.ts' 2 | import type { Atom } from '../../vanilla.ts' 3 | 4 | const getCached = (c: () => T, m: WeakMap, k: object): T => 5 | (m.has(k) ? m : m.set(k, c())).get(k) as T 6 | const cache1 = new WeakMap() 7 | const memo3 = ( 8 | create: () => T, 9 | dep1: object, 10 | dep2: object, 11 | dep3: object, 12 | ): T => { 13 | const cache2 = getCached(() => new WeakMap(), cache1, dep1) 14 | const cache3 = getCached(() => new WeakMap(), cache2, dep2) 15 | return getCached(create, cache3, dep3) 16 | } 17 | 18 | export function selectAtom( 19 | anAtom: Atom, 20 | selector: (v: Value, prevSlice?: Slice) => Slice, 21 | equalityFn?: (a: Slice, b: Slice) => boolean, 22 | ): Atom 23 | 24 | export function selectAtom( 25 | anAtom: Atom, 26 | selector: (v: Value, prevSlice?: Slice) => Slice, 27 | equalityFn: (prevSlice: Slice, slice: Slice) => boolean = Object.is, 28 | ) { 29 | return memo3( 30 | () => { 31 | const EMPTY = Symbol() 32 | const selectValue = ([value, prevSlice]: readonly [ 33 | Value, 34 | Slice | typeof EMPTY, 35 | ]) => { 36 | if (prevSlice === EMPTY) { 37 | return selector(value) 38 | } 39 | const slice = selector(value, prevSlice) 40 | return equalityFn(prevSlice, slice) ? prevSlice : slice 41 | } 42 | const derivedAtom: Atom & { 43 | init?: typeof EMPTY 44 | } = atom((get) => { 45 | const prev = get(derivedAtom) 46 | const value = get(anAtom) 47 | return selectValue([value, prev] as const) 48 | }) 49 | // HACK to read derived atom before initialization 50 | derivedAtom.init = EMPTY 51 | return derivedAtom 52 | }, 53 | anAtom, 54 | selector, 55 | equalityFn, 56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /tests/react/provider.test.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { render, screen, waitFor } from '@testing-library/react' 3 | import { it } from 'vitest' 4 | import { Provider, useAtom } from 'jotai/react' 5 | import { atom, createStore } from 'jotai/vanilla' 6 | 7 | it('uses initial values from provider', async () => { 8 | const countAtom = atom(1) 9 | const petAtom = atom('cat') 10 | 11 | const Display = () => { 12 | const [count] = useAtom(countAtom) 13 | const [pet] = useAtom(petAtom) 14 | 15 | return ( 16 | <> 17 |

count: {count}

18 |

pet: {pet}

19 | 20 | ) 21 | } 22 | 23 | const store = createStore() 24 | store.set(countAtom, 0) 25 | store.set(petAtom, 'dog') 26 | 27 | render( 28 | 29 | 30 | 31 | 32 | , 33 | ) 34 | 35 | await waitFor(() => { 36 | screen.getByText('count: 0') 37 | screen.getByText('pet: dog') 38 | }) 39 | }) 40 | 41 | it('only uses initial value from provider for specific atom', async () => { 42 | const countAtom = atom(1) 43 | const petAtom = atom('cat') 44 | 45 | const Display = () => { 46 | const [count] = useAtom(countAtom) 47 | const [pet] = useAtom(petAtom) 48 | 49 | return ( 50 | <> 51 |

count: {count}

52 |

pet: {pet}

53 | 54 | ) 55 | } 56 | 57 | const store = createStore() 58 | store.set(petAtom, 'dog') 59 | 60 | render( 61 | 62 | 63 | 64 | 65 | , 66 | ) 67 | 68 | await waitFor(() => { 69 | screen.getByText('count: 1') 70 | screen.getByText('pet: dog') 71 | }) 72 | }) 73 | 74 | it('renders correctly without children', () => { 75 | render( 76 | 77 | 78 | , 79 | ) 80 | }) 81 | -------------------------------------------------------------------------------- /tests/react/utils/types.test.tsx: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest' 2 | import { useHydrateAtoms } from 'jotai/react/utils' 3 | import { atom } from 'jotai/vanilla' 4 | 5 | it('useHydrateAtoms should not allow invalid atom types when array is passed', () => { 6 | function Component() { 7 | const countAtom = atom(0) 8 | const activeAtom = atom(true) 9 | // Adding @ts-ignore for typescript 3.8 support 10 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 11 | // @ts-ignore 12 | // @ts-expect-error TS2769 13 | useHydrateAtoms([ 14 | [countAtom, 'foo'], 15 | [activeAtom, 0], 16 | ]) 17 | // Adding @ts-ignore for typescript 3.8 support 18 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 19 | // @ts-ignore 20 | // @ts-expect-error TS2769 21 | useHydrateAtoms([ 22 | [countAtom, 1], 23 | [activeAtom, 0], 24 | ]) 25 | // Adding @ts-ignore for typescript 3.8 support 26 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 27 | // @ts-ignore 28 | // @ts-expect-error TS2769 29 | useHydrateAtoms([ 30 | [countAtom, true], 31 | [activeAtom, false], 32 | ]) 33 | } 34 | expect(Component).toBeDefined() 35 | }) 36 | -------------------------------------------------------------------------------- /tests/setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/vitest' 2 | -------------------------------------------------------------------------------- /tests/vanilla/basic.test.tsx: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest' 2 | import { atom } from 'jotai/vanilla' 3 | 4 | it('creates atoms', () => { 5 | // primitive atom 6 | const countAtom = atom(0) 7 | const anotherCountAtom = atom(1) 8 | // read-only derived atom 9 | const doubledCountAtom = atom((get) => get(countAtom) * 2) 10 | // read-write derived atom 11 | const sumCountAtom = atom( 12 | (get) => get(countAtom) + get(anotherCountAtom), 13 | (get, set, value: number) => { 14 | set(countAtom, get(countAtom) + value / 2) 15 | set(anotherCountAtom, get(anotherCountAtom) + value / 2) 16 | }, 17 | ) 18 | // write-only derived atom 19 | const decrementCountAtom = atom(null, (get, set) => { 20 | set(countAtom, get(countAtom) - 1) 21 | }) 22 | delete countAtom.debugLabel 23 | delete doubledCountAtom.debugLabel 24 | delete sumCountAtom.debugLabel 25 | delete decrementCountAtom.debugLabel 26 | expect({ 27 | countAtom, 28 | doubledCountAtom, 29 | sumCountAtom, 30 | decrementCountAtom, 31 | }).toMatchInlineSnapshot(` 32 | { 33 | "countAtom": { 34 | "init": 0, 35 | "read": [Function], 36 | "toString": [Function], 37 | "write": [Function], 38 | }, 39 | "decrementCountAtom": { 40 | "init": null, 41 | "read": [Function], 42 | "toString": [Function], 43 | "write": [Function], 44 | }, 45 | "doubledCountAtom": { 46 | "read": [Function], 47 | "toString": [Function], 48 | }, 49 | "sumCountAtom": { 50 | "read": [Function], 51 | "toString": [Function], 52 | "write": [Function], 53 | }, 54 | } 55 | `) 56 | }) 57 | 58 | it('should let users mark atoms as private', () => { 59 | const internalAtom = atom(0) 60 | internalAtom.debugPrivate = true 61 | delete internalAtom.debugLabel 62 | 63 | expect(internalAtom).toMatchInlineSnapshot(` 64 | { 65 | "debugPrivate": true, 66 | "init": 0, 67 | "read": [Function], 68 | "toString": [Function], 69 | "write": [Function], 70 | } 71 | `) 72 | }) 73 | -------------------------------------------------------------------------------- /tests/vanilla/internals.test.tsx: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | import { createStore } from 'jotai' 3 | import { 4 | INTERNAL_buildStoreRev1 as INTERNAL_buildStore, 5 | INTERNAL_getBuildingBlocksRev1 as INTERNAL_getBuildingBlocks, 6 | } from 'jotai/vanilla/internals' 7 | 8 | describe('internals', () => { 9 | it('should not return a sparse building blocks array', () => { 10 | const isSparse = (arr: ReadonlyArray) => { 11 | return arr.some((_, i) => !Object.prototype.hasOwnProperty.call(arr, i)) 12 | } 13 | { 14 | const store = createStore() 15 | const buildingBlocks = INTERNAL_getBuildingBlocks(store) 16 | expect(buildingBlocks.length).toBe(20) 17 | expect(isSparse(buildingBlocks)).toBe(false) 18 | } 19 | { 20 | const store = INTERNAL_buildStore() 21 | const buildingBlocks = INTERNAL_getBuildingBlocks(store) 22 | expect(buildingBlocks.length).toBe(20) 23 | expect(isSparse(buildingBlocks)).toBe(false) 24 | } 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /tests/vanilla/types.test.tsx: -------------------------------------------------------------------------------- 1 | import { expectType } from 'ts-expect' 2 | import type { TypeOf } from 'ts-expect' 3 | import { expect, it } from 'vitest' 4 | import { atom } from 'jotai/vanilla' 5 | import type { 6 | Atom, 7 | ExtractAtomArgs, 8 | ExtractAtomResult, 9 | ExtractAtomValue, 10 | PrimitiveAtom, 11 | WritableAtom, 12 | } from 'jotai/vanilla' 13 | 14 | it('atom() should return the correct types', () => { 15 | function Component() { 16 | // primitive atom 17 | const primitiveAtom = atom(0) 18 | expectType>(primitiveAtom) 19 | expectType, typeof primitiveAtom>>(true) 20 | expectType, typeof primitiveAtom>>( 21 | false, 22 | ) 23 | 24 | // primitive atom without initial value 25 | const primitiveWithoutInitialAtom = atom() 26 | expectType>(primitiveWithoutInitialAtom) 27 | expectType>(atom()) 28 | 29 | // read-only derived atom 30 | const readonlyDerivedAtom = atom((get) => get(primitiveAtom) * 2) 31 | expectType>(readonlyDerivedAtom) 32 | 33 | // read-write derived atom 34 | const readWriteDerivedAtom = atom( 35 | (get) => get(primitiveAtom), 36 | (get, set, value: number) => { 37 | set(primitiveAtom, get(primitiveAtom) + value) 38 | }, 39 | ) 40 | expectType>(readWriteDerivedAtom) 41 | 42 | // write-only derived atom 43 | const writeonlyDerivedAtom = atom(null, (get, set) => { 44 | set(primitiveAtom, get(primitiveAtom) - 1) 45 | }) 46 | expectType>(writeonlyDerivedAtom) 47 | } 48 | expect(Component).toBeDefined() 49 | }) 50 | 51 | it('type utils should work', () => { 52 | function Component() { 53 | const readWriteAtom = atom( 54 | (_get) => 1 as number, 55 | async (_get, _set, _value: string) => {}, 56 | ) 57 | 58 | const value: ExtractAtomValue = 1 59 | expectType(value) 60 | 61 | const args: ExtractAtomArgs = [''] 62 | expectType<[string]>(args) 63 | 64 | const result: ExtractAtomResult = Promise.resolve() 65 | expectType>(result) 66 | } 67 | expect(Component).toBeDefined() 68 | }) 69 | -------------------------------------------------------------------------------- /tests/vanilla/utils/atomWithLazy.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it, vi } from 'vitest' 2 | import { createStore } from 'jotai/vanilla' 3 | import { atomWithLazy } from 'jotai/vanilla/utils' 4 | 5 | it('initializes on first store get', async () => { 6 | const storeA = createStore() 7 | const storeB = createStore() 8 | 9 | let externalState = 'first' 10 | const initializer = vi.fn(() => externalState) 11 | const anAtom = atomWithLazy(initializer) 12 | 13 | expect(initializer).not.toHaveBeenCalled() 14 | expect(storeA.get(anAtom)).toEqual('first') 15 | expect(initializer).toHaveBeenCalledOnce() 16 | 17 | externalState = 'second' 18 | 19 | expect(storeA.get(anAtom)).toEqual('first') 20 | expect(initializer).toHaveBeenCalledOnce() 21 | expect(storeB.get(anAtom)).toEqual('second') 22 | expect(initializer).toHaveBeenCalledTimes(2) 23 | }) 24 | 25 | it('is writable', async () => { 26 | const store = createStore() 27 | const anAtom = atomWithLazy(() => 0) 28 | 29 | store.set(anAtom, 123) 30 | 31 | expect(store.get(anAtom)).toEqual(123) 32 | }) 33 | 34 | it('should work with a set state action', async () => { 35 | const store = createStore() 36 | const anAtom = atomWithLazy(() => 4) 37 | 38 | store.set(anAtom, (prev: number) => prev * prev) 39 | 40 | expect(store.get(anAtom)).toEqual(16) 41 | }) 42 | -------------------------------------------------------------------------------- /tests/vanilla/utils/atomWithRefresh.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, vi } from 'vitest' 2 | import { createStore } from 'jotai/vanilla' 3 | import { atomWithRefresh } from 'jotai/vanilla/utils' 4 | 5 | describe('atomWithRefresh', () => { 6 | it('[DEV-ONLY] throws when refresh is called with extra arguments', () => { 7 | const atom = atomWithRefresh(() => {}) 8 | const store = createStore() 9 | const args = ['some arg'] as unknown as [] 10 | expect(() => store.set(atom, ...args)).throws() 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /tests/vanilla/utils/atomWithReset.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeEach, describe, expect, it, vi } from 'vitest' 2 | import { createStore } from 'jotai/vanilla' 3 | import { RESET, atomWithReset } from 'jotai/vanilla/utils' 4 | 5 | describe('atomWithReset', () => { 6 | let initialValue: number 7 | let testAtom: any 8 | 9 | beforeEach(() => { 10 | vi.clearAllMocks() 11 | initialValue = 10 12 | testAtom = atomWithReset(initialValue) 13 | }) 14 | 15 | it('should reset to initial value using RESET', () => { 16 | const store = createStore() 17 | store.set(testAtom, 123) 18 | store.set(testAtom, RESET) 19 | expect(store.get(testAtom)).toBe(initialValue) 20 | }) 21 | 22 | it('should update atom with a new value', () => { 23 | const store = createStore() 24 | store.set(testAtom, 123) 25 | store.set(testAtom, 30) 26 | expect(store.get(testAtom)).toBe(30) 27 | }) 28 | 29 | it('should update atom using a function', () => { 30 | const store = createStore() 31 | store.set(testAtom, 123) 32 | store.set(testAtom, (prev: number) => prev + 10) 33 | expect(store.get(testAtom)).toBe(133) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /tests/vanilla/utils/loadable.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | import { atom, createStore } from 'jotai/vanilla' 3 | import { loadable } from 'jotai/vanilla/utils' 4 | 5 | describe('loadable', () => { 6 | it('should return fulfilled value of an already resolved async atom', async () => { 7 | const store = createStore() 8 | const asyncAtom = atom(Promise.resolve('concrete')) 9 | 10 | expect(await store.get(asyncAtom)).toEqual('concrete') 11 | expect(store.get(loadable(asyncAtom))).toEqual({ 12 | state: 'loading', 13 | }) 14 | await new Promise((r) => setTimeout(r)) // wait for a tick 15 | expect(store.get(loadable(asyncAtom))).toEqual({ 16 | state: 'hasData', 17 | data: 'concrete', 18 | }) 19 | }) 20 | 21 | it('should get the latest loadable state after the promise resolves', async () => { 22 | const store = createStore() 23 | const asyncAtom = atom(Promise.resolve()) 24 | const loadableAtom = loadable(asyncAtom) 25 | 26 | expect(store.get(loadableAtom)).toHaveProperty('state', 'loading') 27 | 28 | await store.get(asyncAtom) 29 | 30 | expect(store.get(loadableAtom)).toHaveProperty('state', 'hasData') 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /tests/vanilla/utils/types.test.tsx: -------------------------------------------------------------------------------- 1 | import { expectType } from 'ts-expect' 2 | import type { TypeEqual } from 'ts-expect' 3 | import { it } from 'vitest' 4 | import { atom } from 'jotai/vanilla' 5 | import type { Atom, SetStateAction, WritableAtom } from 'jotai/vanilla' 6 | import { selectAtom, unwrap } from 'jotai/vanilla/utils' 7 | 8 | it('selectAtom() should return the correct types', () => { 9 | const doubleCount = (x: number) => x * 2 10 | const syncAtom = atom(0) 11 | const syncSelectedAtom = selectAtom(syncAtom, doubleCount) 12 | expectType, typeof syncSelectedAtom>>(true) 13 | }) 14 | 15 | it('unwrap() should return the correct types', () => { 16 | const getFallbackValue = () => -1 17 | const syncAtom = atom(0) 18 | const syncUnwrappedAtom = unwrap(syncAtom, getFallbackValue) 19 | expectType< 20 | TypeEqual< 21 | WritableAtom], void>, 22 | typeof syncUnwrappedAtom 23 | > 24 | >(true) 25 | 26 | const asyncAtom = atom(Promise.resolve(0)) 27 | const asyncUnwrappedAtom = unwrap(asyncAtom, getFallbackValue) 28 | expectType< 29 | TypeEqual< 30 | WritableAtom>], void>, 31 | typeof asyncUnwrappedAtom 32 | > 33 | >(true) 34 | 35 | const maybeAsyncAtom = atom(Promise.resolve(0) as number | Promise) 36 | const maybeAsyncUnwrappedAtom = unwrap(maybeAsyncAtom, getFallbackValue) 37 | expectType< 38 | TypeEqual< 39 | WritableAtom>], void>, 40 | typeof maybeAsyncUnwrappedAtom 41 | > 42 | >(true) 43 | }) 44 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "strict": true, 5 | "jsx": "react-jsx", 6 | "esModuleInterop": true, 7 | "module": "esnext", 8 | "moduleResolution": "bundler", 9 | "skipLibCheck": true /* FIXME remove this once vite fixes it */, 10 | "allowImportingTsExtensions": true, 11 | "noUncheckedIndexedAccess": true, 12 | "exactOptionalPropertyTypes": true, 13 | "verbatimModuleSyntax": true, 14 | "declaration": true, 15 | "isolatedDeclarations": true, 16 | "types": ["@testing-library/jest-dom"], 17 | "noEmit": true, 18 | "baseUrl": ".", 19 | "paths": { 20 | "jotai": ["./src/index.ts"], 21 | "jotai/*": ["./src/*.ts"] 22 | } 23 | }, 24 | "include": ["src/**/*", "tests/**/*", "benchmarks/**/*"], 25 | "exclude": ["node_modules", "dist"] 26 | } 27 | -------------------------------------------------------------------------------- /vitest.config.mts: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'node:fs' 2 | import { resolve } from 'node:path' 3 | import react from '@vitejs/plugin-react' 4 | import { defineConfig } from 'vitest/config' 5 | 6 | export default defineConfig({ 7 | resolve: { 8 | alias: [ 9 | { find: /^jotai$/, replacement: resolve('./src/index.ts') }, 10 | { find: /^jotai(.*)$/, replacement: resolve('./src/$1.ts') }, 11 | ], 12 | }, 13 | plugins: [ 14 | react({ 15 | babel: { 16 | plugins: existsSync('./dist/babel/plugin-debug-label.js') 17 | ? [ 18 | // FIXME Can we read from ./src instead of ./dist? 19 | './dist/babel/plugin-debug-label.js', 20 | ] 21 | : [], 22 | }, 23 | }), 24 | ], 25 | test: { 26 | name: 'jotai', 27 | // Keeping globals to true triggers React Testing Library's auto cleanup 28 | // https://vitest.dev/guide/migration.html 29 | globals: true, 30 | environment: 'jsdom', 31 | dir: 'tests', 32 | reporters: process.env.GITHUB_ACTIONS 33 | ? ['default', 'github-actions'] 34 | : ['default'], 35 | setupFiles: ['tests/setup.ts'], 36 | coverage: { 37 | reporter: ['text', 'json', 'html', 'text-summary'], 38 | reportsDirectory: './coverage/', 39 | provider: 'v8', 40 | include: ['src/**'], 41 | }, 42 | onConsoleLog(log) { 43 | if (log.includes('DOMException')) return false 44 | }, 45 | }, 46 | }) 47 | -------------------------------------------------------------------------------- /website/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "babel-preset-gatsby", 5 | { 6 | "reactRuntime": "automatic", 7 | "targets": { 8 | "browsers": [ 9 | ">0.25%", 10 | "not dead", 11 | "not ie <=11", 12 | "not ie_mob <=11", 13 | "not op_mini all" 14 | ] 15 | } 16 | } 17 | ] 18 | ], 19 | "plugins": ["jotai/babel/plugin-react-refresh"] 20 | } 21 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # dotenv environment variable files 55 | .env* 56 | 57 | # gatsby files 58 | .cache/ 59 | public 60 | 61 | # Mac files 62 | .DS_Store 63 | 64 | # Yarn 65 | yarn-error.log 66 | .pnp/ 67 | .pnp.js 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | -------------------------------------------------------------------------------- /website/api/contact.js: -------------------------------------------------------------------------------- 1 | import * as postmark from 'postmark' 2 | 3 | const client = new postmark.ServerClient(process.env.POSTMARK_API_TOKEN) 4 | 5 | export default async function handler(request, response) { 6 | const body = request.body 7 | 8 | if (!body.name || !body.email || !body.message) { 9 | return response.status(400).json({ data: 'Invalid' }) 10 | } 11 | 12 | const subject = `Message from ${body.name} (${body.email}) via jotai.org` 13 | 14 | const message = ` 15 | Name: ${body.name}\r\n 16 | Email: ${body.email}\r\n 17 | Message: ${body.message} 18 | ` 19 | 20 | try { 21 | await client.sendEmail({ 22 | From: 'noreply@jotai.org', 23 | To: process.env.EMAIL_RECIPIENTS, 24 | Subject: subject, 25 | ReplyTo: body.email, 26 | TextBody: message, 27 | }) 28 | 29 | response.status(200).json({ status: 'Sent' }) 30 | } catch (error) { 31 | response.status(500).json({ status: 'Not sent' }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /website/gatsby-browser.js: -------------------------------------------------------------------------------- 1 | import './src/styles/index.css' 2 | 3 | export { wrapRootElement, wrapPageElement } from './gatsby-shared.js' 4 | -------------------------------------------------------------------------------- /website/gatsby-shared.js: -------------------------------------------------------------------------------- 1 | import { MDXProvider } from '@mdx-js/react' 2 | import { Provider as JotaiProvider, createStore } from 'jotai' 3 | import { countAtom, menuAtom, searchAtom, textAtom } from './src/atoms/index.js' 4 | import { CodeSandbox } from './src/components/code-sandbox.js' 5 | import { Code } from './src/components/code.js' 6 | import { InlineCode } from './src/components/inline-code.js' 7 | import { Layout } from './src/components/layout.js' 8 | import { A, H2, H3, H4, H5 } from './src/components/mdx.js' 9 | import { Stackblitz } from './src/components/stackblitz.js' 10 | import { TOC } from './src/components/toc.js' 11 | 12 | const store = createStore() 13 | 14 | store.set(countAtom, 0) 15 | store.set(menuAtom, false) 16 | store.set(searchAtom, false) 17 | store.set(textAtom, 'hello') 18 | 19 | const components = { 20 | code: Code, 21 | inlineCode: InlineCode, 22 | CodeSandbox, 23 | Stackblitz, 24 | TOC, 25 | h2: H2, 26 | h3: H3, 27 | h4: H4, 28 | h5: H5, 29 | a: A, 30 | } 31 | 32 | export const wrapRootElement = ({ element }) => ( 33 | 34 | {element} 35 | 36 | ) 37 | 38 | export const wrapPageElement = ({ element, props }) => { 39 | return {element} 40 | } 41 | -------------------------------------------------------------------------------- /website/gatsby-ssr.js: -------------------------------------------------------------------------------- 1 | export { wrapRootElement, wrapPageElement } from './gatsby-shared.js' 2 | 3 | export const onRenderBody = ({ setHtmlAttributes, setPreBodyComponents }) => { 4 | setHtmlAttributes({ lang: 'en' }) 5 | setPreBodyComponents([ 6 |