diff --git a/.github/DISCUSSION_TEMPLATE/bug-report.yml b/.github/DISCUSSION_TEMPLATE/bug-report.yml
index cd8c436c3b..9e1fe7b404 100644
--- a/.github/DISCUSSION_TEMPLATE/bug-report.yml
+++ b/.github/DISCUSSION_TEMPLATE/bug-report.yml
@@ -1,4 +1,4 @@
-labels: ["bug"]
+labels: ['bug']
body:
- type: markdown
attributes:
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index bb6135f5eb..00e23d7387 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -4,6 +4,4 @@ about: This is to create a new issue that already has an assignee. Please open a
title: ''
labels: ''
assignees: ''
-
---
-
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index c2a3094f8a..6da9f825cd 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,3 +1,4 @@
+blank_issues_enabled: false
contact_links:
- name: Bug Reports
url: https://github.com/pmndrs/jotai/discussions/new?category=bug-report
diff --git a/.github/workflows/test-multiple-builds.yml b/.github/workflows/test-multiple-builds.yml
index b045639fea..b1bcfe74f3 100644
--- a/.github/workflows/test-multiple-builds.yml
+++ b/.github/workflows/test-multiple-builds.yml
@@ -30,20 +30,20 @@ jobs:
- name: Patch for DEV-ONLY
if: ${{ matrix.env == 'development' }}
run: |
- sed -i~ "s/\(it\|describe\)[.a-zA-Z]*('\[DEV-ONLY\]/\1('/" tests/*/*.tsx tests/*/*/*.tsx
- sed -i~ "s/\(it\|describe\)[.a-zA-Z]*('\[PRD-ONLY\]/\1.skip('/" tests/*/*.tsx tests/*/*/*.tsx
+ sed -i~ "s/\(it\|describe\)[.a-zA-Z]*('\[DEV-ONLY\]/\1('/" tests/*/*.ts* tests/*/*/*.ts*
+ sed -i~ "s/\(it\|describe\)[.a-zA-Z]*('\[PRD-ONLY\]/\1.skip('/" tests/*/*.ts* tests/*/*/*.ts*
- name: Patch for PRD-ONLY
if: ${{ matrix.env == 'production' }}
run: |
- sed -i~ "s/\(it\|describe\)[.a-zA-Z]*('\PRD-ONLY\]/\1('/" tests/*/*.tsx tests/*/*/*.tsx
- sed -i~ "s/\(it\|describe\)[.a-zA-Z]*('\[DEV-ONLY\]/\1.skip('/" tests/*/*.tsx tests/*/*/*.tsx
+ sed -i~ "s/\(it\|describe\)[.a-zA-Z]*('\PRD-ONLY\]/\1('/" tests/*/*.ts* tests/*/*/*.ts*
+ sed -i~ "s/\(it\|describe\)[.a-zA-Z]*('\[DEV-ONLY\]/\1.skip('/" tests/*/*.ts* tests/*/*/*.ts*
- name: Patch for CJS
if: ${{ matrix.build == 'cjs' }}
run: |
sed -i~ "s/resolve('\.\/src\(.*\)\.ts')/resolve('\.\/dist\1.js')/" vitest.config.mts
sed -i~ "s/import { useResetAtom } from 'jotai\/react\/utils'/const { useResetAtom } = require('..\/..\/..\/dist\/react\/utils.js')/" tests/react/utils/useResetAtom.test.tsx
sed -i~ "s/import { RESET, atomWithReducer, atomWithReset } from 'jotai\/vanilla\/utils'/const { RESET, atomWithReducer, atomWithReset } = require('..\/..\/..\/dist\/vanilla\/utils.js')/" tests/react/utils/useResetAtom.test.tsx
- perl -i~ -0777 -pe "s/import {[^}]+} from 'jotai\/vanilla\/internals'/const { INTERNAL_buildStoreRev1: INTERNAL_buildStore, INTERNAL_initializeStoreHooks, INTERNAL_getBuildingBlocksRev1: INTERNAL_getBuildingBlocks } = require('..\/..\/dist\/vanilla\/internals.js')/g" tests/vanilla/store.test.tsx tests/vanilla/internals.test.tsx tests/vanilla/derive.test.tsx tests/vanilla/effect.test.ts
+ perl -i~ -0777 -pe "s/import {[^}]+} from 'jotai\/vanilla\/internals'/const { INTERNAL_buildStoreRev1: INTERNAL_buildStore, INTERNAL_initializeStoreHooks, INTERNAL_getBuildingBlocksRev1: INTERNAL_getBuildingBlocks } = require('..\/..\/dist\/vanilla\/internals.js')/g" tests/vanilla/store.test.tsx tests/vanilla/internals.test.tsx tests/vanilla/derive.test.tsx tests/vanilla/effect.test.ts
- name: Patch for ESM
if: ${{ matrix.build == 'esm' }}
run: |
diff --git a/.github/workflows/test-multiple-versions.yml b/.github/workflows/test-multiple-versions.yml
index 597aac78c1..7d4b178b86 100644
--- a/.github/workflows/test-multiple-versions.yml
+++ b/.github/workflows/test-multiple-versions.yml
@@ -33,8 +33,8 @@ jobs:
- 18.2.0
- 18.3.1
- 19.0.0
- - 19.1.0-canary-e670e72f-20250214
- - 0.0.0-experimental-e670e72f-20250214
+ - 19.1.0-canary-f9d78089-20250306
+ - 0.0.0-experimental-f9d78089-20250306
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000000..dee70d2f54
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,2 @@
+dist
+pnpm-lock.yaml
diff --git a/LICENSE b/LICENSE
index 7eaa45c5ed..38ab200c3e 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2020-2023 Poimandres
+Copyright (c) 2020 Poimandres
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/docs/extensions/effect.mdx b/docs/extensions/effect.mdx
index b00f2b7ba6..8a2c9171a1 100644
--- a/docs/extensions/effect.mdx
+++ b/docs/extensions/effect.mdx
@@ -45,12 +45,10 @@ function observe(effect: Effect, store?: Store): Unobserve
```js
import { observe } from 'jotai-effect'
-// activate the effect on the default store
const unobserve = observe((get, set) => {
set(logAtom, `someAtom changed: ${get(someAtom)}`)
})
-// Clean up the effect
unobserve()
```
@@ -58,7 +56,7 @@ This allows you to run Jotai state-dependent logic outside React's lifecycle, id
### Usage With React
-When using a Jotai Provider, pass the store to both `observe` and the `Provider` to ensure the effect is mounted on the correct store.
+Pass the store to both `observe` and the `Provider` to ensure the effect is mounted to the correct store.
```tsx
const store = createStore()
@@ -69,19 +67,6 @@ const unobserve = observe((get, set) => {
...
```
-Using `observe` in a `useEffect`
-
-```tsx
-function effect(get: Getter, set: Setter) {
- set(logAtom, `someAtom changed: ${get(someAtom)}`)
-}
-
-function Component() {
- const store = useStore()
- useEffect(() => observe(effect, store), [store])
-}
-```
-
## atomEffect
@@ -103,7 +88,6 @@ import { atomEffect } from 'jotai-effect'
const logEffect = atomEffect((get, set) => {
set(logAtom, get(someAtom)) // Runs on mount or when someAtom changes
-
return () => {
set(logAtom, 'unmounting') // Cleanup on unmount
}
@@ -138,14 +122,18 @@ import { withAtomEffect } from 'jotai-effect'
const valuesAtom = withAtomEffect(atom(null), (get, set) => {
set(valuesAtom, get(countAtom))
- return unsubscribe
+ return () => {
+ // cleanup
+ }
})
```
-## Effect Behavior
+## Dependency Management
-- **Cleanup Function:**
- The cleanup function is invoked on unmount or before re-evaluation.
+Aside from mount events, the effect runs when any of its dependencies change value.
+
+- **Sync:**
+ All atoms accessed with `get` inside the effect are added to the atom's dependencies.
@@ -153,88 +141,90 @@ const valuesAtom = withAtomEffect(atom(null), (get, set) => {
```js
atomEffect((get, set) => {
- const intervalId = setInterval(() => set(clockAtom, Date.now()))
- return () => clearInterval(intervalId)
+ // updates whenever `anAtom` changes value
+ get(anAtom)
})
```
-- **Resistant to Infinite Loops:**
- `atomEffect` avoids rerunning when it updates a value that it is watching.
+- **Async:**
+ Asynchronous `get` calls do not add dependencies.
Example
```js
- const countAtom = atom(0)
atomEffect((get, set) => {
- get(countAtom)
- set(countAtom, increment) // Will not loop
+ setTimeout(() => {
+ // does not add `anAtom` as a dependency
+ get(anAtom)
+ })
})
```
-- **Supports Recursion:**
- Recursion is supported with `set.recurse` but not in cleanup.
+- **Cleanup:**
+ `get` calls in cleanup do not add dependencies.
Example
```js
- const countAtom = atom(0)
atomEffect((get, set) => {
- const count = get(countAtom)
- const timeoutId = setTimeout(() => {
- set.recurse(countAtom, increment)
- }, 1000)
- return () => clearTimeout(timeoutId)
+ return () => {
+ // does not add `anAtom` as a dependency
+ get(anAtom)
+ }
})
```
-- **Supports Peek:**
- Use `get.peek` to read atom data without subscribing.
+- **Dependency Map Recalculation:**
+ Dependencies are recalculated on every run.
Example
```js
- const countAtom = atom(0)
atomEffect((get, set) => {
- const count = get.peek(countAtom) // Will not add countAtom as a dependency
+ if (get(isEnabledAtom)) {
+ // `isEnabledAtom` and `anAtom` are dependencies
+ const aValue = get(anAtom)
+ } else {
+ // `isEnabledAtom` and `anotherAtom` are dependencies
+ const anotherValue = get(anotherAtom)
+ }
})
```
-- **Executes In The Next Microtask:**
- `effect` runs in the next available microtask after synchronous evaluations complete.
+## Effect Behavior
+
+- **Executes Synchronously:**
+ `effect` runs synchronous in the current task after synchronous evaluations complete.
Example
```js
- const countAtom = atom(0)
- const logAtom = atom('')
const logCounts = atomEffect((get, set) => {
- set(logAtom, `count is now ${get(countAtom)}`)
+ set(logAtom, `count is ${get(countAtom)}`)
})
- const setCountAndReadLog = atom(null, async (get, set) => {
- get(logAtom) // 'count is now 0'
- set(countAtom, increment) // effect runs in next microtask
- get(logAtom) // 'count is now 0'
- await Promise.resolve()
- get(logAtom) // 'count is now 1'
+ const actionAtom = atom(null, (get, set) => {
+ get(logAtom) // 'count is 0'
+ set(countAtom, (value) => value + 1) // effect runs synchronously
+ get(logAtom) // 'count is 1'
})
store.sub(logCounts, () => {})
- store.set(setCountAndReadLog)
+ store.set(actionAtom)
```
@@ -265,160 +255,107 @@ const valuesAtom = withAtomEffect(atom(null), (get, set) => {
-- **Conditionally Running Effects:**
- `atomEffect` only runs when mounted.
+- **Resistant to Infinite Loops:**
+ `atomEffect` avoids rerunning when it updates a value that it is watching.
Example
```js
- atom((get) => {
- if (get(isEnabledAtom)) {
- get(effectAtom)
- }
+ atomEffect((get, set) => {
+ get(countAtom)
+ set(countAtom, (value) => value + 1) // Will not loop
})
```
-- **Idempotency:**
- `atomEffect` runs once per state change, regardless of how many times it is referenced.
+- **Cleanup Function:**
+ The cleanup function is invoked on unmount or before re-evaluation.
Example
```js
- let i = 0
- const effectAtom = atomEffect(() => {
- get(countAtom)
- i++
+ atomEffect((get, set) => {
+ const intervalId = setInterval(() => set(clockAtom, Date.now()))
+ return () => clearInterval(intervalId)
})
- store.sub(effectAtom, () => {})
- store.sub(effectAtom, () => {})
- store.set(countAtom, increment)
- await Promise.resolve()
- console.log(i) // 1
```
-
-
-## Dependency Management
-
-Aside from mount events, the effect runs when any of its dependencies change value.
+
-- **Sync:**
- All atoms accessed with `get` during the synchronous evaluation of the effect are added to the atom's internal dependency map.
+- **Idempotency:**
+ `atomEffect` runs once per state change, regardless of how many times it is referenced.
Example
```js
- atomEffect((get, set) => {
- // updates whenever `anAtom` changes value but not when `anotherAtom` changes value
- get(anAtom)
- setTimeout(() => {
- get(anotherAtom)
- }, 5000)
+ let i = 0
+ const effectAtom = atomEffect(() => {
+ get(countAtom)
+ i++
})
+ store.sub(effectAtom, () => {})
+ store.sub(effectAtom, () => {})
+ store.set(countAtom, (value) => value + 1)
+ console.log(i) // 1
```
-- **Async:**
- Use an abort controller to cancel pending fetch requests and promises.
+- **Conditionally Running Effects:**
+ `atomEffect` only runs when mounted.
Example
```js
- class AbortError extends Error {}
-
- atomEffect((get, set) => {
- const abortController = new AbortController()
- fetchData(abortController.signal).catch((error) => {
- if (error instanceof AbortError) {
- // async cleanup logic here
- } else {
- throw error
- }
- })
- return () => abortController.abort(new AbortError())
+ atom((get) => {
+ if (get(isEnabledAtom)) {
+ get(effectAtom)
+ }
})
```
-- **Cleanup:**
- `get` calls in cleanup do not add dependencies.
+- **Supports Peek:**
+ Use `get.peek` to read atom data without subscribing.
Example
```js
+ const countAtom = atom(0)
atomEffect((get, set) => {
- set(logAtom, get(valueAtom))
- return () => {
- get(idAtom) // Not a dependency
- }
+ const count = get.peek(countAtom) // Will not add `countAtom` as a dependency
})
```
-- **Dependency Map Recalculation:**
- Dependencies are recalculated on every run.
+- **Supports Recursion:**
+ Recursion is supported with `set.recurse` but not in cleanup.
Example
```js
- const isEnabledAtom = atom(true)
-
atomEffect((get, set) => {
- // if `isEnabledAtom` is true, runs when `isEnabledAtom` or `anAtom` changes value
- // otherwise runs when `isEnabledAtom` or `anotherAtom` changes value
- if (get(isEnabledAtom)) {
- const aValue = get(anAtom)
- } else {
- const anotherValue = get(anotherAtom)
+ const count = get(countAtom)
+ if (count % 10 === 0) {
+ return
}
+ set.recurse(countAtom, (value) => value + 1)
})
```
-
-## Comparison with useEffect
-
-### Component Side Effects
-
-[useEffect](https://react.dev/reference/react/useEffect) is a React Hook that lets you synchronize a component with an external system.
-
-Hooks are functions that let you “hook into” React state and lifecycle features from function components.
-They are a way to reuse, but not centralize, stateful logic.
-Each call to a hook has a completely isolated state.
-This isolation can be referred to as _component-scoped_.
-For synchronizing component props and state with a Jotai atom, you should use the useEffect hook.
-
-### Global Side Effects
-
-For setting up global side-effects, deciding between useEffect and atomEffect comes down to developer preference.
-Whether you prefer to build this logic directly into the component or build this logic into the Jotai state model depends on what mental model you adopt.
-
-atomEffects are more appropriate for modeling behavior in atoms.
-They are scoped to the store context rather than the component.
-This guarantees that a single effect will be used regardless of how many calls they have.
-
-The same guarantee can be achieved with the useEffect hook if you ensure that the useEffect is idempotent.
-
-atomEffects are distinguished from useEffect in a few other ways. They can directly react to atom state changes, are resistent to infinite loops, and can be mounted conditionally.
-
-### It's up to you
-
-Both useEffect and atomEffect have their own advantages and applications. Your project's specific needs and your comfort level should guide your selection.
-Always lean towards an approach that gives you a smoother, more intuitive development experience. Happy coding!
diff --git a/docs/extensions/query.mdx b/docs/extensions/query.mdx
index efe3dc6e8b..14112b97ad 100644
--- a/docs/extensions/query.mdx
+++ b/docs/extensions/query.mdx
@@ -7,7 +7,7 @@ keywords: tanstack,query
[TanStack Query](https://tanstack.com/query/) provides a set of functions for managing async state (typically external data).
-From the [Overview docs](https://tanstack.com/query/v5/docs/overview):
+From the [Overview docs](https://tanstack.com/query/v5/docs/framework/react/overview):
> React Query is often described as the missing data-fetching library for React, but in more technical terms, it makes **fetching, caching, synchronizing and updating server state** in your React applications a breeze.
@@ -91,7 +91,7 @@ const UserData = () => {
### atomWithInfiniteQuery usage
-`atomWithInfiniteQuery` is very similar to `atomWithQuery`, however it is for an `InfiniteQuery`, which is used for data that is meant to be paginated. You can [read more about Infinite Queries here](https://tanstack.com/query/v5/docs/guides/infinite-queries).
+`atomWithInfiniteQuery` is very similar to `atomWithQuery`, however it is for an `InfiniteQuery`, which is used for data that is meant to be paginated. You can [read more about Infinite Queries here](https://tanstack.com/query/v5/docs/framework/react/guides/infinite-queries).
> Rendering lists that can additively "load more" data onto an existing set of data or "infinite scroll" is also a very common UI pattern. React Query supports a useful version of useQuery called useInfiniteQuery for querying these types of lists.
@@ -135,7 +135,7 @@ const Posts = () => {
### atomWithMutation usage
-`atomWithMutation` creates a new atom that implements a standard [`Mutation`](https://tanstack.com/query/v5/docs/guides/mutations) from TanStack Query.
+`atomWithMutation` creates a new atom that implements a standard [`Mutation`](https://tanstack.com/query/v5/docs/framework/react/guides/mutations) from TanStack Query.
> Unlike queries, mutations are typically used to create/update/delete data or perform server side-effects.
@@ -326,7 +326,7 @@ export const useTodoMutation = () => {
### SSR support
-All atoms can be used within the context of a server side rendered app, such as a next.js app or Gatsby app. You can [use both options](https://tanstack.com/query/v5/docs/guides/ssr) that React Query supports for use within SSR apps, [hydration](https://tanstack.com/query/v5/docs/react/guides/ssr#using-the-hydration-apis) or [`initialData`](https://tanstack.com/query/v5/docs/react/guides/ssr#get-started-fast-with-initialdata).
+All atoms can be used within the context of a server side rendered app, such as a next.js app or Gatsby app. You can [use both options](https://tanstack.com/query/v5/docs/framework/react/guides/ssr) that React Query supports for use within SSR apps, [hydration](https://tanstack.com/query/v5/docs/react/guides/ssr#using-the-hydration-apis) or [`initialData`](https://tanstack.com/query/v5/docs/react/guides/ssr#get-started-fast-with-initialdata).
### Error handling
diff --git a/docs/utilities/resettable.mdx b/docs/utilities/resettable.mdx
index ca7e12de15..16b5edf16d 100644
--- a/docs/utilities/resettable.mdx
+++ b/docs/utilities/resettable.mdx
@@ -198,7 +198,7 @@ const PostsList = () => {
{/* Clicking this button will re-fetch the posts */}
-