@exodus/atoms

中文文档
10.3.5发布于 2026/10/10UNLICENSEDTypeScriptES Module近 30 天下载 3.5万

Abstraction for encapsulating a piece of data behind a simple unified interface: get, set, observe

原始使用文档
文档目录12 节
  1. Install
  2. What is an atom?
  3. Data sources
  4. Troubleshooting
  5. Usage
  6. Enhancers
  7. compute({ atom, selector }): ReadonlyAtom
  8. combine({ [key]: Atom }): ReadonlyAtom
  9. dedupe(atom)
  10. withSerialization({ atom, serialize, deserialize })
  11. difference(atom)
  12. filter(atom, predicate)

@exodus/atoms

Install

    pnpm add @exodus/atoms

What is an atom?

An atom is a data source wrapper that exposes a single piece of data through 3 different methods:

  • get(): read data
  • set(newValue): sets the atoms value, blocking until all observers have resolved.
  • observe(async (data) => {}): observes data changes. Will be called initially with current data value. Observers are awaited in series.
  • reset(): clear the stored value. The next get() call will return the default value* and observers will be called with the default value.

Data sources

This library provides helpers for creating atoms from multiple data sources we use in our apps.

getsetobserve
Memory✅✅✅
Storage✅✅✅
Keystore✅🟡 *✅
Event emitter✅❌✅

* A keystore atom needs a special isSoleWriter param to allow write access.

See also:

Note: this library originally hosted a bunch of media-specific factories, which have since been moved out, like the two above. The above will likely follow suit, and this library will only implement the common media-agnostic atom behaviors.

Troubleshooting

Theoretically all atoms should behave similarly. In practice, there are a few currently inconsistent behaviors, which we aim to fix in the future, particularly around memory atoms and atoms created from an event emitter:

  • Memory atoms hang on get() if no defaultValue is provided.
  • Memory and Event Emitter atom observers are non-blocking, i.e. memoryAtom.set() is fire-and-forget

Usage

import { EventEmitter } from 'events'
import {
  createInMemoryAtom,
  createStorageAtomFactory,
  fromEventEmitter,
  createKeystoreAtom,
} from '@exodus/atoms'

// In memory atoms
const availableAssetNamesAtom = createInMemoryAtom({
  defaultValue: {},
})

// Storage atoms
const storageAtomFactory = createStorageAtomFactory({ storage })

const acceptedTermsAtom = storageAtomFactory({
  key: 'acceptedTerms',
  defaultValue: false,
  isSoleWriter: true,
})

// Event emitter
const geolocation = new EventEmitter()
const geolocationAtom = fromEventEmitter({
  emitter: geolocation,
  event: 'geolocation',
  get: async () =>
    new Promise((resolve, reject) => navigator.geolocation.getCurrentPosition(resolve, reject)),
})

navigator.geolocation.watchPosition((position) => {
  geolocation.emit('geolocation', position)
})

// keystore
const keystoreAtom = createKeystoreAtom({
  keystore, // see @exodus/keystore-mobile
  config: {
    key: 'my-secret',
    defaultValue, // optional
    isSoleWriter, // if you plan to call set() on this atom instance
  },
})

Enhancers

To compute derived values, combine multiple atoms into, and perform other useful derivations, there are a bunch of enhancers available. Below is a non-exhaustive list, so check out ./src/enhancers for more.

compute({ atom, selector }): ReadonlyAtom

Computes an atom from another by applying a selector function to the observed data source.

Example:

import { createInMemoryAtom, compute } from '@exodus/atoms'

const yearAtom = createInMemoryAtom({ defaultValue: 2025 })

const isDoorsOfStoneOutYetAtom = compute({
  atom: yearAtom,
  selector: (year) => year > 2040,
})

isDoorsOfStoneOutYetAtom.observe(console.log) // false

combine({ [key]: Atom }): ReadonlyAtom

Combines multiple atoms into one:

  • combinedAtom.observe: fires for the first time when all atoms have emitted a value.
  • combinedAtom.get: resolves to an object with the values of all atoms as keyed in the input.

Example:

import { createInMemoryAtom, combine } from '@exodus/atoms'

const nameAtom = createInMemoryAtom()
const ageAtom = createInMemoryAtom()
const userAtom = combine({
  name: nameAtom,
  age: ageAtom,
})

userAtom.observe(console.log) // hangs until both name and age are set
nameAtom.set('Voldemort')
ageAtom.set(25)
// userAtom atom fires with { name: 'Voldemort', age: 25 }

dedupe(atom)

By default, atoms perform a shallow equality check to determine if a newly written value differs from the current one, and avoid notifying observers if it doesn't. If you want a deep equality check, use dedupe. (Even better, don't write deeply equal objects to that atom in the first place, and don't use dedupe!)

Example:

import { createInMemoryAtom, dedupe } from '@exodus/atoms'

const userAtom = createInMemoryAtom({ defaultValue: { name: 'Voldemort', age: 25 } })
const dedupedUserAtom = dedupe(userAtom)
userAtom.set({ name: 'Voldemort', age: 25 }) // `userAtom` observers are notified
dedupedUserAtom.set({ name: 'Voldemort', age: 25 }) // `dedupedUserAtom` observers are NOT notified

withSerialization({ atom, serialize, deserialize })

If you're storing data in an atom that needs to (de)serialize it, e.g. a storage atom, and the data doesn't survive a roundtrip through JSON.stringify / JSON.parse, use withSerialization to provide custom serialization.

Example:

import BJSON from 'buffer-json'
import { createInMemoryAtom, withSerialization } from '@exodus/atoms'

const rawPublicKeysAtom = createInMemoryAtom()
const publicKeysAtom = withSerialization({
  atom: rawPublicKeysAtom,
  serialize: BJSON.stringify,
  deserialize: BJSON.parse,
})

publicKeysAtom.set({
  bitcoin: Buffer.from([...]),
  ethereum: Buffer.from([...]),
})

difference(atom)

If you want to get both the current and previous value emitted by an atom, use difference.

Example:

import { createInMemoryAtom, difference } from '@exodus/atoms'

const nameAtom = createInMemoryAtom({ defaultValue: 'Tom' })
const nameChangeAtom = difference(nameAtom)
nameChangeAtom.observe(console.log)
nameAtom.set('Voldemort')
// nameChangeAtom emits
// { previous: 'Tom', current: 'Voldemort' }

filter(atom, predicate)

If you're only interested in a subset of values an atom emits, use filter:

Example:

import { createInMemoryAtom, filter } from '@exodus/atoms'

const nameAtom = createInMemoryAtom({ defaultValue: 'Tom' })
const unusualNameAtom = filter(nameAtom, (name) => !['Tom', 'Dick', 'Harry'].includes(name))
unusualNameAtom.observe(console.log)
nameAtom.set('Dick') // unusualNameAtom doesn't emit
nameAtom.set('Harry') // unusualNameAtom doesn't emit
nameAtom.set('Voldemort') // unusualNameAtom emits 'Voldemort'

版本与发布记录

@exodus/atoms 共发布 84 个版本,首次发布于 2022/07/26;过去 12 个月发布了 10 个版本。

版本发布日期距今
10.3.5latest2026/10/101 个月前
10.3.42026/04/137 个月前
10.3.32026/04/077 个月前
10.3.22026/04/067 个月前
10.3.12026/04/057 个月前
10.3.02026/04/037 个月前

关于 @exodus/atoms 的常见问题

@exodus/atoms 是什么?

Abstraction for encapsulating a piece of data behind a simple unified interface: get, set, observe

如何安装 @exodus/atoms?

在项目目录运行 npm install @exodus/atoms,或使用 pnpm add / yarn add / bun add。

@exodus/atoms 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@exodus/atoms 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@exodus/atoms 可以商用吗?

@exodus/atoms 使用 UNLICENSED 许可。作者声明为 UNLICENSED,即不授予公开使用许可(与公共领域的 Unlicense 不同)。使用前需获得作者授权。以上为概括,不构成法律意见,请以许可原文为准。

@exodus/atoms 的下载量有多少?

根据 npm 官方下载统计,@exodus/atoms 在 2026/10/14 – 2026/11/12 这 30 天内被下载 35,420 次,日均约 1,181 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@exodus/atoms 还在维护吗?

最新版本 10.3.5 发布于 2026/10/10(1 个月前);至今共发布 84 个版本,其中过去 12 个月发布了 10 个;首次发布于 2022/07/26。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。