Skip to content

Latest commit

 

History

History
293 lines (229 loc) · 7.31 KB

atom-creators.mdx

File metadata and controls

293 lines (229 loc) · 7.31 KB
title nav
Atom creators
5.02

atomWithToggle

atomWithToggle creates a new atom with a boolean as initial state & a setter function to toggle it.

This avoids the boilerplate of having to set up another atom just to update the state of the first.

import { WritableAtom, atom } from 'jotai'

export function atomWithToggle(
  initialValue?: boolean
): WritableAtom<boolean, boolean | undefined> {
  const anAtom = atom(initialValue, (get, set, nextValue?: boolean) => {
    const update = nextValue ?? !get(anAtom)
    set(anAtom, update)
  })

  return anAtom as WritableAtom<boolean, boolean | undefined>
}

An optional initial state can be provided as the first argument.

The setter function can have an optional argument to force a particular state, such as if you want to make a setActive function out of it.

Here is how it's used.

import { atomWithToggle } from 'XXX'

// will have an initial value set to true
const isActiveAtom = atomWithToggle(true)

And in a component:

const Toggle = () => {
  const [isActive, toggle] = useAtom(isActiveAtom)

  return (
    <>
      <button onClick={() => toggle()}>
        isActive: {isActive ? 'yes' : 'no'}
      </button>
      <button onClick={() => toggle(true)}>force true</button>
      <button onClick={() => toggle(false)}>force false</button>
    </>
  )
}

atomWithToggleAndStorage

atomWithToggleAndStorage is like atomWithToggle but also persist the state anytime it changes in given storage using atomWithStorage.

Here is the source:

import { WritableAtom, atom } from 'jotai'
import { atomWithStorage } from 'jotai/utils'

export function atomWithToggleAndStorage(
  key: string,
  initialValue?: boolean,
  storage?: any
): WritableAtom<boolean, boolean | undefined> {
  const anAtom = atomWithStorage(key, initialValue, storage)
  const derivedAtom = atom(
    (get) => get(anAtom),
    (get, set, nextValue?: boolean) => {
      const update = nextValue ?? !get(anAtom)
      set(anAtom, update)
    }
  )

  return derivedAtom
}

And how it's used:

import { atomWithToggleAndStorage } from 'XXX'

// will have an initial value set to false & get stored in localStorage under the key "isActive"
const isActiveAtom = atomWithToggleAndStorage('isActive')

The usage in a component is also the same as atomWithToggle.

atomWithCompare

atomWithCompare creates atom that triggers updates when custom compare function areEqual(prev, next) is false.

This can help you avoid unwanted re-renders by ignoring state changes that don't matter to your application.

Note: Jotai uses Object.is internally to compare values when changes occur. If areEqual(a, b) returns false, but Object.is(a, b) returns true, Jotai will not trigger an update.

import { atomWithReducer } from 'jotai/utils'

export function atomWithCompare<Value>(
  initialValue: Value,
  areEqual: (prev: Value, next: Value) => boolean
) {
  return atomWithReducer(initialValue, (prev: Value, next: Value) => {
    if (areEqual(prev, next)) {
      return prev
    }

    return next
  })
}

Here's how you'd use it to make an atom that ignores updates that are shallow-equal:

import { atomWithCompare } from 'XXX'
import { shallowEquals } from 'YYY'
import { CSSProperties } from 'react'

const styleAtom = atomWithCompare<CSSProperties>(
  { backgroundColor: 'blue' },
  shallowEquals
)

In a component:

const StylePreview = () => {
  const [styles, setStyles] = useAtom(styleAtom)

  return (
    <div>
      <div styles={styles}>Style preview</div>

      {/* Clicking this button twice will only trigger one render */}
      <button onClick={() => setStyles({ ...styles, backgroundColor: 'red' })}>
        Set background to red
      </button>

      {/* Clicking this button twice will only trigger one render */}
      <button onClick={() => setStyles({ ...styles, fontSize: 32 })}>
        Enlarge font
      </button>
    </div>
  )
}

atomWithRefresh

atomWithRefresh creates a derived atom that can be force-refreshed, by using the update function.

This is helpful when you need to refresh asynchronous data after performing a side effect.

It can also be used to implement "pull to refresh" functionality.

import { atom, Getter } from 'jotai'

export function atomWithRefresh<T>(fn: (get: Getter) => T) {
  const refreshCounter = atom(0)

  return atom(
    (get) => {
      get(refreshCounter)
      return fn(get)
    },
    (_, set) => set(refreshCounter, (i) => i + 1)
  )
}

Here's how you'd use it to implement an refresh-able source of data:

import { atomWithRefresh } from 'XXX'

const postsAtom = atomWithRefresh((get) =>
  fetch('https://jsonplaceholder.typicode.com/posts').then((r) => r.json())
)

In a component:

const PostsList = () => {
  const [posts, refreshPosts] = useAtom(postsAtom)

  return (
    <div>
      <ul>
        {posts.map((post: any) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>

      {/* Clicking this button will re-fetch the posts */}
      <button type="button" onClick={refreshPosts}>
        Refresh posts
      </button>
    </div>
  )
}

atomWithListeners

atomWithListeners creates an atom and a hook. The hook can be called to add a new listener. The hook takes as an argument a callback, and that callback is called every time the atom's value is set. The hook also returns a function to remove the listener.

This can be useful when you want to create a component that can listen to when an atom's state changes without having to re-render that component with each of those state changes.

import { useEffect } from 'react'
import { atom, Getter, Setter, SetStateAction } from 'jotai'
import { useUpdateAtom } from 'jotai/utils'

type Callback<Value> = (
  get: Getter,
  set: Setter,
  newVal: Value,
  prevVal: Value
) => void

export function atomWithListeners<Value>(initialValue: Value) {
  const baseAtom = atom(initialValue)
  const listenersAtom = atom(<Callback<Value>[]>[])
  const anAtom = atom(
    (get) => get(baseAtom),
    (get, set, arg: SetStateAction<Value>) => {
      const prevVal = get(baseAtom)
      set(baseAtom, arg)
      const newVal = get(baseAtom)
      get(listenersAtom).forEach((callback) => {
        callback(get, set, newVal, prevVal)
      })
    }
  )
  const useListener = (callback: Callback<Value>) => {
    const setListeners = useUpdateAtom(listenersAtom)
    useEffect(() => {
      setListeners((prev) => [...prev, callback])
      return () =>
        setListeners((prev) => {
          const index = prev.indexOf(callback)
          return [...prev.slice(0, index), ...prev.slice(index + 1)]
        })
    }, [setListeners, callback])
  }
  return [anAtom, useListener] as const
}

In a component:

const [countAtom, useCountListener] = atomWithListeners(0)

function EvenCounter() {
  const [evenCount, setEvenCount] = useState(0)

  useCountListener(
    useCallback(
      (get, set, newVal, prevVal) => {
        // Every time `countAtom`'s value is set, we check if its new value
        // is even, and if it is, we increment `evenCount`.
        if (newVal % 2 === 0) {
          setEvenCount((c) => c + 1)
        }
      },
      [setEvenCount]
    )
  )

  return <>Count was set to an even number {evenCount} times.</>
}