Skip to content

Commit

Permalink
Merge pull request #6 from jotaijs/update-docs
Browse files Browse the repository at this point in the history
update docs and enable all tests
  • Loading branch information
dmaskasky authored Oct 11, 2023
2 parents a8d3a5a + 75b08c3 commit d4fc185
Show file tree
Hide file tree
Showing 2 changed files with 178 additions and 66 deletions.
242 changes: 177 additions & 65 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,35 @@

## atomEffect

`atomEffect` is a utility function, allowing developers to manage side effects in their state management architecture seamlessly. It is useful for observing state changes in atoms and triggering side effects.
`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

```typescript
```ts
type CleanupFn = () => void

type EffectFn = (get: Getter, set: Setter) => CleanupFn | void | Promise<CleanupFn | void>

function atomEffect(effectFn: EffectFn): Atom<void>
```

**effectFn** (required): A function or async 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. Cleanup these side effects with the optionally returned cleanup function.
**effectFn** (required): A function or async 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
```jsx
```js
import { atomEffect } from 'jotai/utils'
const loggingEffect = atomEffect((get, set) => {
// reruns when someAtom changes
// runs on mount or whenever someAtom changes
const value = get(someAtom)
loggingService.setValue(value)
})
```

Setup and Teardown Side Effects
```jsx
```js
const subscriptionEffect = atomEffect((get, set) => {
const unsubscribe = subscribe((value) => {
set(valueAtom, value)
Expand All @@ -43,39 +43,135 @@ const subscriptionEffect = atomEffect((get, set) => {

After defining an effect using `atomEffect`, it can be integrated within another atom's read function or passed to Jotai hooks.

```typescript
```js
import { atom } from 'jotai'
const anAtom = atom((get) => {
// mounts loggingEffect while anAtom is mounted
// mounts the atomEffect when anAtom mounts
get(loggingEffect)
// ... other logic
})
// mounts loggingEffect while the component is mounted
useAtom(loggingEffect)
// mounts the atomEffect when the component mounts
function MyComponent() {
useAtom(subscriptionEffect)
...
}
```

<Codesandbox id="85zrzn">
<Codesandbox id="85zrzn" />

## Behavior

The `atomEffect` behavior during operation and interactions with other atoms is described below.
## The `atomEffect` behavior

- **Cleanup Function:**
The cleanup function is invoked on unmount or before re-evaluation.
<details>
<summary>Example</summary>

```js
atomEffect((get, set) => {
const intervalId = setInterval(() => set(clockAtom, Date.now()))
return () => clearInterval(intervalId)
})
```
</details>

- **Resistent To Infinite Loops:**
`atomEffect` does not rerun when it changes a value that it is watching.

- **Asynchronous Execution:**
`effectFn` runs asynchronously in the next available microtask, after all Jotai synchronous read evaluations have completed.

- **Batches Synchronous Updates:**
Multiple synchronous updates to an atom that `atomEffect` is watching are batched. The effect is rerun only with the final synchronous value.
<details>
<summary>Example</summary>

```js
const countAtom = atom(0)
atomEffect((get, set) => {
// this will not infinite loop
get(countAtom) // after mount, count will be 1
set(countAtom, increment)
})
```
</details>

- **Executes In The Next Microtask:**
`effectFn` runs in the next available microtask, after all Jotai synchronous read evaluations have completed.
<details>
<summary>Example</summary>

```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)
```
</details>

- **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.
<details>
<summary>Example</summary>

```js
const enabledAtom = atom(false)
const countAtom = atom(0)
const updateLettersAndNumbers = 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(updateLettersAndNumbers)
store.get(combos) // [[false, 0], [true, 1]]
```
</details>

- **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.
`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.
<details>
<summary>Example</summary>

```js
atom((get) => {
if (get(isEnabledAtom)) {
get(effectAtom)
}
})
```
</details>

- **Idempotent:**
`atomEffect` runs once when state changes regardless of how many times it is mounted.
<details>
<summary>Example</summary>

```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
})
```
</details>

### Dependency Management

Expand All @@ -84,62 +180,78 @@ Aside from mount events, the effect runs when any of its dependencies change val
- **Sync:**
All atoms accessed with `get` during the synchronous evaluation of the effect are added to the atom's internal dependency map.

```jsx
const asyncEffect = atomEffect((get, set) => {
// updates whenever `anAtom` changes value but not when `anotherAtom` changes value
get(anAtom)
setTimeout(() => {
get(anotherAtom)
}, 5000)
})
```
<details>
<summary>Example</summary>

```js
const asyncEffect = atomEffect((get, set) => {
// updates whenever `anAtom` changes value but not when `anotherAtom` changes value
get(anAtom)
setTimeout(() => {
get(anotherAtom)
}, 5000)
})
```
</details>

- **Async:**
For effects that return a promise, all atoms accessed with `get` prior to the returned promise resolving are added to the atom's internal dependency map. Atoms that have been watched after the promise has resolved, for instance in a `setTimeout`, are not included in the dependency map.

```jsx
const asyncEffect = atomEffect(async (get, set) => {
await new Promise(resolve => setTimeout(resolve, 1000))
// updates whenever `anAtom` changes value but not when `anotherAtom` changes value
get(anAtom)
setTimeout(() => {
get(anotherAtom)
}, 5000)
})
```
<details>
<summary>Example</summary>

```js
const asyncEffect = atomEffect(async (get, set) => {
await new Promise(resolve => setTimeout(resolve, 1000))
// updates whenever `anAtom` changes value but not when `anotherAtom` changes value
get(anAtom)
setTimeout(() => {
get(anotherAtom)
}, 5000)
})
```
</details>

- **Cleanup:**
Accessing atoms with `get` in the cleanup function does not add them to the atom's internal dependency map.

```jsx
const asyncEffect = atomEffect((get, set) => {
// runs once on atom mount
// does not update when `idAtom` changes
const unsubscribe = subscribe((value) => {
const id = get(idAtom)
set(valueAtom, { id value })
<details>
<summary>Example</summary>

```js
const asyncEffect = atomEffect((get, set) => {
// runs once on atom mount
// does not update when `idAtom` changes
const unsubscribe = subscribe((value) => {
const id = get(idAtom)
set(valueAtom, { id value })
})
return () => {
unsubscribe(get(idAtom))
}
})
return () => {
unsubscribe(get(idAtom))
}
})
```
```
</details>

- **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.

```jsx
const isEnabledAtom = atom(true)
const asyncEffect = atomEffect((get, set) => {
// if `isEnabledAtom` is true, reruns when `isEnabledAtom` or `anAtom` changes value
// otherwise reruns when `isEnabledAtom` or `anotherAtom` changes value
if (get(isEnabledAtom)) {
const aValue = get(anAtom)
} else {
const anotherValue = get(anotherAtom)
}
})
```
<details>
<summary>Example</summary>

```js
const isEnabledAtom = atom(true)
const asyncEffect = 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)
}
})
```
</details>


2 changes: 1 addition & 1 deletion __tests__/atomEffect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,7 @@ it('should batch synchronous updates as a single transaction', async () => {
expect(result.current.lettersAndNumbers).toEqual(['a0', 'b1'])
})

it.only('should run the effect once even if the effect is mounted multiple times', async () => {
it('should run the effect once even if the effect is mounted multiple times', async () => {
expect.assertions(3)
const lettersAtom = atom('a')
lettersAtom.debugLabel = 'lettersAtom'
Expand Down

0 comments on commit d4fc185

Please sign in to comment.