Docs
API

API

atom

  • key (string): The key of the store. This should be unique because it will be used to keep track of each atom's state.

  • default (any): The default value of the store.

  • persist (boolean): If true, any writes to this atom will be saved in the persistenceProvider passed.

  • persistenceProvider (PersistenceStoreType): The persistence mechanism to save atom's values. This should have the following shape:

type PersistenceStoreType = {
  get?: (key: string) => any
  getItem?: (key: string) => any
  getItemAsync?: (key: string) => any
 
  set?: (key: string, value: any) => void
  setItem?: (key: string, value: any) => void
  setItemAsync?: (key: string, value: any) => void
 
  remove?: (key: string) => void
  removeItem?: (key: string) => void
  removeItemAsync?: (key: string) => void
  delete?: (key: string) => void
  deleteItem?: (key: string) => void
  deleteItemAsync?: (key: string) => void
}

Persistence providers supported by default: localStorage (default), sessionStorage, AsyncStorage and expo-secure-store (React Native), js-cookie.

Example with AsyncStorage:

import { atom } from 'atomic-state'
 
import AsyncStorage from '@react-native-async-storage/async-storage'
 
const countState = atom({
  key: 'count',
  default: 0,
  persist: true,
  persistenceProvider: AsyncStorage
})

Check this React Native starter template (opens in a new tab) that uses atomic-state expo-secure-store (opens in a new tab) as persistence provider

  • actions: An object containing the state's actions. Internally, they have access to the latest state value and function to update it. They can also read other atoms' and selectors' states:
import { atom, useValue, useActions } from 'atomic-state'
 
const textState = atom({
  key: 'text',
  default: ''
})
 
const countState = atom({
  key: 'count',
  default: 0,
  actions: {
    change({ args, state, dispatch, get }) {
      // The latest textState value
      const text = get(textState)
 
      switch (args.type) {
        case '+':
          dispatch(state + 1)
          break
        case '-':
          dispatch(state - 1)
          break
      }
    }
  }
})
 
export default function App() {
  const count = useValue(countState)
 
  // The useActions hook returns only the actions of the atom
  const actions = useActions(countState)
 
  return (
    <div>
      <p>{count}</p>
      <section>
        <button onClick={() => actions.change({ type: '+' })}>++</button>
        <button onClick={() => actions.change({ type: '-' })}>--</button>
      </section>
    </div>
  )
}

You only need to pass one argument to an action. This will be the args property in your reducer

  • effects: Side effects that run once for every update. They can be used to sync state with external providers, or even prevent state updates without re-renders:

In this example, updates are commited only when the new value is lower than 9:

import { atom } from 'atomic-state'
 
const countState = atom({
  key: 'count',
  default: 0,
  effects: [
    ({ state, previous }) => {
      return state < 9
    }
  ]
})

If any effect returns false, the state update will be prevented. This will also prevent any re-renders that would have happened as the result of a state update.

Effects can also return cleanup functions:

import { atom } from 'atomic-state'
 
const countState = atom({
  key: 'count',
  default: 0,
  effects: [
    ({ state, previous }) => {
      console.log('Subscribing')
      return () => {
        console.log('Unsubscribing')
      }
    }
  ]
})

State updates can also be prevented in effects that are marked as async by using the cancel function:

import { atom } from 'atomic-state'
 
const countState = atom({
  key: 'count',
  default: 0,
  effects: [
    async ({ state, cancel }) => {
      if (state > 9) cancel()
    }
  ]
})

Selectors

Selectors are a way to create derived states.

  • key: The key of the filter. This should be unique
  • default: The default value of the filter. This is not necesary for synchronus operations, like filtering an array state. It's recommended that you set a default value with asynchronus code, like a network request.
  • get: The action that will return the derived state. It's also used to subscribe the selector to state changes in other atoms
import { atom } from 'atomic-state'
 
const textState = atom({
  key: 'textState',
  default: ''
})
 
// The selector
const resultsState = atom({
  key: 'results',
  default: [],
  get({ get }) {
    const text = get(textState)
 
    return fetch('/search?q=' + text).then((res) => res.json())
  }
})

Snapshots

You can take snapshots that include information about all the atoms and selectors:

import {
  AtomicState,
  atom,
  useAtom,
  useValue,
  takeSnapshot
} from 'atomic-state'
 
const countState = atom({
  key: 'count',
  default: 0
})
 
const doubleState = atom({
  key: 'double',
  get({ get }) {
    const count = get(countState)
    return count * 2
  }
})
 
function Counter() {
  const [count, setCount] = useAtom(countState)
 
  return <button onClick={() => setCount((c) => c + 1)}>{count}</button>
}
 
function Double() {
  const double = useValue(doubleState)
  return <p>{double}</p>
}
 
export default function App() {
  return (
    <main>
      <button
        onClick={() => {
          console.log(takeSnapshot())
        }}
      >
        Take snapshot
      </button>
      <AtomicState>
        <Counter />
        <Double />
      </AtomicState>
      <AtomicState storeName='another-provider'>
        <Counter />
        <Double />
      </AtomicState>
    </main>
  )
}

If you click the Take snapshot button, you will see something like this (considering count has not changed):

const mySnapshot = {
  default: {
    double: 0,
    count: 0
  },
  'another-provider': {
    double: 0,
    count: 0
  }
}

You may notice that a storeName prop was added to AtomicState. This creates two different state providers that are independent from each other. It is possible to have different providers:

export default function App() {
  return (
    <main>
      <button
        onClick={() => {
          console.log(takeSnapshot())
        }}
      ></button>
      <AtomicState>
        <Counter />
        <AtomicState storeName='another-provider'>
          <Double />
        </AtomicState>
      </AtomicState>
      <AtomicState storeName='another-provider'>
        <Counter />
        <Double />
      </AtomicState>
    </main>
  )
}

If you want to take a snapshot of only one provider, pass the storeName as the first argument:

takeSnapshot('store')

For the example above, it would be:

takeSnapshot('another-provider')

And you should see something like this:

{
  double: 0,
  count: 0
}

AtomicState

The Atomic State root component.

  • atoms: The default values for atoms. This is very useful with SSR:
import { AtomicState } from 'atomic-state'
 
export default function App({ children }) {
  return (
    <main>
      <AtomicState
        default={{
          count: 0
        }}
      >
        {children}
      </AtomicState>
    </main>
  )
}
  • persistenceProvider: The persistence mechanism. This will only replace the default global persistence mechanism (localStorage), not the per-atom persistenceProvider:
import { AtomicState } from 'atomic-state'
 
import AsyncStorage from '@react-native-async-storage/async-storage'
 
export default function App({ children }) {
  return (
    <main>
      <AtomicState persistenceProvider={AsyncStorage}>{children}</AtomicState>
    </main>
  )
}
Last updated on January 27, 2024