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 thepersistenceProviderpassed. -
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.setValueCheck 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.setValueIf 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.setValueState 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.setValueSelectors
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'sgetfunction 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.useValueSnapshots
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>
)
}