npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

Iโ€™ve always been into building performant and accessible sites, but lately Iโ€™ve been taking it extremely seriously. So much so that Iโ€™ve been building a tool to help me optimize and monitor the sites that I build to make sure that Iโ€™m making an attempt to offer the best experience to those who visit them. If youโ€™re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, ๐Ÿ‘‹, Iโ€™m Ryan Hefnerย  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If youโ€™re interested in other things Iโ€™m working on, follow me on Twitter or check out the open source projects Iโ€™ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soonโ€“ish.

Open Software & Tools

This site wouldnโ€™t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you ๐Ÿ™

ยฉ 2024 โ€“ย Pkg Stats / Ryan Hefner

jotai-effect

v1.0.3

Published

๐Ÿ‘ป๐Ÿ”

Downloads

191,006

Readme

Effect

jotai-effect is a utility package for reactive side effects.

install

npm i 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

type CleanupFn = () => void

type EffectFn = (
  get: Getter & { peek: Getter },
  set: Setter & { recurse: Setter },
) => CleanupFn | void

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

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

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

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.

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.

    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 with set that it is watching.

    const countAtom = atom(0)
    atomEffect((get, set) => {
      // this will not infinite loop
      get(countAtom) // after mount, count will be 1
      set(countAtom, increment)
    })
  • Supports Recursion: Recursion is supported with set.recurse for both sync and async use cases, but is not supported in the cleanup function.

    const countAtom = atom(0)
    atomEffect((get, set) => {
      // increments count once per second
      const count = get(countAtom)
      const timeoutId = setTimeout(() => {
        set.recurse(countAtom, increment)
      }, 1000)
      return () => clearTimeout(timeoutId)
    })
  • Supports Peek: Read atom data without subscribing to changes with get.peek.

    const countAtom = atom(0)
    atomEffect((get, set) => {
      // will not rerun when countAtom changes
      const count = get.peek(countAtom)
    })
  • Executes In The Next Microtask: effectFn runs in the next available microtask, after all Jotai synchronous read evaluations have completed.

    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.

    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.

    atom((get) => {
      if (get(isEnabledAtom)) {
        get(effectAtom)
      }
    })
  • Idempotent: atomEffect runs once when state changes regardless of how many times it is mounted.

    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.

    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.

    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.

    atomEffect((get, set) => {
      // runs once on mount
      // does not update when `idAtom` changes
      const unsubscribe = subscribe((valueAtom) => {
        const value = get(valueAtom)
        // ...
      })
      return () => {
        const id = get(idAtom)
        unsubscribe(id)
      }
    })
  • 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.

    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)
      }
    })

withAtomEffect

withAtomEffect is a utility to define an effect that is bound to the target atom. This is useful for creating effects that are active when the target atom is mounted.

Parameters

declare function withAtomEffect<T>(
  targetAtom: Atom<T>,
  effectFn: EffectFn,
): Atom<T>

targetAtom (required): The atom to which the effect is mounted.

effectFn (required): A function for listening to state updates with get and writing state updates with set.

Returns: An atom that is equivalent to the target atom but with the effect mounted.

Usage

import { withAtomEffect } from 'jotai-effect'

const anAtom = atom(0)
const loggingAtom = withAtomEffect(anAtom, (get, set) => {
  // runs on mount or whenever anAtom changes
  const value = get(anAtom)
  loggingService.setValue(value)
})

Comparison with useEffect

Component Side Effects

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!