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
): Iftrue
, any writes to this atom will be saved in thepersistenceProvider
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 uniquedefault
: 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-atompersistenceProvider
:
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>
)
}