Docs
API

API

create

  • 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 { create } from 'atomic-state'
 
import AsyncStorage from '@react-native-async-storage/async-storage'
 
const count = create({
  key: 'count',
  default: 0,
  persist: true,
  persistenceProvider: AsyncStorage
})
 
export const useCount = count.useValue
export const setCount = count.setValue
  • 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 { create } from 'atomic-state'
 
const text = create({
  key: 'text',
  default: ''
})
 
const count = create({
  key: 'count',
  default: 0,
  actions: {
    change({ args, get, dispatch }) {
      // The latest text value
      const text = get(textState)
 
      switch (args.type) {
        case '+':
          dispatch((count) => count + 1)
          break
        case '-':
          dispatch((count) => count - 1)
          break
      }
    }
  }
})
 
const useCount = count.useValue
const countActions = count.actions
 
export default function App() {
  const count = useCount()
 
  return (
    <div>
      <p>{count}</p>
      <section>
        <button onClick={() => countActions.change({ type: '+' })}>++</button>
        <button onClick={() => countActions.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 { create } from 'atomic-state'
 
const count = create({
  key: 'count',
  default: 0,
  effects: [
    ({ state }) => {
      return state < 9
    }
  ]
})
 
export const useCount = count.useValue
export const setCount = count.setValue

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 the state update.

Effects can also return cleanup functions:

import { create } from 'atomic-state'
 
const count = create({
  key: 'count',
  default: 0,
  effects: [
    ({ state }) => {
      console.log('Subscribing')
      return () => {
        console.log('Unsubscribing')
      }
    }
  ]
})
 
export const useCount = count.useValue
export const setCount = count.setValue

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

import { create } from 'atomic-state'
 
const count = create({
  key: 'count',
  default: 0,
  effects: [
    async ({ state, cancel }) => {
      if (state > 9) cancel()
    }
  ]
})
 
export const useCount = count.useValue
export const setCount = count.setValue

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 when the seector's get function returns a promise, e.g. 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 { create } from 'atomic-state'
 
const text = create({
  key: 'textState',
  default: ''
})
 
// The selector
const results = create({
  key: 'results',
  default: [],
  get({ get }) {
    const textValue = get(text)
 
    return fetch('/search?q=' + textValue).then((res) => res.json())
  }
})
 
export const useText = text.useValue
export const setText = text.setValue
export const useResults = results.useValue

Snapshots

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

import { AtomicState, create, takeSnapshot } from 'atomic-state'
 
const count = create({
  key: 'count',
  default: 0
})
 
const double = create({
  key: 'double',
  get({ get }) {
    const countValue = get(count)
    return countValue * 2
  }
})
 
const useCount = count.useValue
const setCount = count.setValue
const useDouble = double.useValue
 
function Counter() {
  const count = useCount()
 
  return <button onClick={() => setCount((c) => c + 1)}>{count}</button>
}
 
function Double() {
  const doubleValue = useDouble()
  return <p>{doubleValue}</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):

 {
  count: 0,
  double: 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='anotherProvider'>
          <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:

declare function takeSnapshot(store: string): { [k: string]: any }

For the example above, it would be:

takeSnapshot('anotherProvider')

And you should see this:

{
  count: 0,
  double: 0,
}

AtomicState

The Atomic State root component.

  • value: The default values for atoms. Can be used for SSR:
import { AtomicState } from 'atomic-state'
 
export default function App({ children }) {
  return (
    <AtomicState
      value={{
        count: 0
      }}
    >
      <main>{children}</main>
    </AtomicState>
  )
}
  • 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 August 8, 2024