jotai-effect

repository·main·Indexed 41 days ago

https://github.com/jotaijs/jotai-effect

A utility package for managing reactive side effects within Jotai state management. It provides tools like `atomEffect` for creating reactive atoms, `withAtomEffect` for binding effects to existing atoms, and `observe` for running global side effects outside the React lifecycle. Version 2.4.1.

Tokens
3.3K
Snippets
14
Records
15
Agent score
42%

What's inside jotai-effect

  1. Understand effect execution and behavior

    main

    Effects in jotai-effect follow these behaviors:

    • Synchronous Execution: Effects run synchronously in the current task after synchronous evaluations complete.
    • Batched Updates: Multiple synchronous updates are batched into a single atomic transaction.
    • Infinite Loop Resistance: atomEffect avoids re-running if it updates a value that it is currently watching.
    • Idempotency: An effect runs exactly once per state change, regardless of how many times the effect atom is referenced.
    • Cleanup: A returned function is invoked on unmount or before the effect re-evaluates due to dependency changes.
    • Recursion: You can use set.recurse to trigger updates, but recursion is not supported within cleanup functions.
    // Example of loop resistance
    atomEffect((get, set) => {
      get(countAtom)
      set(countAtom, (value) => value + 1) // Will not loop
    })
    
    // Example of recursion
    atomEffect((get, set) => {
      const count = get(countAtom)
      if (count % 10 === 0) return
      set.recurse(countAtom, (value) => value + 1)
    })
  2. How dependency management works in effects

    main

    Effects automatically track dependencies based on how you access atoms:

    • Synchronous get: Any atom accessed via get inside the effect body is added as a dependency. Dependencies are recalculated on every run, allowing for conditional dependencies.
    • Asynchronous get: Calls to get inside asynchronous blocks (e.g., inside a setTimeout) do not add dependencies.
    • Cleanup get: Calls to get inside a returned cleanup function do not add dependencies.
    • get.peek: Using get.peek(atom) allows you to read a value without subscribing to it as a dependency.
    // Conditional dependencies
    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)
      }
    })
    
    // Using peek to avoid dependencies
    atomEffect((get, set) => {
      const count = get.peek(countAtom) // Will not add `countAtom` as a dependency
    })
  3. Use observe with React Providers

    main

    When using observe in a React application with a custom store, ensure you pass the same store instance to both observe and the Jotai Provider to ensure the effect is mounted to the correct store.

    const store = createStore()
    const unobserve = observe((get, set) => {
      set(logAtom, `someAtom changed: ${get(someAtom)}`)
    }, store)
    
    <Provider store={store}>...</Provider>
  4. Use observe to run global side effects

    main

    The observe function mounts an effect to a Jotai store to watch state changes. It is ideal for running global side effects or logic at the store level outside of the React lifecycle.

    If you do not have access to the store object or are not using the default store, use atomEffect or withAtomEffect instead.

    import { observe } from 'jotai-effect'
    
    const unobserve = observe((get, set) => {
      set(logAtom, `someAtom changed: ${get(someAtom)}`)
    })
    
    // To stop the effect and clean up:
    unobserve()
  5. Use withAtomEffect to bind effects to atoms

    main

    withAtomEffect binds an effect to a clone of a target atom. The effect remains active as long as the cloned atom is mounted. It returns a new atom that is functionally equivalent to the target atom but includes the bound effect.

    import { withAtomEffect } from 'jotai-effect'
    
    const valuesAtom = withAtomEffect(atom(null), (get, set) => {
      set(valuesAtom, get(countAtom))
      return () => {
        // cleanup
      }
    })
  6. Use atomEffect to create reactive atoms

    main

    atomEffect creates an atom that declares side effects which react to state changes when the atom is mounted. This is typically used within a React component by calling useAtom(effectAtom).

    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
      }
    })
    
    function Component() {
      // activates the atomEffect while Component is mounted
      useAtom(logEffect)
    }
  7. Use withAtomEffect to attach effects to existing atoms

    main

    The withAtomEffect function is used to attach an effect to an existing atom without modifying the original atom's definition. This is useful for adding side effects to atoms provided by external libraries or shared state.

    import { withAtomEffect } from 'jotai-effect'
    
    const atomWithEffect = withAtomEffect(baseAtom, (effect) => {
      // effect logic here
    })
  8. Wrap an atom with effects using withAtomEffect

    main

    The withAtomEffect function is a higher-order function that wraps an existing Jotai atom to attach an effect to it. This allows the atom to trigger side effects whenever its state changes or when it is mounted/unmounted.

    When you wrap an atom with withAtomEffect, the resulting atom retains all the original properties of the targetAtom (read/write capabilities) but gains an additional .effect property containing the Effect logic.

    Key behaviors:

    • The effect is automatically managed by the Jotai store.
    • It handles subscription and invalidation: when the atom's value changes, the effect is re-run.
    • It manages lifecycle: the effect is mounted when the atom is mounted and unmounted when the atom is unmounted.
    • It provides a specialized getter and setter to the effect function to prevent infinite loops during state updates.
    import { withAtomEffect } from 'jotai-effect'
    
    // Assuming 'myAtom' is an existing Jotai atom
    const atomWithEffect = withAtomEffect(myAtom, (get, set) => {
      // Your effect logic here
      const value = get(myAtom)
      console.log('Atom value changed:', value)
    })
  9. Create an effect atom with atomEffect()

    main

    Use atomEffect to create an atom that runs a side effect. This atom is designed to be mounted within the Jotai atom tree. When the atom is mounted (e.g., by being read by a component or another atom), the provided effect function executes.

    An Effect function receives two specialized arguments:

    1. get: A GetterWithPeek used to read atoms. It includes a .peek() method to read an atom's value without subscribing to it.
    2. set: A SetterWithRecurse used to write to atoms. It includes a .recurse() method to allow writing to atoms while bypassing certain dependency tracking constraints (useful for specific recursive update patterns).

    The effect function can optionally return a Cleanup function (a function that returns void), which will be called when the atom is unmounted.

    import { atomEffect } from 'jotai-effect'
    
    const myEffectAtom = atomEffect((get, set) => {
      // 1. Perform side effect
      console.log('Effect running')
    
      // 2. Read atoms (subscribes to changes)
      const value = get(someAtom)
    
      // 3. Read atoms without subscribing
      const peekValue = get.peek(someAtom)
    
      // 4. Write to atoms
      set(otherAtom, newValue)
    
      // 5. Return cleanup function
      return () => {
        console.log('Cleaning up...')
      }
    })
  10. Run side effects outside of React using observe()

    main

    The observe function allows you to mount an Effect to a Jotai store, enabling side effects to run even when no React components are currently using the atoms. This is useful for running logic in vanilla JavaScript environments, background workers, or outside the React component lifecycle.

    To use it, pass an Effect (created via atomEffect) and an optional Jotai Store. If no store is provided, it defaults to the store returned by getDefaultStore() from jotai/vanilla.

    observe returns an Unobserve function, which you should call to stop the effect and clean up its subscription to the store.

    import { observe } from 'jotai-effect'
    import { atomEffect } from 'jotai-effect'
    import { getDefaultStore } from 'jotai/vanilla'
    
    const store = getDefaultStore()
    const myEffect = atomEffect((...) => { /* side effect logic */ })
    
    // Start the effect
    const unobserve = observe(myEffect, store)
    
    // Later, stop the effect
    unobserve()
  11. Use observe to monitor atom changes

    main

    The observe function allows you to subscribe to changes in an atom and execute a callback whenever the atom's value updates. This is a way to perform side effects in response to state changes outside of the standard Jotai atom lifecycle.

    import { observe } from 'jotai-effect'
    
    observe(myAtom, (newValue) => {
      console.log('Atom changed to:', newValue)
    })