├── .prettierignore
├── benchmarks
├── .gitignore
├── simple-read.ts
├── simple-write.ts
└── subscribe-write.ts
├── website
├── src
│ ├── styles
│ │ ├── base.css
│ │ ├── utilities.css
│ │ ├── index.css
│ │ ├── fonts.css
│ │ ├── pmndrs.css
│ │ └── layout.css
│ ├── components
│ │ ├── external-link.js
│ │ ├── main.js
│ │ ├── wrapper.js
│ │ ├── footer.js
│ │ ├── client-only.js
│ │ ├── headline.js
│ │ ├── tabs.js
│ │ ├── search-button.js
│ │ ├── inline-code.js
│ │ ├── stackblitz.js
│ │ ├── support-modal.js
│ │ ├── code.js
│ │ ├── modal.js
│ │ ├── code-sandbox.js
│ │ ├── layout.js
│ │ ├── shelf.js
│ │ ├── mdx.js
│ │ ├── extensions-demo.js
│ │ ├── credits.js
│ │ ├── intro.js
│ │ ├── toggle.js
│ │ ├── jotai.js
│ │ ├── core-demo.js
│ │ ├── menu.js
│ │ ├── meta.js
│ │ └── logo.js
│ ├── pages
│ │ ├── 404.js
│ │ └── docs
│ │ │ └── {Mdx.slug}.js
│ ├── hooks
│ │ └── index.js
│ ├── atoms
│ │ └── index.js
│ ├── utils
│ │ └── index.js
│ └── api
│ │ └── contact.js
├── public
│ └── robots.txt
├── static
│ ├── fonts
│ │ ├── meslo.woff2
│ │ ├── inter-var.woff2
│ │ └── inter-italic-var.woff2
│ ├── robots.txt
│ └── favicon.svg
├── gatsby-browser.js
├── postcss.config.js
├── reach-router.js
├── jsconfig.json
├── README.md
├── .babelrc
├── api
│ └── contact.js
├── gatsby-ssr.js
├── .gitignore
├── gatsby-shared.js
├── tailwind.config.js
└── package.json
├── tests
├── setup.ts
├── test-utils.ts
├── vanilla
│ ├── utils
│ │ ├── atomWithRefresh.test.ts
│ │ ├── atomWithReset.test.ts
│ │ ├── loadable.test.ts
│ │ ├── atomWithLazy.test.ts
│ │ └── types.test.tsx
│ └── basic.test.tsx
└── react
│ ├── utils
│ └── types.test.tsx
│ └── provider.test.tsx
├── pnpm-workspace.yaml
├── examples
├── hello
│ ├── src
│ │ ├── vite-env.d.ts
│ │ ├── index.tsx
│ │ ├── style.css
│ │ └── App.tsx
│ ├── vite.config.ts
│ ├── index.html
│ ├── package.json
│ ├── tsconfig.json
│ ├── README.md
│ └── public
│ │ └── index.html
├── mega-form
│ ├── src
│ │ ├── vite-env.d.ts
│ │ ├── index.tsx
│ │ ├── useAtomSlice.ts
│ │ ├── style.css
│ │ └── initialValue.ts
│ ├── vite.config.ts
│ ├── index.html
│ ├── public
│ │ └── index.html
│ ├── tsconfig.json
│ ├── README.md
│ └── package.json
├── starter
│ ├── src
│ │ ├── vite-env.d.ts
│ │ ├── assets
│ │ │ └── jotai-mascot.png
│ │ ├── index.css
│ │ └── index.tsx
│ ├── vite.config.ts
│ ├── index.html
│ ├── package.json
│ ├── tsconfig.json
│ └── README.md
├── todos
│ ├── src
│ │ ├── vite-env.d.ts
│ │ ├── index.tsx
│ │ └── styles.css
│ ├── vite.config.ts
│ ├── tsconfig.json
│ ├── index.html
│ ├── package.json
│ ├── README.md
│ └── public
│ │ └── index.html
├── hacker_news
│ ├── src
│ │ ├── vite-env.d.ts
│ │ ├── index.tsx
│ │ ├── styles.css
│ │ └── App.tsx
│ ├── vite.config.ts
│ ├── index.html
│ ├── tsconfig.json
│ ├── package.json
│ ├── README.md
│ └── public
│ │ └── index.html
├── text_length
│ ├── src
│ │ ├── react-app-env.d.ts
│ │ ├── index.tsx
│ │ └── App.tsx
│ ├── public
│ │ ├── castle.jpg
│ │ └── snippet.png
│ ├── vite.config.ts
│ ├── package.json
│ ├── tsconfig.json
│ ├── README.md
│ └── index.html
└── todos_with_atomFamily
│ ├── src
│ ├── vite-env.d.ts
│ ├── index.tsx
│ └── styles.css
│ ├── vite.config.ts
│ ├── index.html
│ ├── tsconfig.json
│ ├── package.json
│ ├── README.md
│ └── public
│ └── index.html
├── src
├── index.ts
├── utils.ts
├── types.d.ts
├── vanilla
│ ├── utils
│ │ ├── constants.ts
│ │ ├── atomWithLazy.ts
│ │ ├── atomWithReducer.ts
│ │ ├── atomWithReset.ts
│ │ ├── atomWithDefault.ts
│ │ ├── atomWithRefresh.ts
│ │ ├── selectAtom.ts
│ │ ├── freezeAtom.ts
│ │ └── loadable.ts
│ ├── typeUtils.ts
│ ├── utils.ts
│ └── store.ts
├── react.ts
├── react
│ ├── utils.ts
│ ├── utils
│ │ ├── useResetAtom.ts
│ │ ├── useAtomCallback.ts
│ │ ├── useReducerAtom.ts
│ │ └── useHydrateAtoms.ts
│ ├── useSetAtom.ts
│ ├── Provider.ts
│ └── useAtom.ts
├── vanilla.ts
└── babel
│ ├── preset.ts
│ ├── utils.ts
│ └── plugin-debug-label.ts
├── img
├── jotai-mascot.png
├── jotai-header-dark.png
├── jotai-opengraph.png
├── jotai-course-banner.jpg
└── jotai-header-light.png
├── .github
├── pull_request_template.md
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── config.yml
├── workflows
│ ├── compressed-size.yml
│ ├── preview-release.yml
│ ├── livecodes-post-comment.yml
│ ├── livecodes-preview.yml
│ ├── publish.yml
│ ├── test.yml
│ ├── ecosystem-ci.yml
│ └── test-multiple-versions.yml
├── DISCUSSION_TEMPLATE
│ └── bug-report.yml
└── FUNDING.yml
├── docs
├── recipes
│ ├── atom-with-refresh.mdx
│ ├── atom-with-toggle-and-storage.mdx
│ ├── use-reducer-atom.mdx
│ ├── atom-with-toggle.mdx
│ ├── atom-with-compare.mdx
│ ├── custom-useatom-hooks.mdx
│ ├── use-atom-effect.mdx
│ └── atom-with-broadcast.mdx
├── guides
│ ├── waku.mdx
│ ├── remix.mdx
│ ├── vite.mdx
│ ├── react-native.mdx
│ ├── using-store-outside-react.mdx
│ └── resettable.mdx
├── utilities
│ ├── reducer.mdx
│ └── callback.mdx
├── core
│ ├── store.mdx
│ └── provider.mdx
├── extensions
│ ├── zustand.mdx
│ ├── redux.mdx
│ ├── optics.mdx
│ └── trpc.mdx
├── basics
│ ├── concepts.mdx
│ └── showcase.mdx
├── index.mdx
└── third-party
│ └── history.mdx
├── .codesandbox
└── ci.json
├── .gitignore
├── babel.config.mjs
├── tsconfig.json
├── LICENSE
├── vitest.config.mts
└── .livecodes
└── react.json
/.prettierignore:
--------------------------------------------------------------------------------
1 | dist
2 | pnpm-lock.yaml
3 |
--------------------------------------------------------------------------------
/benchmarks/.gitignore:
--------------------------------------------------------------------------------
1 | /*.json
2 | /*.html
3 |
--------------------------------------------------------------------------------
/website/src/styles/base.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 |
--------------------------------------------------------------------------------
/website/src/styles/utilities.css:
--------------------------------------------------------------------------------
1 | @tailwind utilities;
2 |
--------------------------------------------------------------------------------
/tests/setup.ts:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom/vitest'
2 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - .
3 | minimumReleaseAge: 1440
4 |
--------------------------------------------------------------------------------
/examples/hello/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/examples/mega-form/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/examples/starter/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/examples/todos/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './vanilla.ts'
2 | export * from './react.ts'
3 |
--------------------------------------------------------------------------------
/examples/hacker_news/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/examples/text_length/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/img/jotai-mascot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pmndrs/jotai/HEAD/img/jotai-mascot.png
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | export * from './vanilla/utils.ts'
2 | export * from './react/utils.ts'
3 |
--------------------------------------------------------------------------------
/examples/todos_with_atomFamily/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/img/jotai-header-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pmndrs/jotai/HEAD/img/jotai-header-dark.png
--------------------------------------------------------------------------------
/img/jotai-opengraph.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pmndrs/jotai/HEAD/img/jotai-opengraph.png
--------------------------------------------------------------------------------
/src/types.d.ts:
--------------------------------------------------------------------------------
1 | declare interface ImportMeta {
2 | env?: {
3 | MODE: string
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/img/jotai-course-banner.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pmndrs/jotai/HEAD/img/jotai-course-banner.jpg
--------------------------------------------------------------------------------
/img/jotai-header-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pmndrs/jotai/HEAD/img/jotai-header-light.png
--------------------------------------------------------------------------------
/website/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Allow: /
3 | Sitemap: https://jotai.org/sitemap/sitemap-index.xml
4 |
--------------------------------------------------------------------------------
/website/static/fonts/meslo.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pmndrs/jotai/HEAD/website/static/fonts/meslo.woff2
--------------------------------------------------------------------------------
/website/static/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Allow: /
3 | Sitemap: https://jotai.org/sitemap/sitemap-index.xml
4 |
--------------------------------------------------------------------------------
/website/static/fonts/inter-var.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pmndrs/jotai/HEAD/website/static/fonts/inter-var.woff2
--------------------------------------------------------------------------------
/examples/text_length/public/castle.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pmndrs/jotai/HEAD/examples/text_length/public/castle.jpg
--------------------------------------------------------------------------------
/examples/text_length/public/snippet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pmndrs/jotai/HEAD/examples/text_length/public/snippet.png
--------------------------------------------------------------------------------
/examples/starter/src/assets/jotai-mascot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pmndrs/jotai/HEAD/examples/starter/src/assets/jotai-mascot.png
--------------------------------------------------------------------------------
/website/static/fonts/inter-italic-var.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pmndrs/jotai/HEAD/website/static/fonts/inter-italic-var.woff2
--------------------------------------------------------------------------------
/website/gatsby-browser.js:
--------------------------------------------------------------------------------
1 | import './src/styles/index.css'
2 |
3 | export { wrapRootElement, wrapPageElement } from './gatsby-shared.js'
4 |
--------------------------------------------------------------------------------
/src/vanilla/utils/constants.ts:
--------------------------------------------------------------------------------
1 | export const RESET: unique symbol = Symbol(
2 | import.meta.env?.MODE !== 'production' ? 'RESET' : '',
3 | )
4 |
--------------------------------------------------------------------------------
/website/static/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 | 👻
3 |
4 |
--------------------------------------------------------------------------------
/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/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/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/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/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_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 |
--------------------------------------------------------------------------------
/website/src/styles/index.css:
--------------------------------------------------------------------------------
1 | @import './base.css';
2 | @import './fonts.css';
3 | @import './pmndrs.css';
4 | @import './layout.css';
5 | @import './components.css';
6 | @import './utilities.css';
7 |
--------------------------------------------------------------------------------
/website/src/styles/fonts.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=block');
2 | @import url('https://fonts.googleapis.com/css2?family=Fira+Code&display=block');
3 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/website/postcss.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | module.exports = {
3 | plugins: {
4 | 'postcss-import': {},
5 | tailwindcss: {},
6 | autoprefixer: {},
7 | },
8 | }
9 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/website/reach-router.js:
--------------------------------------------------------------------------------
1 | exports.default = function (source) {
2 | if (source.includes('exports.BaseContext')) {
3 | return source
4 | } else {
5 | return source + 'exports.BaseContext = BaseContext;'
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/website/src/components/external-link.js:
--------------------------------------------------------------------------------
1 | export const ExternalLink = ({ to, children, ...rest }) => {
2 | return (
3 |
4 | {children}
5 |
6 | )
7 | }
8 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/website/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "./",
4 | "checkJs": true,
5 | "removeComments": true
6 | },
7 | "include": ["**/*.js", "**/*.jsx"],
8 | "exclude": ["node_modules", "public"]
9 | }
10 |
--------------------------------------------------------------------------------
/website/src/pages/404.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 | import { navigate } from 'gatsby'
3 |
4 | export default function NotFoundPage() {
5 | useEffect(() => {
6 | navigate('/')
7 | }, [])
8 |
9 | return null
10 | }
11 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/website/src/components/main.js:
--------------------------------------------------------------------------------
1 | export const Main = ({ children, ...rest }) => {
2 | return (
3 |
7 | {children}
8 |
9 | )
10 | }
11 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/website/src/components/wrapper.js:
--------------------------------------------------------------------------------
1 | export const Wrapper = ({ children, ...rest }) => {
2 | return (
3 |
7 | {children}
8 |
9 | )
10 | }
11 |
--------------------------------------------------------------------------------
/website/src/components/footer.js:
--------------------------------------------------------------------------------
1 | import { Credits } from '../components/credits.js'
2 |
3 | export const Footer = () => {
4 | return (
5 |
11 | )
12 | }
13 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/website/src/components/client-only.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 |
3 | export const ClientOnly = ({ children }) => {
4 | const [hasMounted, setHasMounted] = useState(false)
5 |
6 | useEffect(() => {
7 | setHasMounted(true)
8 | }, [])
9 |
10 | if (!hasMounted) {
11 | return null
12 | }
13 |
14 | return children
15 | }
16 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/website/src/components/headline.js:
--------------------------------------------------------------------------------
1 | import cx from 'classnames'
2 |
3 | export const Headline = ({ className = '', children }) => {
4 | return (
5 |
11 | {children}
12 |
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/website/README.md:
--------------------------------------------------------------------------------
1 | ## Getting Started
2 |
3 | Install the dependencies:
4 |
5 | ```bash
6 | pnpm install --ignore-workspace
7 | ```
8 |
9 | Make a cache directory:
10 |
11 | ```bash
12 | mkdir .cache
13 | ```
14 |
15 | Run the development server:
16 |
17 | ```bash
18 | pnpm run dev
19 | ```
20 |
21 | Open [http://localhost:9000](http://localhost:9000) with your browser to see the result.
22 |
--------------------------------------------------------------------------------
/website/src/components/tabs.js:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react'
2 |
3 | export const Tabs = ({ tabs = {} }) => {
4 | const tabContents = useMemo(() => Object.values(tabs), [tabs])
5 |
6 | return (
7 | <>
8 |
9 | {tabContents.map((content) => (
10 |
{content}
11 | ))}
12 |
13 | >
14 | )
15 | }
16 |
--------------------------------------------------------------------------------
/examples/todos/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Jotai Examples | Todos
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/website/src/components/search-button.js:
--------------------------------------------------------------------------------
1 | import { useSetAtom } from 'jotai'
2 | import { searchAtom } from '../atoms/index.js'
3 | import { Button } from '../components/button.js'
4 |
5 | export const SearchButton = (props) => {
6 | const setIsSearchOpen = useSetAtom(searchAtom)
7 |
8 | return (
9 | setIsSearchOpen(true)} icon="search" dark {...props}>
10 | Search...
11 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/examples/hacker_news/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Jotai Examples | Hacker News
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/tests/test-utils.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react'
2 |
3 | export function sleep(ms: number): Promise {
4 | return new Promise((resolve) => setTimeout(resolve, ms))
5 | }
6 |
7 | export function useCommitCount(): number {
8 | const commitCountRef = useRef(1)
9 | useEffect(() => {
10 | commitCountRef.current += 1
11 | })
12 | // eslint-disable-next-line react-hooks/refs
13 | return commitCountRef.current
14 | }
15 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Jotai Examples | Mega Form
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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@v6
10 | - uses: pnpm/action-setup@v4
11 | - uses: actions/setup-node@v6
12 | with:
13 | node-version: 'lts/*'
14 | cache: 'pnpm'
15 | - uses: preactjs/compressed-size-action@v3
16 | with:
17 | pattern: './dist/**/*.{js,mjs}'
18 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/website/src/components/inline-code.js:
--------------------------------------------------------------------------------
1 | import cx from 'classnames'
2 |
3 | export const InlineCode = ({ dark = false, children }) => {
4 | return (
5 |
13 | {children}
14 |
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/tests/vanilla/utils/atomWithRefresh.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } 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 |
--------------------------------------------------------------------------------
/.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/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@v6
10 | - uses: pnpm/action-setup@v4
11 | - uses: actions/setup-node@v6
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 |
--------------------------------------------------------------------------------
/website/src/hooks/index.js:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect } from 'react'
2 |
3 | export const useOnEscape = (handler) => {
4 | const handleEscape = useCallback(
5 | ({ code }) => {
6 | if (code === 'Escape') {
7 | handler()
8 | }
9 | },
10 | [handler],
11 | )
12 |
13 | useEffect(() => {
14 | document.addEventListener('keydown', handleEscape, false)
15 |
16 | return () => {
17 | document.removeEventListener('keydown', handleEscape, false)
18 | }
19 | }, [handleEscape])
20 | }
21 |
--------------------------------------------------------------------------------
/examples/starter/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Jotai Examples | Starter
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/website/src/atoms/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/extensions */
2 | import { atom } from 'jotai'
3 | import { atomWithStorage } from 'jotai/utils'
4 | import { atomWithImmer } from 'jotai-immer'
5 |
6 | export const menuAtom = atom(false)
7 | export const searchAtom = atom(false)
8 | export const helpAtom = atom(false)
9 | export const textAtom = atom('hello')
10 | export const uppercaseAtom = atom((get) => get(textAtom).toUpperCase())
11 | export const darkModeAtom = atomWithStorage('darkMode', false)
12 | export const countAtom = atomWithImmer(0)
13 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/examples/hello/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 | Jotai Examples | Hello
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/.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@v6
10 | - uses: pnpm/action-setup@v4
11 | - uses: actions/setup-node@v6
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 |
--------------------------------------------------------------------------------
/website/src/utils/index.js:
--------------------------------------------------------------------------------
1 | import { Children, isValidElement } from 'react'
2 | import kebabCase from 'just-kebab-case'
3 |
4 | export const getAnchor = (value) => {
5 | return kebabCase(getTextContent(value).toLowerCase().replaceAll("'", ''))
6 | }
7 |
8 | const getTextContent = (children) => {
9 | let text = ''
10 |
11 | Children.toArray(children).forEach((child) => {
12 | if (typeof child === 'string') {
13 | text += child
14 | } else if (isValidElement(child) && child.props.children) {
15 | text += getTextContent(child.props.children)
16 | }
17 | })
18 |
19 | return text
20 | }
21 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish
2 |
3 | on:
4 | release:
5 | types: [published]
6 |
7 | permissions:
8 | id-token: write
9 | contents: read
10 |
11 | jobs:
12 | publish:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v6
16 | - uses: pnpm/action-setup@v4
17 | - uses: actions/setup-node@v6
18 | with:
19 | node-version: 'lts/*'
20 | registry-url: 'https://registry.npmjs.org'
21 | cache: 'pnpm'
22 | - run: pnpm install
23 | - run: pnpm run build
24 | - run: npm publish
25 | working-directory: dist
26 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/examples/hello/README.md:
--------------------------------------------------------------------------------
1 | # Hello [](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/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/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/starter/README.md:
--------------------------------------------------------------------------------
1 | # Starter [](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 |
--------------------------------------------------------------------------------
/.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@v6
14 | - uses: pnpm/action-setup@v4
15 | - uses: actions/setup-node@v6
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 |
--------------------------------------------------------------------------------
/examples/mega-form/README.md:
--------------------------------------------------------------------------------
1 | # Mega form [](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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/hello/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 | Jotai Examples | Hello
15 |
16 |
17 | You need to enable JavaScript to run this app.
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/website/src/components/stackblitz.js:
--------------------------------------------------------------------------------
1 | export const Stackblitz = ({ id, file }) => {
2 | return (
3 |
4 |
12 |
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/examples/text_length/README.md:
--------------------------------------------------------------------------------
1 | # Text Length [](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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/website/src/components/support-modal.js:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react'
2 | import { useAtom } from 'jotai'
3 | import { helpAtom } from '../atoms/index.js'
4 | import { Modal } from '../components/modal.js'
5 | import { Support } from '../components/support.js'
6 |
7 | export const SupportModal = () => {
8 | const [showHelp, setShowHelp] = useAtom(helpAtom)
9 |
10 | const onClose = useCallback(() => {
11 | setShowHelp(false)
12 | }, [setShowHelp])
13 |
14 | return (
15 |
16 |
17 |
18 |
19 |
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/examples/hacker_news/README.md:
--------------------------------------------------------------------------------
1 | # Hacker News [](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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/website/src/components/code.js:
--------------------------------------------------------------------------------
1 | import Highlight, { defaultProps } from 'prism-react-renderer'
2 |
3 | export const Code = ({ language = 'jsx', children }) => {
4 | const code = children.trim()
5 |
6 | return (
7 |
13 | {({ className, style, tokens, getLineProps, getTokenProps }) => (
14 |
15 | {tokens.map((line, i) => (
16 |
17 | {line.map((token, key) => (
18 |
19 | ))}
20 |
21 | ))}
22 |
23 | )}
24 |
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/website/src/components/modal.js:
--------------------------------------------------------------------------------
1 | import { Dialog } from '@headlessui/react'
2 | import { RemoveScroll } from 'react-remove-scroll'
3 |
4 | export const Modal = ({ isOpen, onClose, children, ...rest }) => {
5 | return (
6 |
7 |
11 |
12 |
13 |
14 | {children}
15 |
16 |
17 |
18 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/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 | ,
26 | )
27 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/website/src/components/code-sandbox.js:
--------------------------------------------------------------------------------
1 | export const CodeSandbox = ({ id, tests }) => {
2 | return (
3 |
4 |
14 |
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/examples/todos/README.md:
--------------------------------------------------------------------------------
1 | # Todos [](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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/src/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/website/src/components/layout.js:
--------------------------------------------------------------------------------
1 | import { ClientOnly } from '../components/client-only.js'
2 | import { Footer } from '../components/footer.js'
3 | import { Main } from '../components/main.js'
4 | import { Menu } from '../components/menu.js'
5 | import { SearchModal } from '../components/search-modal.js'
6 | import { Shelf } from '../components/shelf.js'
7 | import { Sidebar } from '../components/sidebar.js'
8 | import { SupportModal } from '../components/support-modal.js'
9 | import { Toggle } from '../components/toggle.js'
10 | import { Wrapper } from '../components/wrapper.js'
11 |
12 | export const Layout = ({ isDocs = false, children }) => {
13 | return (
14 | <>
15 |
16 |
17 |
18 | {children}
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | >
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/website/src/pages/docs/{Mdx.slug}.js:
--------------------------------------------------------------------------------
1 | import { graphql } from 'gatsby'
2 | import { MDXRenderer } from 'gatsby-plugin-mdx'
3 | import { Jotai } from '../../components/jotai.js'
4 | import { Meta } from '../../components/meta.js'
5 |
6 | export default function DocsPage({ data }) {
7 | const { frontmatter, body } = data.mdx
8 | const { title } = frontmatter
9 |
10 | return (
11 | <>
12 |
13 |
14 |
15 | {title}
16 |
17 | {body}
18 |
19 | >
20 | )
21 | }
22 |
23 | export const Head = ({ data }) => {
24 | const { slug, frontmatter } = data.mdx
25 | const { title, description } = frontmatter
26 | const uri = `docs/${slug}`
27 |
28 | return
29 | }
30 |
31 | export const pageQuery = graphql`
32 | query PageQuery($slug: String) {
33 | mdx(slug: { eq: $slug }) {
34 | slug
35 | frontmatter {
36 | title
37 | description
38 | }
39 | body
40 | }
41 | }
42 | `
43 |
--------------------------------------------------------------------------------
/examples/todos_with_atomFamily/README.md:
--------------------------------------------------------------------------------
1 | # Todos with atomFamily [](https://stackblitz.com/github/pmndrs/jotai/tree/main/examples/todos_with_atomFamily)
2 |
3 | > **⚠️ Note:** `atomFamily` from `jotai/utils` is deprecated and will be removed in v3. For new projects, please use the [`jotai-family`](https://github.com/jotaijs/jotai-family) package instead.
4 |
5 | ## Description
6 |
7 | 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`.
8 |
9 | ## Set up locally
10 |
11 | ```bash
12 | git clone https://github.com/pmndrs/jotai
13 |
14 | # install project dependencies & build the library
15 | cd jotai && pnpm install
16 |
17 | # move to the examples folder & install dependencies
18 | cd examples/todos_with_atomFamily && pnpm install
19 |
20 | # start the dev server
21 | pnpm dev
22 | ```
23 |
24 | ## Set up on `StackBlitz`
25 |
26 | Link: https://stackblitz.com/github/pmndrs/jotai/tree/main/examples/todos_with_atomFamily
27 |
--------------------------------------------------------------------------------
/examples/text_length/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Jotai Examples | Text Length
7 |
18 |
19 |
20 |
21 |
24 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/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 atomic architecture will encourage you to split logic and data, providing a top-most experience to control every one of your render ([or commits, to be precise](https://legacy.reactjs.org/blog/2018/09/10/introducing-the-react-profiler.html#browsing-commits)) and therefore reach the best performances.
20 |
21 | And always remember that renders have to be fast, split calculation logic to async actions.
22 |
--------------------------------------------------------------------------------
/src/vanilla/store.ts:
--------------------------------------------------------------------------------
1 | import { INTERNAL_buildStoreRev2 as INTERNAL_buildStore } from './internals.ts'
2 | import type { INTERNAL_Store } from './internals.ts'
3 |
4 | export type Store = INTERNAL_Store
5 |
6 | let overiddenCreateStore: typeof createStore | undefined
7 |
8 | export function INTERNAL_overrideCreateStore(
9 | fn: (prev: typeof createStore | undefined) => typeof createStore,
10 | ): void {
11 | overiddenCreateStore = fn(overiddenCreateStore)
12 | }
13 |
14 | export function createStore(): Store {
15 | if (overiddenCreateStore) {
16 | return overiddenCreateStore()
17 | }
18 | return INTERNAL_buildStore()
19 | }
20 |
21 | let defaultStore: Store | undefined
22 |
23 | export function getDefaultStore(): Store {
24 | if (!defaultStore) {
25 | defaultStore = createStore()
26 | if (import.meta.env?.MODE !== 'production') {
27 | ;(globalThis as any).__JOTAI_DEFAULT_STORE__ ||= defaultStore
28 | if ((globalThis as any).__JOTAI_DEFAULT_STORE__ !== defaultStore) {
29 | console.warn(
30 | 'Detected multiple Jotai instances. It may cause unexpected behavior with the default store. https://github.com/pmndrs/jotai/discussions/2044',
31 | )
32 | }
33 | }
34 | }
35 | return defaultStore
36 | }
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | ,
34 | ])
35 | }
36 |
--------------------------------------------------------------------------------
/tests/vanilla/utils/loadable.test.ts:
--------------------------------------------------------------------------------
1 | import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2 | import { atom, createStore } from 'jotai/vanilla'
3 | import { loadable } from 'jotai/vanilla/utils'
4 |
5 | describe('loadable', () => {
6 | beforeEach(() => {
7 | vi.useFakeTimers()
8 | })
9 |
10 | afterEach(() => {
11 | vi.useRealTimers()
12 | })
13 |
14 | it('should return fulfilled value of an already resolved async atom', async () => {
15 | const store = createStore()
16 | const asyncAtom = atom(Promise.resolve('concrete'))
17 |
18 | expect(store.get(loadable(asyncAtom))).toEqual({
19 | state: 'loading',
20 | })
21 | await vi.advanceTimersByTimeAsync(0)
22 | expect(store.get(loadable(asyncAtom))).toEqual({
23 | state: 'hasData',
24 | data: 'concrete',
25 | })
26 | })
27 |
28 | it('should get the latest loadable state after the promise resolves', async () => {
29 | const store = createStore()
30 | const asyncAtom = atom(Promise.resolve())
31 | const loadableAtom = loadable(asyncAtom)
32 |
33 | expect(store.get(loadableAtom)).toHaveProperty('state', 'loading')
34 | await vi.advanceTimersByTimeAsync(0)
35 | expect(store.get(loadableAtom)).toHaveProperty('state', 'hasData')
36 | })
37 | })
38 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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', () => {
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', () => {
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', () => {
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
22 | +1
23 |
24 | >
25 | )
26 | }
27 |
28 | function App() {
29 | return (
30 |
31 |
32 |
40 |
41 |
42 |
Jotai Starter
43 |
44 |
45 |
46 | )
47 | }
48 |
49 | createRoot(document.getElementById('root')!).render(
50 |
51 |
52 | ,
53 | )
54 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/website/src/components/shelf.js:
--------------------------------------------------------------------------------
1 | import { useSetAtom } from 'jotai'
2 | import { menuAtom } from '../atoms/index.js'
3 | import { Button } from '../components/button.js'
4 |
5 | export const Shelf = () => {
6 | const setIsMenuOpen = useSetAtom(menuAtom)
7 |
8 | return (
9 |
10 |
11 |
19 | GitHub
20 |
21 |
29 | npm
30 |
31 | setIsMenuOpen(true)}
34 | className="font-bold uppercase tracking-wider"
35 | dark
36 | small
37 | >
38 | Docs
39 |
40 |
41 |
42 |
43 | )
44 | }
45 |
--------------------------------------------------------------------------------
/website/src/components/mdx.js:
--------------------------------------------------------------------------------
1 | import { getAnchor } from '../utils/index.js'
2 |
3 | export const H2 = ({ children }) => {
4 | const anchor = getAnchor(children)
5 | const link = `#${anchor}`
6 |
7 | return (
8 |
11 | )
12 | }
13 |
14 | export const H3 = ({ children }) => {
15 | const anchor = getAnchor(children)
16 | const link = `#${anchor}`
17 |
18 | return (
19 |
22 | )
23 | }
24 |
25 | export const H4 = ({ children }) => {
26 | const anchor = getAnchor(children)
27 | const link = `#${anchor}`
28 |
29 | return (
30 |
33 | )
34 | }
35 |
36 | export const H5 = ({ children }) => {
37 | const anchor = getAnchor(children)
38 | const link = `#${anchor}`
39 |
40 | return (
41 |
44 | )
45 | }
46 |
47 | export const A = ({ href, children, ...rest }) => {
48 | if (href.startsWith('http')) {
49 | return (
50 |
51 | {children}
52 |
53 | )
54 | }
55 |
56 | const newHref = href.replace('.mdx', '')
57 |
58 | return (
59 |
60 | {children}
61 |
62 | )
63 | }
64 |
--------------------------------------------------------------------------------
/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 | setState((prev) => ({ ...prev, count: prev.count + 1 }))}
45 | >
46 | button
47 |
48 | >
49 | )
50 | }
51 | ```
52 |
53 | ### Examples
54 |
55 |
56 |
--------------------------------------------------------------------------------
/website/src/components/extensions-demo.js:
--------------------------------------------------------------------------------
1 | import { useAtom } from 'jotai'
2 | import { countAtom } from '../atoms/index.js'
3 | import { Button } from '../components/button.js'
4 | import { Code } from '../components/code.js'
5 |
6 | export const ExtensionsDemo = () => {
7 | const [count, setCount] = useAtom(countAtom)
8 |
9 | const increment = () => setCount((c) => (c = c + 1))
10 |
11 | const code = `import { useAtom } from 'jotai'
12 | import { atomWithImmer } from 'jotai-immer'
13 |
14 | // Create a new atom with an immer-based write function
15 | const countAtom = atomWithImmer(0)
16 |
17 | const Counter = () => {
18 | const [count] = useAtom(countAtom)
19 | return (
20 | count: {count}
21 | )
22 | }
23 |
24 | const Controls = () => {
25 | // setCount === update: (draft: Draft) => void
26 | const [, setCount] = useAtom(countAtom)
27 | const increment = () => setCount((c) => (c = c + 1))
28 | return (
29 | +1
30 | )
31 | }`
32 |
33 | return (
34 | <>
35 |
36 |
37 | Increment
38 |
39 |
40 | {count}
41 |
42 |
43 | {code}
44 | >
45 | )
46 | }
47 |
--------------------------------------------------------------------------------
/src/vanilla/utils/atomWithDefault.ts:
--------------------------------------------------------------------------------
1 | import { atom } from '../../vanilla.ts'
2 | import type { 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 | type DefaultSetStateAction =
12 | | Value
13 | | typeof RESET
14 | | ((prev: Value) => Value | typeof RESET)
15 |
16 | export function atomWithDefault(
17 | getDefault: Read], void>,
18 | ): WritableAtom], void> {
19 | const EMPTY = Symbol()
20 | const overwrittenAtom = atom(EMPTY)
21 |
22 | if (import.meta.env?.MODE !== 'production') {
23 | overwrittenAtom.debugPrivate = true
24 | }
25 |
26 | const anAtom: WritableAtom], void> =
27 | atom(
28 | (get, options) => {
29 | const overwritten = get(overwrittenAtom)
30 | if (overwritten !== EMPTY) {
31 | return overwritten
32 | }
33 | return getDefault(get, options)
34 | },
35 | (get, set, update) => {
36 | const newValue =
37 | typeof update === 'function'
38 | ? (update as (prev: Value) => Value)(get(anAtom))
39 | : update
40 | set(overwrittenAtom, newValue === RESET ? EMPTY : newValue)
41 | },
42 | )
43 | return anAtom
44 | }
45 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/benchmarks/simple-read.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env npx tsx
2 |
3 | ///
4 |
5 | import path from 'node:path'
6 | import { fileURLToPath } from 'node:url'
7 | import { add, complete, cycle, save, suite } from 'benny'
8 | import { atom } from '../src/vanilla/atom.ts'
9 | import type { PrimitiveAtom } from '../src/vanilla/atom.ts'
10 | import { createStore } from '../src/vanilla/store.ts'
11 |
12 | const __dirname = path.dirname(fileURLToPath(import.meta.url))
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.set(a, i)
23 | }
24 | if (!targetAtom) {
25 | throw new Error()
26 | }
27 | return [store, targetAtom] as const
28 | }
29 |
30 | const main = async () => {
31 | await suite(
32 | 'simple-read',
33 | ...[2, 3, 4, 5, 6].map((n) =>
34 | add(`atoms=${10 ** n}`, () => {
35 | const [store, targetAtom] = createStateWithAtoms(10 ** n)
36 | return () => store.get(targetAtom)
37 | }),
38 | ),
39 | cycle(),
40 | complete(),
41 | save({
42 | folder: __dirname,
43 | file: 'simple-read',
44 | format: 'json',
45 | }),
46 | save({
47 | folder: __dirname,
48 | file: 'simple-read',
49 | format: 'chart.html',
50 | }),
51 | )
52 | }
53 |
54 | main()
55 |
--------------------------------------------------------------------------------
/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 }>
30 | > {
31 | const storeRef = useRef(null)
32 | if (store) {
33 | return createElement(StoreContext.Provider, { value: store }, children)
34 | }
35 | if (storeRef.current === null) {
36 | storeRef.current = createStore()
37 | }
38 | return createElement(
39 | StoreContext.Provider,
40 | {
41 | // TODO: If this is not a false positive, consider using useState instead of useRef like https://github.com/pmndrs/jotai/pull/2771
42 | // eslint-disable-next-line react-hooks/refs
43 | value: storeRef.current,
44 | },
45 | children,
46 | )
47 | }
48 |
--------------------------------------------------------------------------------
/benchmarks/simple-write.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env npx tsx
2 |
3 | ///
4 |
5 | import path from 'node:path'
6 | import { fileURLToPath } from 'node:url'
7 | import { add, complete, cycle, save, suite } from 'benny'
8 | import { atom } from '../src/vanilla/atom.ts'
9 | import type { PrimitiveAtom } from '../src/vanilla/atom.ts'
10 | import { createStore } from '../src/vanilla/store.ts'
11 |
12 | const __dirname = path.dirname(fileURLToPath(import.meta.url))
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.set(a, i)
23 | }
24 | if (!targetAtom) {
25 | throw new Error()
26 | }
27 | return [store, targetAtom] as const
28 | }
29 |
30 | const main = async () => {
31 | await suite(
32 | 'simple-write',
33 | ...[2, 3, 4, 5, 6].map((n) =>
34 | add(`atoms=${10 ** n}`, () => {
35 | const [store, targetAtom] = createStateWithAtoms(10 ** n)
36 | return () => store.set(targetAtom, (c) => c + 1)
37 | }),
38 | ),
39 | cycle(),
40 | complete(),
41 | save({
42 | folder: __dirname,
43 | file: 'simple-write',
44 | format: 'json',
45 | }),
46 | save({
47 | folder: __dirname,
48 | file: 'simple-write',
49 | format: 'chart.html',
50 | }),
51 | )
52 | }
53 |
54 | main()
55 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | dispatch({ type: 'INC' })}>button
50 | >
51 | )
52 | }
53 | ```
54 |
55 | ### Examples
56 |
57 |
58 |
--------------------------------------------------------------------------------
/website/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/extensions */
2 | /* eslint-disable @typescript-eslint/no-var-requires */
3 | const colors = require('tailwindcss/colors')
4 |
5 | /** @type {import('tailwindcss').Config} */
6 | module.exports = {
7 | content: [
8 | './src/**/*.js',
9 | './src/**/*.jsx',
10 | './src/**/*.ts',
11 | './src/**/*.tsx',
12 | ],
13 | darkMode: 'class',
14 | theme: {
15 | colors: {
16 | transparent: 'transparent',
17 | current: 'currentColor',
18 | black: colors.black,
19 | white: colors.white,
20 | gray: {
21 | ...colors.neutral,
22 | 350: '#bcbcbc',
23 | 650: '#494949',
24 | 950: '#0c0c0c',
25 | },
26 | blue: { ...colors.blue, 950: '#0f1d45' },
27 | red: { ...colors.red, 950: '#400f0f' },
28 | teal: { ...colors.teal, 950: '#0a2725' },
29 | },
30 | fontFamily: {
31 | sans: ['"Inter"', 'sans-serif'],
32 | mono: ['"Fira Code"', 'monospace'],
33 | },
34 | fontSize: {
35 | xs: ['0.75rem'],
36 | sm: ['0.875rem'],
37 | base: ['1rem'],
38 | lg: ['1.125rem'],
39 | xl: ['1.25rem'],
40 | '2xl': ['1.5rem'],
41 | '3xl': ['1.875rem'],
42 | '4xl': ['2.25rem'],
43 | '5xl': ['3rem'],
44 | '6xl': ['3.75rem'],
45 | '7xl': ['4.5rem'],
46 | '8xl': ['6rem'],
47 | '9xl': ['8rem'],
48 | },
49 | },
50 | plugins: [require('@tailwindcss/forms')],
51 | future: {
52 | hoverOnlyWhenSupported: true,
53 | },
54 | }
55 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 | dispatch({ type: 'inc' })}>+1
50 | dispatch({ type: 'dec' })}>-1
51 |
52 | )
53 | }
54 | ```
55 |
56 |
57 |
--------------------------------------------------------------------------------
/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 | toggle()}>
48 | isActive: {isActive ? 'yes' : 'no'}
49 |
50 | toggle(true)}>force true
51 | toggle(false)}>force false
52 | >
53 | )
54 | }
55 | ```
56 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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} +1 \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 |
--------------------------------------------------------------------------------
/website/src/components/credits.js:
--------------------------------------------------------------------------------
1 | import { Button } from '../components/button.js'
2 |
3 | export const Credits = () => {
4 | return (
5 | <>
6 |
15 | library by Daishi Kato
16 |
17 |
26 | art by Jessie Waters
27 |
28 |
37 |
38 |
site by
39 |
44 |
45 |
46 | >
47 | )
48 | }
49 |
--------------------------------------------------------------------------------
/.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@v6
13 | with:
14 | repository: 'jotaijs/jotai-ecosystem-ci'
15 | - uses: pnpm/action-setup@v4
16 | - uses: actions/setup-node@v6
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 | tee >(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 | VERBOSE: 1
38 | - uses: peter-evans/create-or-update-comment@v5
39 | with:
40 | issue-number: ${{ github.event.issue.number }}
41 | body: |
42 | ## Ecosystem CI Output
43 | ```
44 | ${{ steps.run_command.outputs.x }}
45 | ```
46 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/tests/vanilla/utils/types.test.tsx:
--------------------------------------------------------------------------------
1 | import { expectTypeOf, it } from 'vitest'
2 | import { atom } from 'jotai/vanilla'
3 | import type { Atom, SetStateAction, WritableAtom } from 'jotai/vanilla'
4 | import { selectAtom, unwrap } from 'jotai/vanilla/utils'
5 |
6 | it('selectAtom() should return the correct types', () => {
7 | const doubleCount = (x: number) => x * 2
8 | const syncAtom = atom(0)
9 | const syncSelectedAtom = selectAtom(syncAtom, doubleCount)
10 | // NOTE: expectTypeOf is not available in TypeScript 4.0.5 and below
11 | // [ONLY-TS-4.0.5] [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore
12 | expectTypeOf(syncSelectedAtom).toEqualTypeOf>()
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 | // [ONLY-TS-4.0.5] [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore
20 | expectTypeOf(syncUnwrappedAtom).toEqualTypeOf<
21 | WritableAtom], void>
22 | >()
23 |
24 | const asyncAtom = atom(Promise.resolve(0))
25 | const asyncUnwrappedAtom = unwrap(asyncAtom, getFallbackValue)
26 | // [ONLY-TS-4.0.5] [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore
27 | expectTypeOf(asyncUnwrappedAtom).toEqualTypeOf<
28 | WritableAtom>], void>
29 | >()
30 |
31 | const maybeAsyncAtom = atom(Promise.resolve(0) as number | Promise)
32 | const maybeAsyncUnwrappedAtom = unwrap(maybeAsyncAtom, getFallbackValue)
33 | // [ONLY-TS-4.0.5] [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore
34 | expectTypeOf(maybeAsyncUnwrappedAtom).toEqualTypeOf<
35 | WritableAtom>], void>
36 | >()
37 | })
38 |
--------------------------------------------------------------------------------
/benchmarks/subscribe-write.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env npx tsx
2 |
3 | ///
4 |
5 | import path from 'node:path'
6 | import { fileURLToPath } from 'node:url'
7 | import { add, complete, cycle, save, suite } from 'benny'
8 | import { atom } from '../src/vanilla/atom.ts'
9 | import type { PrimitiveAtom } from '../src/vanilla/atom.ts'
10 | import { createStore } from '../src/vanilla/store.ts'
11 |
12 | const __dirname = path.dirname(fileURLToPath(import.meta.url))
13 |
14 | const cleanupFns = new Set<() => void>()
15 | const cleanup = () => {
16 | cleanupFns.forEach((fn) => fn())
17 | cleanupFns.clear()
18 | }
19 |
20 | const createStateWithAtoms = (n: number) => {
21 | let targetAtom: PrimitiveAtom | undefined
22 | const store = createStore()
23 | for (let i = 0; i < n; ++i) {
24 | const a = atom(i)
25 | if (!targetAtom) {
26 | targetAtom = a
27 | }
28 | store.get(a)
29 | const unsub = store.sub(a, () => {
30 | store.get(a)
31 | })
32 | cleanupFns.add(unsub)
33 | }
34 | if (!targetAtom) {
35 | throw new Error()
36 | }
37 | return [store, targetAtom] as const
38 | }
39 |
40 | const main = async () => {
41 | await suite(
42 | 'subscribe-write',
43 | ...[2, 3, 4, 5, 6].map((n) =>
44 | add(`atoms=${10 ** n}`, () => {
45 | cleanup()
46 | const [store, targetAtom] = createStateWithAtoms(10 ** n)
47 | return () => store.set(targetAtom, (c) => c + 1)
48 | }),
49 | ),
50 | cycle(),
51 | complete(),
52 | save({
53 | folder: __dirname,
54 | file: 'subscribe-write',
55 | format: 'json',
56 | }),
57 | save({
58 | folder: __dirname,
59 | file: 'subscribe-write',
60 | format: 'chart.html',
61 | }),
62 | )
63 | }
64 |
65 | main()
66 |
--------------------------------------------------------------------------------
/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} setCount((c) => c + 1)}>+1
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 |
--------------------------------------------------------------------------------
/examples/todos/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 |
14 |
15 |
16 |
25 | Jotai Examples | Todos
26 |
27 |
28 |
29 | You need to enable JavaScript to run this app.
30 |
31 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/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 | You need to enable JavaScript to run this app.
30 |
31 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/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 | You need to enable JavaScript to run this app.
30 |
31 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/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 | // @ts-expect-error TS2769 [SKIP-TS-3.9.7]
10 | // [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore
11 | useHydrateAtoms([
12 | [countAtom, 'foo'],
13 | [activeAtom, 0],
14 | ])
15 | // @ts-expect-error TS2769 [SKIP-TS-5.0.4] [SKIP-TS-4.9.5] [SKIP-TS-4.8.4] [SKIP-TS-4.7.4] [SKIP-TS-4.6.4] [SKIP-TS-4.5.5] [SKIP-TS-4.4.4] [SKIP-TS-4.3.5] [SKIP-TS-4.2.3] [SKIP-TS-4.1.5] [SKIP-TS-4.0.5] [SKIP-TS-3.9.7]
16 | // [ONLY-TS-5.0.4] [ONLY-TS-4.9.5] [ONLY-TS-4.8.4] [ONLY-TS-4.7.4] [ONLY-TS-4.6.4] [ONLY-TS-4.5.5] [ONLY-TS-4.4.4] [ONLY-TS-4.3.5] [ONLY-TS-4.2.3] [ONLY-TS-4.1.5] [ONLY-TS-4.0.5] [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore
17 | useHydrateAtoms([
18 | [countAtom, 1],
19 | [activeAtom, 0],
20 | ])
21 | // @ts-expect-error TS2769 [SKIP-TS-5.0.4] [SKIP-TS-4.9.5] [SKIP-TS-4.8.4] [SKIP-TS-4.7.4] [SKIP-TS-4.6.4] [SKIP-TS-4.5.5] [SKIP-TS-4.4.4] [SKIP-TS-4.3.5] [SKIP-TS-4.2.3] [SKIP-TS-4.1.5] [SKIP-TS-4.0.5] [SKIP-TS-3.9.7]
22 | // [ONLY-TS-5.0.4] [ONLY-TS-4.9.5] [ONLY-TS-4.8.4] [ONLY-TS-4.7.4] [ONLY-TS-4.6.4] [ONLY-TS-4.5.5] [ONLY-TS-4.4.4] [ONLY-TS-4.3.5] [ONLY-TS-4.2.3] [ONLY-TS-4.1.5] [ONLY-TS-4.0.5] [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore
23 | useHydrateAtoms([
24 | [countAtom, true],
25 | [activeAtom, false],
26 | ])
27 | // Valid case
28 | // [ONLY-TS-3.9.7] [ONLY-TS-3.8.3] @ts-ignore
29 | useHydrateAtoms([
30 | [countAtom, 1],
31 | [activeAtom, true],
32 | ])
33 | }
34 | expect(Component).toBeDefined()
35 | })
36 |
--------------------------------------------------------------------------------
/website/src/components/intro.js:
--------------------------------------------------------------------------------
1 | import { InlineCode } from '../components/inline-code.js'
2 | import { Jotai } from '../components/jotai.js'
3 |
4 | export const Intro = () => {
5 | return (
6 |
7 |
8 |
9 |
10 |
15 |
16 | v2
17 |
18 |
19 |
20 |
Welcome to Jotai v2!
21 |
22 | Fully compatible with React 18 and the upcoming{' '}
23 | use hook. Now with a store interface
24 | that can be used outside of React.
25 |
26 |
Enjoy the new “Getting started” experience below!
27 |
28 |
29 |
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/website/src/components/toggle.js:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect } from 'react'
2 | import cx from 'classnames'
3 | import { useAtom } from 'jotai'
4 | import { darkModeAtom } from '../atoms/index.js'
5 | import { Icon } from '../components/icon.js'
6 |
7 | export const Toggle = () => {
8 | const [darkMode, setDarkMode] = useAtom(darkModeAtom)
9 |
10 | const toggleDarkMode = useCallback(() => {
11 | setDarkMode(!darkMode)
12 | }, [darkMode, setDarkMode])
13 |
14 | useEffect(() => {
15 | if (darkMode) {
16 | document.body.classList.add('dark')
17 | document.body.classList.remove('light')
18 | } else {
19 | document.body.classList.add('light')
20 | document.body.classList.remove('dark')
21 | }
22 | }, [darkMode])
23 |
24 | return (
25 |
26 |
31 |
32 |
39 |
46 |
47 |
48 |
49 | )
50 | }
51 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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
25 | - 19.3.0-canary-378973b3-20251205
26 | - 0.0.0-experimental-378973b3-20251205
27 | steps:
28 | - uses: actions/checkout@v6
29 | - uses: pnpm/action-setup@v4
30 | - uses: actions/setup-node@v6
31 | with:
32 | node-version: 'lts/*'
33 | cache: 'pnpm'
34 | - run: pnpm install
35 | - name: Install legacy testing-library
36 | if: ${{ startsWith(matrix.react, '16.') || startsWith(matrix.react, '17.') }}
37 | run: |
38 | pnpm add -D @testing-library/react@12.1.4
39 | - name: Patch for React 17
40 | if: ${{ startsWith(matrix.react, '17.') }}
41 | run: |
42 | pnpm add -D vitest@3.2.4
43 | - name: Patch for React 16
44 | if: ${{ startsWith(matrix.react, '16.') }}
45 | run: |
46 | sed -i~ '1s/^/import React from "react";/' tests/*/*.tsx tests/*/*/*.tsx
47 | sed -i~ 's/"jsx": "react-jsx"/"jsx": "react"/' tsconfig.json
48 | sed -i~ 's/import\.meta\.env[?]\.MODE/"DEVELOPMENT".toLowerCase()/' src/*.ts src/*/*.ts src/*/*/*.ts
49 | - name: Test Build # we need to build for babel tests
50 | run: pnpm run build
51 | - name: Test ${{ matrix.react }}
52 | run: |
53 | pnpm add -D react@${{ matrix.react }} react-dom@${{ matrix.react }}
54 | pnpm run test:spec
55 |
--------------------------------------------------------------------------------
/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 | setPostId((id) => id + 1)}>
42 | →
43 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/website/src/components/jotai.js:
--------------------------------------------------------------------------------
1 | import cx from 'classnames'
2 | import { Link } from 'gatsby'
3 | import { Logo } from '../components/logo.js'
4 |
5 | export const Jotai = ({ isDocs = false, small = false, ...rest }) => {
6 | return (
7 |
8 |
9 |
10 |
20 |
21 | Jotai
22 |
23 |
31 |
37 | 状態
38 |
39 |
46 | Primitive and flexible state management for React
47 |
48 |
49 |
50 | )
51 | }
52 |
53 | const Headline = ({ mainTitle = false, children, ...rest }) => {
54 | return mainTitle ? (
55 | {children}
56 | ) : (
57 | {children}
58 | )
59 | }
60 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/website/src/components/core-demo.js:
--------------------------------------------------------------------------------
1 | import { useAtom } from 'jotai'
2 | import { textAtom, uppercaseAtom } from '../atoms/index.js'
3 | import { Code } from '../components/code.js'
4 |
5 | export const CoreDemo = () => {
6 | const Input = () => {
7 | const [text, setText] = useAtom(textAtom)
8 |
9 | return (
10 | setText(event.target.value)}
13 | className="w-full bg-transparent focus:!ring-transparent !border-none"
14 | />
15 | )
16 | }
17 |
18 | const Uppercase = () => {
19 | const [uppercase] = useAtom(uppercaseAtom)
20 |
21 | return {uppercase}
22 | }
23 |
24 | const code = `import { atom, useAtom } from 'jotai'
25 |
26 | // Create your atoms and derivatives
27 | const textAtom = atom('hello')
28 | const uppercaseAtom = atom(
29 | (get) => get(textAtom).toUpperCase()
30 | )
31 |
32 | // Use them anywhere in your app
33 | const Input = () => {
34 | const [text, setText] = useAtom(textAtom)
35 | const handleChange = (e) => setText(e.target.value)
36 | return (
37 |
38 | )
39 | }
40 |
41 | const Uppercase = () => {
42 | const [uppercase] = useAtom(uppercaseAtom)
43 | return (
44 | Uppercase: {uppercase}
45 | )
46 | }
47 |
48 | // Now you have the components
49 | const App = () => {
50 | return (
51 | <>
52 |
53 |
54 | >
55 | )
56 | }`
57 |
58 | return (
59 | <>
60 |
66 | {code}
67 | >
68 | )
69 | }
70 |
--------------------------------------------------------------------------------
/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-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 |
setStyles({ ...styles, backgroundColor: 'red' })}>
55 | Set background to red
56 |
57 |
58 | {/* Clicking this button twice will only trigger one render */}
59 |
setStyles({ ...styles, fontSize: 32 })}>
60 | Enlarge font
61 |
62 |
63 | )
64 | }
65 | ```
66 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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, ...infer Rest]
12 | ? A extends WritableAtom
13 | ? Rest extends Args
14 | ? readonly [A, ...Rest]
15 | : never
16 | : T[K]
17 | : never
18 | }
19 |
20 | // For internal use only
21 | // This can be changed without notice.
22 | export type INTERNAL_InferAtomTuples = InferAtomTuples
23 |
24 | const hydratedMap: WeakMap> = new WeakMap()
25 |
26 | export function useHydrateAtoms<
27 | T extends (readonly [AnyWritableAtom, ...unknown[]])[],
28 | >(values: InferAtomTuples, options?: Options): void
29 |
30 | export function useHydrateAtoms>(
31 | values: T,
32 | options?: Options,
33 | ): void
34 |
35 | export function useHydrateAtoms<
36 | T extends Iterable,
37 | >(values: InferAtomTuples, options?: Options): void
38 |
39 | export function useHydrateAtoms<
40 | T extends Iterable,
41 | >(values: T, options?: Options) {
42 | const store = useStore(options)
43 |
44 | const hydratedSet = getHydratedSet(store)
45 | for (const [atom, ...args] of values) {
46 | if (!hydratedSet.has(atom) || options?.dangerouslyForceHydrate) {
47 | hydratedSet.add(atom)
48 | store.set(atom, ...args)
49 | }
50 | }
51 | }
52 |
53 | const getHydratedSet = (store: Store) => {
54 | let hydratedSet = hydratedMap.get(store)
55 | if (!hydratedSet) {
56 | hydratedSet = new WeakSet()
57 | hydratedMap.set(store, hydratedSet)
58 | }
59 | return hydratedSet
60 | }
61 |
--------------------------------------------------------------------------------
/tests/react/provider.test.tsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from 'react'
2 | import { render, screen } from '@testing-library/react'
3 | import { expect, 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', () => {
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 | expect(screen.getByText('count: 0')).toBeInTheDocument()
36 | expect(screen.getByText('pet: dog')).toBeInTheDocument()
37 | })
38 |
39 | it('only uses initial value from provider for specific atom', () => {
40 | const countAtom = atom(1)
41 | const petAtom = atom('cat')
42 |
43 | const Display = () => {
44 | const [count] = useAtom(countAtom)
45 | const [pet] = useAtom(petAtom)
46 |
47 | return (
48 | <>
49 | count: {count}
50 | pet: {pet}
51 | >
52 | )
53 | }
54 |
55 | const store = createStore()
56 | store.set(petAtom, 'dog')
57 |
58 | render(
59 |
60 |
61 |
62 |
63 | ,
64 | )
65 |
66 | expect(screen.getByText('count: 1')).toBeInTheDocument()
67 | expect(screen.getByText('pet: dog')).toBeInTheDocument()
68 | })
69 |
70 | it('renders correctly without children', () => {
71 | const { container } = render(
72 |
73 |
74 | ,
75 | )
76 |
77 | expect(container).toBeInTheDocument()
78 | })
79 |
--------------------------------------------------------------------------------
/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 |
68 |
69 | )
70 |
71 | export default App
72 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/website/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "jotai-website",
3 | "version": "0.0.0",
4 | "description": "👻 Primitive and flexible state management for React",
5 | "homepage": "https://github.com/pmndrs/jotai",
6 | "bugs": "https://github.com/pmndrs/jotai/issues",
7 | "license": "MIT",
8 | "author": "Sophia Michelle Andren (https://candycode.com/)",
9 | "sideEffects": [
10 | "*.css"
11 | ],
12 | "scripts": {
13 | "dev": "gatsby develop -H 0.0.0.0 -p 9000",
14 | "build": "gatsby build",
15 | "serve": "gatsby serve",
16 | "clean": "gatsby clean",
17 | "browserslist": "npx browserslist@latest --update-db"
18 | },
19 | "packageManager": "pnpm@10.18.3",
20 | "dependencies": {
21 | "@headlessui/react": "^1.7.7",
22 | "@mdx-js/mdx": "^1.6.22",
23 | "@mdx-js/react": "^1.6.22",
24 | "algoliasearch": "^4.13.1",
25 | "classnames": "^2.3.2",
26 | "gatsby": "^4.23.0",
27 | "gatsby-plugin-algolia": "^0.26.0",
28 | "gatsby-plugin-google-gtag": "^4.23.0",
29 | "gatsby-plugin-mdx": "^3.17.0",
30 | "gatsby-plugin-pnpm": "^1.2.10",
31 | "gatsby-plugin-postcss": "^5.23.0",
32 | "gatsby-plugin-sitemap": "^5.23.0",
33 | "gatsby-source-filesystem": "^4.23.0",
34 | "jotai": "^2.4.1",
35 | "jotai-immer": "^0.2.0",
36 | "just-kebab-case": "^4.1.1",
37 | "just-throttle": "^4.1.1",
38 | "prism-react-renderer": "^1.3.3",
39 | "react": "^18.2.0",
40 | "react-dom": "^18.2.0",
41 | "react-instantsearch-hooks-web": "^6.33.0",
42 | "react-remove-scroll": "^2.5.5"
43 | },
44 | "devDependencies": {
45 | "@gatsbyjs/reach-router": "^2.0.1",
46 | "@tailwindcss/forms": "^0.5.6",
47 | "autoprefixer": "^10.4.11",
48 | "babel-core": "^6.26.3",
49 | "babel-loader": "^9.1.3",
50 | "babel-preset-gatsby": "^2.23.0",
51 | "postcss": "^8.4.29",
52 | "postcss-import": "^15.0.0",
53 | "postmark": "^3.0.14",
54 | "prettier": "^3.0.3",
55 | "tailwindcss": "^3.1.8"
56 | },
57 | "private": true,
58 | "browserslist": [
59 | ">0.25%",
60 | "not dead",
61 | "not ie <=11",
62 | "not ie_mob <=11",
63 | "not op_mini all"
64 | ]
65 | }
66 |
--------------------------------------------------------------------------------
/docs/basics/showcase.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Showcase
3 | nav: 7.03
4 | ---
5 |
6 | - Text Length example [](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 [](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 [](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 [](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 [](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 [](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 [](https://codesandbox.io/s/jotai-r3f-fri9d)
31 |
32 | A simple demo to use Jotai with react-three-fiber
33 |
--------------------------------------------------------------------------------
/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/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 | setA((oldA) => oldA + 1)}>Increment a
53 | setB((oldB) => oldB + 1)}>Increment b
54 |
55 | )
56 | }
57 | ```
58 |
59 | #### Stackblitz
60 |
61 |
62 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
35 | setTodoList((l) => [
36 | ...l,
37 | {
38 | description: `New todo ${new Date().toDateString()}`,
39 | checked: false,
40 | },
41 | ])
42 | }
43 | >
44 | Add todo
45 |
46 | Reset
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 | setCents(100)}>Set $1
78 | setCents(200)}>Set $2
79 | Reset
80 | >
81 | )
82 | }
83 | ```
84 |
85 | ### Examples
86 |
87 |
88 |
--------------------------------------------------------------------------------
/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 | Undo
82 | Redo
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 |
--------------------------------------------------------------------------------
/website/src/styles/pmndrs.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Pmndrs theme for JavaScript, CSS and HTML
3 | * Loosely based on https://marketplace.visualstudio.com/items?itemName=pmndrs.pmndrs
4 | * @author Paul Henschel
5 | */
6 |
7 | code[class*='language-'],
8 | pre[class*='language-'] {
9 | word-spacing: normal;
10 | word-wrap: normal;
11 | tab-size: 2;
12 | hyphens: none;
13 | font-size: 0.9em;
14 | @apply break-normal rounded-md text-left leading-normal text-[#e4f0fb] shadow-lg dark:!shadow-none sm:rounded-lg;
15 | }
16 |
17 | /* Code blocks */
18 | pre[class*='language-'] {
19 | @apply my-4 overflow-x-auto whitespace-pre p-6 sm:p-8;
20 | }
21 |
22 | :not(pre) > code[class*='language-'],
23 | pre[class*='language-'] {
24 | @apply bg-[#252b37];
25 | }
26 |
27 | .dark :not(pre) > code[class*='language-'],
28 | .dark pre[class*='language-'] {
29 | @apply bg-[#13161c];
30 | }
31 |
32 | /* Inline code */
33 | :not(pre) > code[class*='language-'] {
34 | @apply whitespace-normal rounded-md p-3 shadow-lg dark:!shadow-none;
35 | }
36 |
37 | .token.comment,
38 | .token.prolog,
39 | .token.doctype,
40 | .token.cdata {
41 | @apply text-[#a6accd];
42 | }
43 |
44 | .token.punctuation {
45 | @apply text-[#e4f0fb];
46 | }
47 |
48 | .token.namespace {
49 | @apply opacity-70;
50 | }
51 |
52 | .token.property,
53 | .token.tag,
54 | .token.constant,
55 | .token.symbol,
56 | .token.deleted {
57 | @apply text-[#e4f0fb];
58 | }
59 |
60 | .token.boolean,
61 | .token.number {
62 | @apply text-[#5de4c7];
63 | }
64 |
65 | .token.selector,
66 | .token.attr-value,
67 | .token.string,
68 | .token.char,
69 | .token.builtin,
70 | .token.inserted {
71 | @apply text-[#5de4c7];
72 | }
73 |
74 | .token.attr-name,
75 | .token.operator,
76 | .token.entity,
77 | .token.url,
78 | .language-css .token.string,
79 | .style .token.string,
80 | .token.variable {
81 | @apply text-[#add7ff];
82 | }
83 |
84 | .token.atrule,
85 | .token.function,
86 | .token.class-name {
87 | @apply text-[#5de4c7];
88 | }
89 |
90 | .token.keyword {
91 | @apply text-[#add7ff];
92 | }
93 |
94 | .token.regex,
95 | .token.important {
96 | @apply text-[#fffac2];
97 | }
98 |
99 | .token.important,
100 | .token.bold {
101 | font-weight: 700;
102 | }
103 |
104 | .token.italic {
105 | font-style: italic;
106 | }
107 |
108 | .token.entity {
109 | @apply cursor-help;
110 | }
111 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/website/src/styles/layout.css:
--------------------------------------------------------------------------------
1 | html {
2 | overflow: visible !important;
3 | padding-right: 0 !important;
4 | }
5 |
6 | body {
7 | @apply overflow-x-clip overflow-y-scroll overscroll-none bg-black leading-none font-sans;
8 | text-rendering: optimizeLegibility;
9 | -webkit-tap-highlight-color: transparent;
10 | }
11 |
12 | #___gatsby {
13 | @apply bg-white text-black dark:bg-gray-950 dark:text-gray-50;
14 | }
15 |
16 | #___gatsby > div {
17 | opacity: 0;
18 | animation: fadeIn cubic-bezier(0.4, 0, 0.2, 1) 0.5s;
19 | animation-iteration-count: 1;
20 | animation-fill-mode: forwards !important;
21 | animation-delay: 0.7s;
22 | }
23 |
24 | @keyframes fadeIn {
25 | 0% {
26 | opacity: 0;
27 | }
28 | 100% {
29 | opacity: 1;
30 | }
31 | }
32 |
33 | pre,
34 | code,
35 | pre span,
36 | code span {
37 | font-family: 'Fira Code', monospace !important;
38 | }
39 |
40 | ::selection {
41 | @apply bg-black text-white dark:bg-gray-50 dark:text-gray-950;
42 | }
43 |
44 | a,
45 | button {
46 | @apply transition-all duration-300 ease-in-out;
47 | }
48 |
49 | *:focus {
50 | outline: 0 !important;
51 | }
52 |
53 | a,
54 | button,
55 | input,
56 | select,
57 | textarea {
58 | @apply relative z-0;
59 | }
60 |
61 | a:focus,
62 | button:focus,
63 | input:focus,
64 | select:focus,
65 | textarea:focus {
66 | @apply z-10 ring-blue-400 dark:!ring-teal-700;
67 | }
68 |
69 | input[type='search']::-webkit-search-decoration,
70 | input[type='search']::-webkit-search-cancel-button,
71 | input[type='search']::-webkit-search-results-button,
72 | input[type='search']::-webkit-search-results-decoration {
73 | @apply appearance-none;
74 | }
75 |
76 | svg:not([fill]) {
77 | fill: currentColor;
78 | }
79 |
80 | *::-webkit-scrollbar {
81 | @apply h-full w-4 bg-white dark:bg-gray-950;
82 | }
83 |
84 | body.dark::-webkit-scrollbar {
85 | @apply bg-gray-950;
86 | }
87 |
88 | *::-webkit-scrollbar-track {
89 | @apply bg-white dark:bg-gray-950;
90 | }
91 |
92 | body.dark::-webkit-scrollbar-track {
93 | @apply bg-gray-950;
94 | }
95 |
96 | *::-webkit-scrollbar-thumb {
97 | @apply rounded-2xl border-4 border-solid border-white bg-gray-350 dark:border-gray-950 dark:bg-gray-800;
98 | }
99 |
100 | body.dark::-webkit-scrollbar-thumb {
101 | @apply border-gray-950 bg-gray-800;
102 | }
103 |
104 | *::-webkit-scrollbar-button {
105 | @apply hidden;
106 | }
107 |
--------------------------------------------------------------------------------
/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 | setCount(count + 1)}>+1
69 |
70 | )
71 | }
72 | ```
73 |
74 |
75 |
--------------------------------------------------------------------------------
/website/src/components/menu.js:
--------------------------------------------------------------------------------
1 | import cx from 'classnames'
2 | import { useAtom } from 'jotai'
3 | import { menuAtom } from '../atoms/index.js'
4 | import { Button } from '../components/button.js'
5 | import { Docs } from '../components/docs.js'
6 | import { Icon } from '../components/icon.js'
7 | import { SearchButton } from '../components/search-button.js'
8 | import { useOnEscape } from '../hooks/index.js'
9 |
10 | export const Menu = () => {
11 | const [isMenuOpen, setIsMenuOpen] = useAtom(menuAtom)
12 |
13 | useOnEscape(() => setIsMenuOpen(false))
14 |
15 | return (
16 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | Jotai docs
29 |
30 |
31 |
32 |
33 | setIsMenuOpen(false)}
36 | className="w-full font-bold uppercase tracking-wider lg:hidden"
37 | dark
38 | >
39 | Close
40 |
41 | setIsMenuOpen(false)}
43 | className="hidden lg:block"
44 | aria-label="Close"
45 | >
46 |
50 |
51 |
52 |
53 |
54 | )
55 | }
56 |
--------------------------------------------------------------------------------
/website/src/components/meta.js:
--------------------------------------------------------------------------------
1 | import { graphql, useStaticQuery } from 'gatsby'
2 |
3 | export const Meta = ({ lang = 'en', title, description, uri }) => {
4 | const data = useStaticQuery(staticQuery)
5 |
6 | const { site } = data
7 |
8 | const siteTitle = site.siteMetadata.title
9 | const siteUrl = site.siteMetadata.siteUrl
10 | const siteIcon = `/favicon.svg`
11 | const socialMediaCardImage = `https://cdn.candycode.com/jotai/jotai-opengraph-v2.png`
12 | const shortName = site.siteMetadata.shortName
13 |
14 | const pageTitle = title
15 | ? `${title} — ${title.length <= 10 ? siteTitle : shortName}`
16 | : siteTitle
17 | const pageDescription = description || site.siteMetadata.description
18 | const pageUrl = uri ? `${siteUrl}/${uri}` : siteUrl
19 |
20 | return (
21 | <>
22 | {pageTitle}
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
44 |
49 |
54 | >
55 | )
56 | }
57 |
58 | const staticQuery = graphql`
59 | query {
60 | site {
61 | siteMetadata {
62 | title
63 | description
64 | siteUrl
65 | shortName
66 | }
67 | }
68 | }
69 | `
70 |
--------------------------------------------------------------------------------
/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 |
Refresh
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 |
--------------------------------------------------------------------------------
/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] as [() => void, number])
25 | refreshAtom.INTERNAL_onInit = (store) => {
26 | store.set(refreshAtom, ([, c]) => [
27 | () => store.set(refreshAtom, ([f, c]) => [f, c + 1]),
28 | c,
29 | ])
30 | }
31 |
32 | if (import.meta.env?.MODE !== 'production') {
33 | refreshAtom.debugPrivate = true
34 | }
35 |
36 | const derivedAtom = atom((get) => {
37 | const [triggerRefresh] = get(refreshAtom)
38 | let value: Value
39 | try {
40 | value = get(anAtom)
41 | } catch (error) {
42 | return { state: 'hasError', error } as Loadable
43 | }
44 | if (!isPromiseLike(value)) {
45 | return { state: 'hasData', data: value } as Loadable
46 | }
47 | const promise = value
48 | const cached1 = loadableCache.get(promise)
49 | if (cached1) {
50 | return cached1
51 | }
52 | promise.then(
53 | (data) => {
54 | loadableCache.set(promise, { state: 'hasData', data })
55 | triggerRefresh()
56 | },
57 | (error) => {
58 | loadableCache.set(promise, { state: 'hasError', error })
59 | triggerRefresh()
60 | },
61 | )
62 |
63 | const cached2 = loadableCache.get(promise)
64 | if (cached2) {
65 | return cached2
66 | }
67 | loadableCache.set(promise, LOADING as Loadable)
68 | return LOADING as Loadable
69 | })
70 |
71 | if (import.meta.env?.MODE !== 'production') {
72 | derivedAtom.debugPrivate = true
73 | }
74 |
75 | return atom((get) => get(derivedAtom))
76 | }, anAtom)
77 | }
78 |
--------------------------------------------------------------------------------
/website/src/components/logo.js:
--------------------------------------------------------------------------------
1 | export const Logo = ({ ...props }) => {
2 | return (
3 |
8 | Jotai
9 |
13 |
17 |
21 |
25 |
29 |
30 | )
31 | }
32 |
--------------------------------------------------------------------------------