https://github.com/jotaijs/jotai-effect
A Jōtai utility package for reactive side-effects
https://github.com/jotaijs/jotai-effect
Last synced: 3 months ago
JSON representation
A Jōtai utility package for reactive side-effects
- Host: GitHub
- URL: https://github.com/jotaijs/jotai-effect
- Owner: jotaijs
- License: mit
- Created: 2023-10-05T01:51:08.000Z (over 1 year ago)
- Default Branch: main
- Last Pushed: 2025-03-30T22:41:00.000Z (3 months ago)
- Last Synced: 2025-04-01T09:21:12.141Z (3 months ago)
- Language: TypeScript
- Size: 475 KB
- Stars: 140
- Watchers: 4
- Forks: 3
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Effect
[jotai-effect](https://jotai.org/docs/extensions/effect) is a utility package for reactive side effects in Jotai.
## Install
```
npm install jotai-effect
```## observe
`observe` mounts an `effect` to watch state changes on a Jotai `store`. It's useful for running global side effects or logic at the store level.
If you don't have access to the store object and are not using the default store, use `atomEffect` or `withAtomEffect` instead.
### Signature
```ts
type Cleanup = () => voidtype Effect = (
get: Getter & { peek: Getter }
set: Setter & { recurse: Setter }
) => Cleanup | voidtype Unobserve = () => void
function observe(effect: Effect, store?: Store): Unobserve
```**effect:** A function for observing and reacting to atom state changes.
**store:** A Jotai store to mount the effect on. Defaults to the global store if not provided.
**returns:** A stable function that removes the effect from the store and cleans up any internal references.
### Usage
```js
import { observe } from 'jotai-effect'const unobserve = observe((get, set) => {
set(logAtom, `someAtom changed: ${get(someAtom)}`)
})unobserve()
```This allows you to run Jotai state-dependent logic outside React's lifecycle, ideal for application-wide effects.
### Usage With React
Pass the store to both `observe` and the `Provider` to ensure the effect is mounted to the correct store.
```tsx
const store = createStore()
const unobserve = observe((get, set) => {
set(logAtom, `someAtom changed: ${get(someAtom)}`)
}, store)...
```## atomEffect
`atomEffect` creates an atom for declaring side effects that react to state changes when mounted.
### Signature
```ts
function atomEffect(effect: Effect): Atom
```**effect:** A function for observing and reacting to atom state changes.
### Usage
```js
import { atomEffect } from 'jotai-effect'const logEffect = atomEffect((get, set) => {
set(logAtom, get(someAtom)) // Runs on mount or when someAtom changes
return () => {
set(logAtom, 'unmounting') // Cleanup on unmount
}
})// activates the atomEffect while Component is mounted
function Component() {
useAtom(logEffect)
}
```## withAtomEffect
`withAtomEffect` binds an effect to a clone of the target atom. The effect is active while the cloned atom is mounted.
### Signature
```ts
function withAtomEffect(targetAtom: Atom, effect: Effect): Atom
```**targetAtom:** The atom to which the effect is bound.
**effect:** A function for observing and reacting to atom state changes.
**Returns:** An atom that is equivalent to the target atom but having a bound effect.
### Usage
```js
import { withAtomEffect } from 'jotai-effect'const valuesAtom = withAtomEffect(atom(null), (get, set) => {
set(valuesAtom, get(countAtom))
return () => {
// cleanup
}
})
```## Dependency Management
Aside from mount events, the effect runs when any of its dependencies change value.
- **Sync:**
All atoms accessed with `get` inside the effect are added to the atom's dependencies.
Example```js
atomEffect((get, set) => {
// updates whenever `anAtom` changes value
get(anAtom)
})
```
- **Async:**
Asynchronous `get` calls do not add dependencies.
Example```js
atomEffect((get, set) => {
setTimeout(() => {
// does not add `anAtom` as a dependency
get(anAtom)
})
})
```
- **Cleanup:**
`get` calls in cleanup do not add dependencies.
Example```js
atomEffect((get, set) => {
return () => {
// does not add `anAtom` as a dependency
get(anAtom)
}
})
```
- **Dependency Map Recalculation:**
Dependencies are recalculated on every run.
Example```js
atomEffect((get, set) => {
if (get(isEnabledAtom)) {
// `isEnabledAtom` and `anAtom` are dependencies
const aValue = get(anAtom)
} else {
// `isEnabledAtom` and `anotherAtom` are dependencies
const anotherValue = get(anotherAtom)
}
})
```
## Effect Behavior
- **Executes Synchronously:**
`effect` runs synchronous in the current task after synchronous evaluations complete.
Example```js
const logCounts = atomEffect((get, set) => {
set(logAtom, `count is ${get(countAtom)}`)
})
const actionAtom = atom(null, (get, set) => {
get(logAtom) // 'count is 0'
set(countAtom, (value) => value + 1) // effect runs synchronously
get(logAtom) // 'count is 1'
})
store.sub(logCounts, () => {})
store.set(actionAtom)
```
- **Batched Updates:**
Multiple synchronous updates are batched as a single atomic transaction.
Example```js
const tensAtom = atom(0)
const onesAtom = atom(0)
const updateTensAndOnes = atom(null, (get, set) => {
set(tensAtom, (value) => value + 1)
set(onesAtom, (value) => value + 1)
})
const combos = atom([])
const effectAtom = atomEffect((get, set) => {
const value = get(tensAtom) * 10 + get(onesAtom)
set(combos, (arr) => [...arr, value])
})
store.sub(effectAtom, () => {})
store.set(updateTensAndOnes)
store.get(combos) // [00, 11]
```
- **Resistant to Infinite Loops:**
`atomEffect` avoids rerunning when it updates a value that it is watching.
Example```js
atomEffect((get, set) => {
get(countAtom)
set(countAtom, (value) => value + 1) // Will not loop
})
```
- **Cleanup Function:**
The cleanup function is invoked on unmount or before re-evaluation.
Example```js
atomEffect((get, set) => {
const intervalId = setInterval(() => set(clockAtom, Date.now()))
return () => clearInterval(intervalId)
})
```
- **Idempotency:**
`atomEffect` runs once per state change, regardless of how many times it is referenced.
Example```js
let i = 0
const effectAtom = atomEffect(() => {
get(countAtom)
i++
})
store.sub(effectAtom, () => {})
store.sub(effectAtom, () => {})
store.set(countAtom, (value) => value + 1)
console.log(i) // 1
```
- **Conditionally Running Effects:**
`atomEffect` only runs when mounted.
Example```js
atom((get) => {
if (get(isEnabledAtom)) {
get(effectAtom)
}
})
```
- **Supports Peek:**
Use `get.peek` to read atom data without subscribing.
Example```js
const countAtom = atom(0)
atomEffect((get, set) => {
const count = get.peek(countAtom) // Will not add `countAtom` as a dependency
})
```
- **Supports Recursion:**
Recursion is supported with `set.recurse` but not in cleanup.
Example```js
atomEffect((get, set) => {
const count = get(countAtom)
if (count % 10 === 0) {
return
}
set.recurse(countAtom, (value) => value + 1)
})
```