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
): 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 { 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
Check this React Native starter
template (opens in a new tab) that uses
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 { 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 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 when the seector'sget
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-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>
)
}