Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/himself65/jotai-effect
A Jōtai utility package for reactive side-effects
https://github.com/himself65/jotai-effect
Last synced: about 2 months ago
JSON representation
A Jōtai utility package for reactive side-effects
- Host: GitHub
- URL: https://github.com/himself65/jotai-effect
- Owner: himself65
- License: mit
- Fork: true (jotaijs/jotai-effect)
- Created: 2023-10-13T23:52:38.000Z (about 1 year ago)
- Default Branch: main
- Last Pushed: 2023-12-05T20:16:13.000Z (12 months ago)
- Last Synced: 2024-04-14T03:08:59.522Z (7 months ago)
- Language: TypeScript
- Size: 415 KB
- Stars: 0
- Watchers: 0
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
README
# Effect
[jotai-effect](https://jotai.org/docs/integrations/effect) is a utility package for reactive side effects.
## install
```
yarn add jotai-effect
```## atomEffect
`atomEffect` is a utility function for declaring side effects and synchronizing atoms in Jotai. It is useful for observing and reacting to state changes.
## Parameters
```ts
type CleanupFn = () => voidtype EffectFn = (get: Getter, set: Setter) => CleanupFn | void
function atomEffect(effectFn: EffectFn): Atom
```**effectFn** (required): A function for listening to state updates with `get` and writing state updates with `set`. The `effectFn` is useful for creating side effects that interact with other Jotai atoms. You can cleanup these side effects by returning a cleanup function.
## Usage
Subscribe to Atom Changes
```js
import { atomEffect } from 'jotai-effect'const loggingEffect = atomEffect((get, set) => {
// runs on mount or whenever someAtom changes
const value = get(someAtom)
loggingService.setValue(value)
})
```Setup and Teardown Side Effects
```js
import { atomEffect } from 'jotai-effect'const subscriptionEffect = atomEffect((get, set) => {
const unsubscribe = subscribe((value) => {
set(valueAtom, value)
})
return unsubscribe
})
```## Mounting with Atoms or Hooks
After defining an effect using `atomEffect`, it can be integrated within another atom's read function or passed to Jotai hooks.
```js
const anAtom = atom((get) => {
// mounts the atomEffect when anAtom mounts
get(loggingEffect)
// ...
})// mounts the atomEffect when the component mounts
function MyComponent() {
useAtom(subscriptionEffect)
// ...
}
```## The `atomEffect` behavior
- **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)
})
```
- **Resistent To Infinite Loops:**
`atomEffect` does not rerun when it changes a value that it is watching.
Example```js
const countAtom = atom(0)
atomEffect((get, set) => {
// this will not infinite loop
get(countAtom) // after mount, count will be 1
set(countAtom, increment)
})
```
- **Executes In The Next Microtask:**
`effectFn` runs in the next available microtask, after all Jotai synchronous read evaluations have completed.
Example```js
const countAtom = atom(0)
const logAtom = atom([])
const logCounts = atomEffect((get, set) => {
set(logAtom, (curr) => [...curr, get(countAtom)])
})
const setCountAndReadLog = atom(null, async (get, set) => {
get(logAtom) // [0]
set(countAtom, increment) // effect runs in next microtask
get(logAtom) // [0]
await Promise.resolve().then()
get(logAtom) // [0, 1]
})
store.set(setCountAndReadLog)
```
- **Batches Synchronous Updates (Atomic Transactions):**
Multiple synchronous updates to `atomEffect` atom dependencies are batched. The effect is run with the final values as a single atomic transaction.
Example```js
const enabledAtom = atom(false)
const countAtom = atom(0)
const updateEnabledAndCount = atom(null, (get, set) => {
set(enabledAtom, (value) => !value)
set(countAtom, (value) => value + 1)
})
const combos = atom([])
const combosEffect = atomEffect((get, set) => {
set(combos, (arr) => [...arr, [get(enabledAtom), get(countAtom)]])
})
store.set(updateEnabledAndCount)
store.get(combos) // [[false, 0], [true, 1]]
```
- **Conditionally Running atomEffect:**
`atomEffect` is active only when it is mounted within the application. This prevents unnecessary computations and side effects when they are not needed. You can disable the effect by unmounting it.
Example```js
atom((get) => {
if (get(isEnabledAtom)) {
get(effectAtom)
}
})
```
- **Idempotent:**
`atomEffect` runs once when state changes regardless of how many times it is mounted.
Example```js
let i = 0
const effectAtom = atomEffect(() => {
get(countAtom)
i++
})
const mountTwice = atom(() => {
get(effectAtom)
get(effectAtom)
})
store.set(countAtom, increment)
Promise.resolve.then(() => {
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.
Example```js
atomEffect((get, set) => {
// updates whenever `anAtom` changes value but not when `anotherAtom` changes value
get(anAtom)
setTimeout(() => {
get(anotherAtom)
}, 5000)
})
```
- **Async:**
For async effects, you should use an abort controller to cancel pending fetch requests and promises.
Example```js
atomEffect((get, set) => {
const count = get(countAtom) // countAtom is an atom dependency
const abortController = new AbortController()
;(async () => {
try {
await delay(1000)
abortController.signal.throwIfAborted()
get(dataAtom); // dataAtom is not an atom dependency
} catch(e) {
if (e instanceof AbortError) {
// async cleanup logic here
} else {
console.error(e)
}
}
})()
return () => {
// abort when countAtom changes
abortController.abort(new AbortError())
}
})
```
- **Cleanup:**
Accessing atoms with `get` in the cleanup function does not add them to the atom's internal dependency map.
Example```js
atomEffect((get, set) => {
// runs once on mount or when valueAtom changes
// does not update when `idAtom` changes
const unsubscribe = subscribe((value) => {
const value = get(valueAtom)
// ...
})
return () => {
unsubscribe(get(idAtom))
}
})
```
- **Recalculation of Dependency Map:**
The dependency map is recalculated on every run. If an atom was not watched during the current run, it will not be in the current run's dependency map. Only actively watched atoms are considered dependencies.
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)
}
})
```
### 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 logic 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. The 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!