⚠️ This library is still in beta! While the API is mostly stable it may change in the future. The concepts of the library may also change and the library itself needs battle testing.
An atom-based state manager for React, Preact, Vue, Svelte, SolidJS, and vanilla JS.
- Atomic. Values are stored per atom instance. No need to call a selector function on every value change.
- Signals-like. Computed atoms track dependencies automatically.
- Batchable. Setters can be called in a batch to reduce recomputations.
- TypeScript-first. This library is written in TypeScript and has good TypeScript support.
- Framework agnostic. Makes it easy to manipulate the state without any frameworks.
Use your favorite package manager or a 3rd-party tool to import the library in browser.
npm i atomous<script src="https://cdn.jsdelivr.net/npm/atomous"></script>import atomous from 'https://esm.run/atomous'The $ symbol is not required in atom names, it's just a convenient way to name them.
import { atom } from 'atomous'
const $count = atom(0)
$count.subscribe(count => console.log(`Count is ${count}`))
function increment() {
$count.update(value => value + 1)
}
increment()- persistent - synchronises its value with provided storage (e.g. localStorage or other custom solutions)
All atom types can be created via either a function or a class constructor. Value of any type of atom is only initialized on subscribe or a first get call.
Regular atom class. Used to just store a value.
- Function:
atom(<initialValue>, [options]) - Class:
new RegularAtom(<initialValue>, [options])
import { atom } from 'atomous'
const $count = atom(0)
$count.subscribe((count) => {
console.log(`Count is ${count}`)
})
// Dynamically update the value
$count.update(count => count + 1)
// Set a completely new value
$count.set(123)This atom computes its value using provided function and automatically tracks used dependencies.
- Function:
computed(<compute>, [options]) - Class:
new ComputedAtom(<compute>, [options])
import { atom, computed } from 'atomous'
const $count = atom(0)
const $double = computed(() => {
return $count.get() * 2
})
$double.subscribe((double) => {
console.log(`Double is ${double}`)
})
// Changing any dependency will update the value
$count.update(count => count + 1) // $double will be updatedAdded in
0.2.0
Computed atoms provide a get helper and an abort signal to the computation callback. These can be used to compute promises.
import { computed } from 'atomous'
const $userId = computed((get, signal) => {
return fetchJson('/api/users/current', { signal })
})
const $userPosts = computed(async (get, signal) => {
const userId = await get($userId)
const posts = await fetchJson(`/api/users/${userId}`, { signal })
return posts
})Computed atoms support setting their value explicitly. It will be replaced with the new computed value when triggered.
import { atom, computed } from 'atomous'
const $firstName = atom('John')
const $lastName = atom('Doe')
const $fullName = computed(() => `${$firstName.get()} ${$lastName.get()}`)
// Set value explicitly
$fullName.set('Jane Smith')
// Update dependencies to update the computed value
$firstName.set('William')
$lastName.set('Miller')
// ... or mark the computed atom to recompute its value
$fullName.reset()Deprecated since
0.2.0
This atom is similar to computed atom but is designed for asynchronous data loading.
- Function:
asyncAtom(<source>, <load>, [options]) - Class:
new AsyncAtom(<source>, <load>, [options])
The
sourceparameter can be either an atom or a plain value.
Async atoms also support setting their value explicitly. See Computed atom for example.
import { asyncAtom, atom } from 'atomous'
const $postId = atom(123)
const $postData = asyncAtom($postId, async (postId, signal) => {
const result = await fetch(`/posts/${postId}`, { signal })
const json = await result.json()
return json
})
// State example
$postData.get() // { status: "success", data: { /* data */ } }
// Abort current loading
$postData.abort()Added in
0.2.0
Simple atom that returns a descriptive state of the source promise. Accepts an atom that returns a promise.
import { computed, loadable } from 'atomous'
const $userId = computed((get, signal) => {
return fetchJson('/api/users/current', { signal })
})
const $userPosts = computed(async (get, signal) => {
const userId = await get($userId)
const posts = await fetchJson(`/api/users/${userId}`, { signal })
return posts
})
const $userPostsState = loadable($userPosts)
$userPostsState.subscribe((state) => {
switch (state.status) {
case 'loading': return console.log('Loading ...')
case 'error': return console.error(state.error)
case 'success': return console.log('Posts loaded.', state.value)
}
})All atoms accept an options object. It can be used to provide custom compare function or a cleanup callback.
import { atom } from 'atomous'
const $atom = atom({
value: 'some value',
dispose() {
console.log('Disposed')
}
}, {
// Used to check if two values equal
compare(a, b) {
return a.value === b.value
},
// Called with current value when new value is set either explicitly or automatically
cleanup(obj) {
obj.dispose()
}
})Atom setters can be called in a batch to reduce recomputations.
batch(<callback>)
import { atom, batch, computed } from 'atomous'
const $firstName = atom('John')
const $lastName = atom('Doe')
const $fullName = computed(() => `${$firstName.get()} ${$lastName.get()}`)
// $fullName updates only once with a value equal to 'William Miller'
batch(() => {
$firstName.set('Jane')
$lastName.set('Smith')
$firstName.set('William')
$lastName.set('Miller')
})Batch has an asynchronous version that supports calling async functions.
import { asyncBatch, atom, computed } from 'atomous'
const $firstName = atom('John')
const $lastName = atom('Doe')
const $fullName = computed(() => `${$firstName.get()} ${$lastName.get()}`)
// $fullName is updated after exiting the batch callback
asyncBatch(async () => {
$firstName.set('Jane')
await waitForSomething()
$lastName.set('Smith')
})You can batch your own functions like this:
import { AtomBatch, batch } from 'atomous'
// Batched callbacks are called after exiting the batch callback
batch(() => {
AtomBatch.addOrRun(callback1)
AtomBatch.addOrRun(callback2)
})
// Batches can be nested. All callbacks will be called after exiting all batch callbacks
batch(() => {
AtomBatch.addOrRun(callback1)
batch(() => {
AtomBatch.addOrRun(nested1)
AtomBatch.addOrRun(nested2)
})
AtomBatch.addOrRun(callback2)
})
// Running the method outside a batch will run provided callback immediately
AtomBatch.addOrRun(callback)