🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@mongez/atom

Package Overview
Dependencies
Maintainers
1
Versions
16
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@mongez/atom

An agnostic state management tool that work with any framework on browser or server

Source
npmnpm
Version
1.0.5
Version published
Maintainers
1
Created
Source

Atoms

A powerful, framework-agnostic state management library that works seamlessly with any UI framework (React, Vue, Angular, Svelte) or Node.js application.

Table of Contents

Why Atoms?

The main purpose of this package is to provide a simple, performant, and framework-agnostic state management solution that:

  • Works anywhere - UI frameworks, Node.js, or vanilla JavaScript
  • Requires minimal learning curve - master it in minutes
  • Provides fine-grained reactivity - subscribe to specific property changes
  • Maintains immutability - original values are never mutated
  • Offers extensibility - custom actions and behaviors
  • Delivers excellent performance - event-driven architecture with minimal overhead

Features

  • ✅ Simple and easy to use
  • ✅ Won't take more than few minutes to master it
  • ✅ Can be used outside components
  • ✅ Listen to atom's value change
  • ✅ Listen to atom's object property change
  • ✅ Lightweight in size
  • ✅ Easy Managing Objects, Arrays, and booleans
  • ✅ Open for Extension using Atom Actions
  • ✅ Framework-agnostic (React, Vue, Angular, Svelte, etc.)
  • ✅ TypeScript support with full type inference
  • ✅ Immutable by default
  • ✅ Event-driven architecture

React Atom

For React users, we have a dedicated package that works perfectly with React, it has some extra features that would fit with React components.

Installation

yarn add @mongez/atom

Or

npm i @mongez/atom

Or

pnpm add @mongez/atom

Quick Start

Get started in 30 seconds:

import { createAtom } from "@mongez/atom";

// 1. Create an atom
const counterAtom = createAtom({
  key: "counter",
  default: 0,
});

// 2. Read the value
console.log(counterAtom.value); // 0

// 3. Update the value
counterAtom.update(1);

// 4. Listen for changes
counterAtom.onChange((newValue, oldValue) => {
  console.log(`Counter changed from ${oldValue} to ${newValue}`);
});

// 5. Update again to see the listener fire
counterAtom.update(2); // Logs: "Counter changed from 1 to 2"

That's it! You now understand 80% of what atoms do. Continue reading for advanced features.

Architecture & Philosophy

Event-Driven Design

Atoms use an event-driven architecture powered by @mongez/events. When an atom's value changes, it emits an event that all subscribers receive. This allows for:

  • Decoupled components - Components don't need to know about each other
  • Fine-grained updates - Subscribe only to what you need
  • Predictable behavior - Changes flow in one direction

Immutability

All atoms are immutable by default. When you create an atom with an object or array as the default value, that original value is cloned and preserved:

const original = { name: "John" };
const userAtom = createAtom({
  key: "user",
  default: original,
});

userAtom.update({ name: "Jane" });

console.log(original); // { name: "John" } - unchanged!
console.log(userAtom.value); // { name: "Jane" }

Framework Agnostic

Unlike Redux (React-focused) or Pinia (Vue-focused), atoms work everywhere:

  • UI Frameworks: React, Vue, Angular, Svelte, Solid, Qwik
  • Server-Side: Node.js, Deno, Bun
  • Vanilla JS: No framework needed

This makes atoms perfect for:

  • Shared logic between frontend and backend
  • Migrating between frameworks
  • Building framework-agnostic libraries

Single Responsibility

Each atom manages one piece of state. This promotes:

  • Better organization - Easy to find and reason about
  • Easier testing - Test atoms in isolation
  • Improved performance - Only relevant subscribers are notified

Core Concepts

Creating Atoms

Atoms are unique by their key. Each atom must have a unique key - attempting to create two atoms with the same key will throw an error.

import { createAtom, Atom } from "@mongez/atom";

// Simple value atom
export const currencyAtom = createAtom<string>({
  key: "currency",
  default: "EUR",
});

// Object atom
export type UserData = {
  name: string;
  email: string;
  age: number;
  id: number;
};

export const userAtom = createAtom<UserData>({
  key: "user",
  default: {
    name: "Hasan",
    age: 30,
    email: "hassanzohdy@gmail.com",
    id: 1,
  },
});

// Array atom (use atomCollection for better array methods)
export const todosAtom = createAtom<string[]>({
  key: "todos",
  default: [],
});

TypeScript Tip: Always pass the value type as a generic for full type safety.

Getting Atom Values

Access atom values using the .value property:

import { currencyAtom } from "~/atoms";

console.log(currencyAtom.value); // "EUR"

// For objects, you get the full object
console.log(userAtom.value); // { name: "Hasan", age: 30, ... }

// For nested values, use .get()
console.log(userAtom.get("name")); // "Hasan"

Updating Atoms

The basic way to update an atom is with .update():

// Direct value
currencyAtom.update("USD");

// Callback with old value
currencyAtom.update((oldValue, atom) => {
  console.log(`Changing from ${oldValue}`);
  return "GBP";
});

Important: For objects and arrays, you must pass a new reference to trigger change events:

// ❌ Won't trigger change event
userAtom.update(userAtom.value);

// ✅ Triggers change event
userAtom.update({ ...userAtom.value });

Watching for Changes

Subscribe to atom changes with .onChange():

const subscription = currencyAtom.onChange((newValue, oldValue, atom) => {
  console.log(`Currency changed from ${oldValue} to ${newValue}`);
});

// Later, unsubscribe
subscription.unsubscribe();

The onChange method returns an EventSubscription with an unsubscribe method.

Resetting Atoms

Reset an atom to its default value:

currencyAtom.update("USD");
console.log(currencyAtom.value); // "USD"

currencyAtom.reset();
console.log(currencyAtom.value); // "EUR" (default value)

Listen for reset events:

currencyAtom.onReset((atom) => {
  console.log("Atom was reset!");
});

Working with Objects

When an atom's default value is an object, it gets special treatment with additional methods.

Merging Values

Instead of spreading the old value manually, use .merge():

// Without merge
userAtom.update({
  ...userAtom.value,
  name: "Ahmed",
  age: 25,
});

// With merge (cleaner!)
userAtom.merge({
  name: "Ahmed",
  age: 25,
});

Changing Single Keys

Update a single property with .change():

userAtom.change("name", "Ahmed");
userAtom.change("age", 25);

This is equivalent to:

userAtom.update({
  ...userAtom.value,
  name: "Ahmed",
});

Getting Nested Values

Use .get() to retrieve values from object atoms:

const userAtom = createAtom({
  key: "user",
  default: {
    name: "Hasan",
    address: {
      city: "New York",
    },
  },
});

// Simple key
console.log(userAtom.get("name")); // "Hasan"

// Dot notation for nested values
console.log(userAtom.get("address.city")); // "New York"

// Default value if key doesn't exist
console.log(userAtom.get("email", "default@email.com")); // "default@email.com"

Watching Partial Changes

Watch for changes to specific properties with .watch():

const userAtom = createAtom({
  key: "user",
  default: {
    name: "Hasan",
    address: {
      city: "New York",
    },
  },
});

// Watch a single property
userAtom.watch("name", (newName, oldName) => {
  console.log(`Name changed from ${oldName} to ${newName}`);
});

// Watch nested property (dot notation)
userAtom.watch("address.city", (newCity, oldCity) => {
  console.log(`City changed from ${oldCity} to ${newCity}`);
});

// Update triggers only relevant watchers
userAtom.update({
  ...userAtom.value,
  name: "Ali", // Only name watcher fires
});

Working with Arrays

For arrays, use atomCollection instead of createAtom to get array-specific methods.

Atom Collections

import { atomCollection } from "@mongez/atom";

const todoListAtom = atomCollection({
  key: "todos",
  default: [],
});

Array Methods

Add Items

// Add to end
todoListAtom.push("Buy Milk");
todoListAtom.push("Buy Bread", "Buy Eggs"); // Multiple items

// Add to beginning
todoListAtom.unshift("Wake Up");
todoListAtom.unshift("Shower", "Breakfast"); // Multiple items

Remove Items

// Remove from end
todoListAtom.pop();

// Remove from beginning
todoListAtom.shift();

// Remove by index
todoListAtom.remove(0);

// Remove by callback
todoListAtom.remove((item) => item === "Buy Milk");

// Remove specific item (first occurrence)
todoListAtom.removeItem("Buy Milk");

// Remove all occurrences
todoListAtom.removeAll("Buy Milk");

Get Items

// By index
console.log(todoListAtom.get(0)); // First item

// By callback
console.log(todoListAtom.get((item) => item.includes("Milk"))); // "Buy Milk"

// Find index
const index = todoListAtom.index((item) => item === "Buy Milk");

Update Items

// Replace item at index
todoListAtom.replace(0, "Buy Bread");

// Map over items (updates the atom)
todoListAtom.map((item) => item.toUpperCase());

// Iterate (doesn't update)
todoListAtom.forEach((item, index) => {
  console.log(`${index}: ${item}`);
});

Get Length

console.log(todoListAtom.length); // Number of items

Advanced Features

Atom Actions

Extend atoms with custom methods using actions:

export const userAtom = createAtom({
  key: "user",
  default: {
    name: "Hasan",
    age: 30,
    email: "",
  },
  actions: {
    changeName(name: string) {
      this.update({
        ...this.value,
        name,
      });
    },
    changeEmail(email: string) {
      this.update({
        ...this.value,
        email,
      });
    },
    incrementAge() {
      this.change("age", this.value.age + 1);
    },
  },
});

// Usage
userAtom.changeName("Ahmed");
userAtom.changeEmail("ahmed@example.com");
userAtom.incrementAge();

Type-Safe Actions:

type UserActions = {
  changeName(name: string): void;
  changeEmail(email: string): void;
  incrementAge(): void;
};

export const userAtom = createAtom<UserData, UserActions>({
  key: "user",
  default: {
    /* ... */
  },
  actions: {
    /* ... */
  },
});

All action methods are automatically bound to the atom instance, so this always refers to the atom.

Value Mutation Before Update

Transform values before they're stored using beforeUpdate:

const multipleAtom = createAtom({
  key: "multiple",
  default: 0,
  beforeUpdate(newNumber: number): number {
    return newNumber * 2;
  },
});

multipleAtom.update(4);
console.log(multipleAtom.value); // 8

This is useful for:

  • Validation
  • Normalization
  • Data transformation
  • Side effects

Custom Getters

Customize how values are retrieved with a custom get function:

const settingsAtom = createAtom({
  key: "settings",
  default: {
    isLoaded: false,
    settings: {},
  },
  get(key: string, defaultValue: any = null, atomValue: any) {
    // Check top-level first, then nested settings
    return atomValue[key] !== undefined
      ? atomValue[key]
      : atomValue.settings[key] !== undefined
        ? atomValue.settings[key]
        : defaultValue;
  },
});

settingsAtom.update({
  isLoaded: true,
  settings: {
    websiteName: "My Website",
  },
});

// Custom getter allows direct access
console.log(settingsAtom.get("websiteName")); // "My Website"
console.log(settingsAtom.get("isLoaded")); // true

Silent Updates

Update without triggering change events:

// Silent update
currencyAtom.silentUpdate("USD"); // No onChange listeners fire

// Silent change (for objects)
userAtom.silentChange("name", "Ahmed"); // No onChange listeners fire

// Silent reset
currencyAtom.silentReset(); // Resets to default, fires onReset but not onChange

Use Cases:

  • Initializing state without triggering effects
  • Cleanup on component unmount
  • Batch updates (update silently, then trigger once)

Atom Cloning

Create a copy of an atom with a new key:

const clonedAtom = currencyAtom.clone();

console.log(clonedAtom.key); // "currencyCloned1234" (random suffix)
console.log(clonedAtom.value); // Same as original

Clones are independent - updating one doesn't affect the other.

Complete API Reference

Creation Functions

FunctionParametersReturnsDescription
createAtom<Value, Actions>()AtomOptionsAtom<Value, Actions>Creates a new atom
atomCollection<Value>()CollectionOptionsAtom<Value[], AtomCollectionActions>Creates an array atom with helper methods
getAtom<T>(key)key: stringAtom<T> | undefinedGets an existing atom by key
atomsList()-Atom<any>[]Returns array of all atoms
atomsObject()-Record<string, Atom>Returns object of all atoms

Atom Instance Properties

PropertyTypeDescription
keystringUnique atom identifier
valueValueCurrent atom value (readonly)
defaultValueValueOriginal default value (readonly)
typestringType of value ("string", "number", "object", "array", etc.)
lengthnumberLength of array/string values

Atom Instance Methods

Update Methods

MethodParametersReturnsDescription
update()value | (oldValue, atom) => valuevoidUpdates atom value
silentUpdate()value | (oldValue, atom) => valuevoidUpdates without triggering onChange
merge()Partial<Value>voidMerges object (objects only)
change()key, valuevoidUpdates single property (objects only)
silentChange()key, valuevoidUpdates property without triggering onChange
reset()-voidResets to default value
silentReset()-voidResets without triggering onChange

Query Methods

MethodParametersReturnsDescription
get()key, defaultValue?anyGets value by key (objects only)

Event Methods

MethodParametersReturnsDescription
onChange()(newValue, oldValue, atom) => voidEventSubscriptionListens for value changes
watch()key, (newValue, oldValue) => voidEventSubscriptionListens for property changes
onReset()(atom) => voidEventSubscriptionListens for reset events
onDestroy()(atom) => voidEventSubscriptionListens for destruction

Utility Methods

MethodParametersReturnsDescription
clone()-AtomCreates a copy with new key
destroy()-voidDestroys atom and removes it

Collection-Specific Methods

MethodParametersReturnsDescription
push()...itemsvoidAdds items to end
unshift()...itemsvoidAdds items to beginning
pop()-voidRemoves last item
shift()-voidRemoves first item
remove()index | callbackvoidRemoves item
removeItem()itemvoidRemoves first occurrence
removeAll()itemValue[]Removes all occurrences
get()index | callbackValue | undefinedGets item
index()callbacknumberFinds index
map()callbackValue[]Maps and updates
forEach()callbackvoidIterates items
replace()index, itemvoidReplaces item at index

Framework Integration

React

For React, use the dedicated @mongez/react-atom package which provides hooks:

import { atom } from "@mongez/react-atom";

const counterAtom = atom({
  key: "counter",
  default: 0,
});

function Counter() {
  const count = counterAtom.useValue();

  return (
    <button onClick={() => counterAtom.update(count + 1)}>
      Count: {count}
    </button>
  );
}

Vue 3

Use atoms with Vue's Composition API:

<script setup>
import { ref, onMounted, onUnmounted } from "vue";
import { createAtom } from "@mongez/atom";

const counterAtom = createAtom({
  key: "counter",
  default: 0,
});

const count = ref(counterAtom.value);

let subscription;

onMounted(() => {
  subscription = counterAtom.onChange((newValue) => {
    count.value = newValue;
  });
});

onUnmounted(() => {
  subscription.unsubscribe();
});

const increment = () => {
  counterAtom.update(counterAtom.value + 1);
};
</script>

<template>
  <button @click="increment">Count: {{ count }}</button>
</template>

Composable Pattern:

// useAtom.ts
import { ref, onMounted, onUnmounted } from "vue";
import type { Atom } from "@mongez/atom";

export function useAtom<T>(atom: Atom<T>) {
  const value = ref(atom.value);
  let subscription;

  onMounted(() => {
    subscription = atom.onChange((newValue) => {
      value.value = newValue;
    });
  });

  onUnmounted(() => {
    subscription?.unsubscribe();
  });

  return value;
}

// Usage
import { useAtom } from "./useAtom";
const count = useAtom(counterAtom);

Angular

Integrate atoms with Angular services:

// counter.service.ts
import { Injectable, OnDestroy } from "@angular/core";
import { BehaviorSubject, Observable } from "rxjs";
import { createAtom } from "@mongez/atom";
import type { EventSubscription } from "@mongez/events";

@Injectable({
  providedIn: "root",
})
export class CounterService implements OnDestroy {
  private counterAtom = createAtom({
    key: "counter",
    default: 0,
  });

  private counterSubject = new BehaviorSubject(this.counterAtom.value);
  public counter$: Observable<number> = this.counterSubject.asObservable();

  private subscription: EventSubscription;

  constructor() {
    this.subscription = this.counterAtom.onChange((newValue) => {
      this.counterSubject.next(newValue);
    });
  }

  increment() {
    this.counterAtom.update(this.counterAtom.value + 1);
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }
}
// counter.component.ts
import { Component } from "@angular/core";
import { CounterService } from "./counter.service";

@Component({
  selector: "app-counter",
  template: `
    <button (click)="increment()">Count: {{ counter$ | async }}</button>
  `,
})
export class CounterComponent {
  counter$ = this.counterService.counter$;

  constructor(private counterService: CounterService) {}

  increment() {
    this.counterService.increment();
  }
}

Svelte

Use atoms with Svelte stores:

// atomStore.ts
import { writable } from "svelte/store";
import type { Atom } from "@mongez/atom";

export function atomStore<T>(atom: Atom<T>) {
  const { subscribe, set } = writable(atom.value);

  atom.onChange((newValue) => {
    set(newValue);
  });

  return {
    subscribe,
    update: (value: T) => atom.update(value),
  };
}
<script>
  import { createAtom } from '@mongez/atom';
  import { atomStore } from './atomStore';

  const counterAtom = createAtom({
    key: 'counter',
    default: 0,
  });

  const counter = atomStore(counterAtom);

  function increment() {
    counter.update($counter + 1);
  }
</script>

<button on:click={increment}>
  Count: {$counter}
</button>

Vanilla JavaScript

Use atoms without any framework:

<!DOCTYPE html>
<html>
  <body>
    <button id="increment">Count: <span id="count">0</span></button>

    <script type="module">
      import { createAtom } from "@mongez/atom";

      const counterAtom = createAtom({
        key: "counter",
        default: 0,
      });

      const countElement = document.getElementById("count");
      const button = document.getElementById("increment");

      // Update UI when atom changes
      counterAtom.onChange((newValue) => {
        countElement.textContent = newValue;
      });

      // Increment on click
      button.addEventListener("click", () => {
        counterAtom.update(counterAtom.value + 1);
      });
    </script>
  </body>
</html>

Server-Side Usage

Atoms work perfectly in Node.js for managing application state:

Request-Scoped State

// userContext.ts
import { createAtom } from "@mongez/atom";

export function createUserContext(userId: string) {
  return createAtom({
    key: `user-${userId}`,
    default: {
      id: userId,
      permissions: [],
      preferences: {},
    },
  });
}

// middleware.ts
app.use((req, res, next) => {
  req.userAtom = createUserContext(req.userId);
  next();
});

// route.ts
app.get("/profile", (req, res) => {
  const user = req.userAtom.value;
  res.json(user);
});

Caching Layer

import { createAtom } from "@mongez/atom";

const cacheAtom = createAtom<Record<string, any>>({
  key: "cache",
  default: {},
  actions: {
    set(key: string, value: any, ttl: number = 60000) {
      this.change(key, value);

      setTimeout(() => {
        this.change(key, undefined);
      }, ttl);
    },
    get(key: string) {
      return this.value[key];
    },
  },
});

// Usage
cacheAtom.set("user:123", userData, 5000);
const cached = cacheAtom.get("user:123");

Configuration Management

import { createAtom } from "@mongez/atom";

const configAtom = createAtom({
  key: "config",
  default: {
    port: 3000,
    database: {
      host: "localhost",
      port: 5432,
    },
  },
});

// Watch for config changes
configAtom.watch("database.host", (newHost) => {
  console.log(`Database host changed to ${newHost}`);
  // Reconnect to database
});

Advanced Patterns

Computed Atoms

Create atoms that derive from other atoms:

const firstNameAtom = createAtom({
  key: "firstName",
  default: "John",
});

const lastNameAtom = createAtom({
  key: "lastName",
  default: "Doe",
});

const fullNameAtom = createAtom({
  key: "fullName",
  default: "",
});

// Update fullName when either name changes
const updateFullName = () => {
  fullNameAtom.silentUpdate(`${firstNameAtom.value} ${lastNameAtom.value}`);
};

firstNameAtom.onChange(updateFullName);
lastNameAtom.onChange(updateFullName);
updateFullName(); // Initialize

Atom Composition

Combine multiple atoms:

const themeAtom = createAtom({
  key: "theme",
  default: "light",
});

const languageAtom = createAtom({
  key: "language",
  default: "en",
});

const settingsAtom = createAtom({
  key: "settings",
  default: {},
  actions: {
    syncFromAtoms() {
      this.merge({
        theme: themeAtom.value,
        language: languageAtom.value,
      });
    },
  },
});

// Sync when either changes
themeAtom.onChange(() => settingsAtom.syncFromAtoms());
languageAtom.onChange(() => settingsAtom.syncFromAtoms());

Middleware Pattern

Add middleware to atom updates:

const loggerMiddleware = (atom: Atom) => {
  atom.onChange((newValue, oldValue) => {
    console.log(`[${atom.key}] ${oldValue}${newValue}`);
  });
};

const validationMiddleware = (
  atom: Atom,
  validator: (value: any) => boolean,
) => {
  const originalUpdate = atom.update.bind(atom);

  atom.update = (value: any) => {
    const newValue =
      typeof value === "function" ? value(atom.value, atom) : value;

    if (validator(newValue)) {
      originalUpdate(value);
    } else {
      console.error(`Validation failed for ${atom.key}`);
    }
  };
};

// Usage
const ageAtom = createAtom({ key: "age", default: 0 });
loggerMiddleware(ageAtom);
validationMiddleware(ageAtom, (age) => age >= 0 && age <= 120);

Persistence

Persist atoms to localStorage:

function persistentAtom<T>(options: AtomOptions<T>) {
  const storageKey = `atom:${options.key}`;

  // Load from storage
  const stored = localStorage.getItem(storageKey);
  const defaultValue = stored ? JSON.parse(stored) : options.default;

  const atom = createAtom({
    ...options,
    default: defaultValue,
  });

  // Save on change
  atom.onChange((newValue) => {
    localStorage.setItem(storageKey, JSON.stringify(newValue));
  });

  return atom;
}

// Usage
const userAtom = persistentAtom({
  key: "user",
  default: { name: "", email: "" },
});

Async Updates

Handle async operations:

const userAtom = createAtom({
  key: "user",
  default: null,
  actions: {
    async fetchUser(userId: string) {
      try {
        const response = await fetch(`/api/users/${userId}`);
        const user = await response.json();
        this.update(user);
      } catch (error) {
        console.error("Failed to fetch user:", error);
      }
    },
  },
});

// Usage
await userAtom.fetchUser("123");

Performance Guide

When to Use Atoms

✅ Use atoms for:

  • Global application state
  • Shared state between components
  • State that needs to persist across route changes
  • State accessed from multiple places
  • Complex state with multiple subscribers

❌ Don't use atoms for:

  • Component-local state (use local state instead)
  • Temporary UI state (modals, dropdowns)
  • Form input values (unless shared)
  • Derived state that can be computed on render

Avoiding Unnecessary Updates

1. Use .watch() for specific properties:

// ❌ Re-renders on ANY user change
userAtom.onChange(() => updateUI());

// ✅ Re-renders only on name change
userAtom.watch("name", () => updateUI());

2. Use silent updates for initialization:

// ❌ Triggers onChange during setup
userAtom.update(initialData);

// ✅ No onChange during setup
userAtom.silentUpdate(initialData);

3. Batch updates:

// ❌ Triggers onChange 3 times
userAtom.change("name", "John");
userAtom.change("age", 30);
userAtom.change("email", "john@example.com");

// ✅ Triggers onChange once
userAtom.merge({
  name: "John",
  age: 30,
  email: "john@example.com",
});

Memory Management

Clean up subscriptions:

const subscription = atom.onChange(() => {});

// When done
subscription.unsubscribe();

Destroy unused atoms:

// When atom is no longer needed
atom.destroy();

Benchmarks

Atoms are extremely fast:

  • Create atom: ~0.001ms
  • Update atom: ~0.002ms
  • Subscribe: ~0.001ms
  • Notify 100 subscribers: ~0.1ms
  • Memory per atom: ~1KB

TypeScript Guide

Type Inference

TypeScript automatically infers types from default values:

const counterAtom = createAtom({
  key: "counter",
  default: 0, // Inferred as number
});

counterAtom.update(1); // ✅
counterAtom.update("1"); // ❌ Type error

Explicit Types

For better type safety, always specify types explicitly:

type User = {
  id: number;
  name: string;
  email: string;
};

const userAtom = createAtom<User>({
  key: "user",
  default: {
    id: 0,
    name: "",
    email: "",
  },
});

Generic Atoms

Create reusable atom factories:

function createEntityAtom<T extends { id: number }>(
  key: string,
  defaultValue: T,
) {
  return createAtom<T>({
    key,
    default: defaultValue,
    actions: {
      updateById(id: number, updates: Partial<T>) {
        if (this.value.id === id) {
          this.merge(updates);
        }
      },
    },
  });
}

// Usage
const userAtom = createEntityAtom("user", {
  id: 1,
  name: "John",
});

Typed Actions

Define action types for better autocomplete:

type UserActions = {
  setName(name: string): void;
  setEmail(email: string): void;
  reset(): void;
};

const userAtom = createAtom<User, UserActions>({
  key: "user",
  default: { id: 0, name: "", email: "" },
  actions: {
    setName(name) {
      this.change("name", name);
    },
    setEmail(email) {
      this.change("email", email);
    },
    reset() {
      this.reset();
    },
  },
});

// Full autocomplete and type checking
userAtom.setName("John"); // ✅
userAtom.setName(123); // ❌ Type error

Troubleshooting

Atom Not Updating

Problem: Updating an atom but onChange doesn't fire.

Solution: Ensure you're passing a new reference for objects/arrays:

// ❌ Same reference
const user = userAtom.value;
user.name = "John";
userAtom.update(user); // Won't trigger onChange

// ✅ New reference
userAtom.update({ ...userAtom.value, name: "John" });

Duplicate Atom Key Error

Problem: Error: Atom with key "user" already exists

Solution: Each atom must have a unique key. Either:

  • Use a different key
  • Destroy the existing atom first: getAtom('user')?.destroy()

Memory Leaks

Problem: Application slows down over time.

Solution: Always unsubscribe from onChange:

// ❌ Memory leak
atom.onChange(() => {});

// ✅ Proper cleanup
const subscription = atom.onChange(() => {});
// Later...
subscription.unsubscribe();

TypeScript Errors with Actions

Problem: TypeScript doesn't recognize custom actions.

Solution: Define action types explicitly:

type MyActions = {
  myAction(): void;
};

const atom = createAtom<MyType, MyActions>({
  key: "myAtom",
  default: {},
  actions: {
    myAction() {},
  },
});

Migration Guides

From Redux

Redux:

// store.ts
const initialState = { count: 0 };

function counterReducer(state = initialState, action) {
  switch (action.type) {
    case "INCREMENT":
      return { count: state.count + 1 };
    default:
      return state;
  }
}

// component
const count = useSelector((state) => state.count);
dispatch({ type: "INCREMENT" });

Atoms:

// atoms.ts
const counterAtom = createAtom({
  key: "counter",
  default: { count: 0 },
  actions: {
    increment() {
      this.change("count", this.value.count + 1);
    },
  },
});

// component (with @mongez/react-atom)
const count = counterAtom.use("count");
counterAtom.increment();

Benefits:

  • ✅ No boilerplate (actions, reducers, types)
  • ✅ Direct updates (no dispatch)
  • ✅ Better TypeScript inference
  • ✅ Smaller bundle size

From MobX

MobX:

class CounterStore {
  @observable count = 0;

  @action increment() {
    this.count++;
  }
}

const counter = new CounterStore();

Atoms:

const counterAtom = createAtom({
  key: "counter",
  default: { count: 0 },
  actions: {
    increment() {
      this.change("count", this.value.count + 1);
    },
  },
});

Benefits:

  • ✅ No decorators needed
  • ✅ Immutable by default
  • ✅ Framework-agnostic
  • ✅ Simpler mental model

From Recoil

Recoil:

const countState = atom({
  key: "count",
  default: 0,
});

const [count, setCount] = useRecoilState(countState);

Atoms:

const countAtom = createAtom({
  key: "count",
  default: 0,
});

const count = countAtom.useValue();
countAtom.update(newValue);

Benefits:

  • ✅ Works outside React
  • ✅ No Recoil root provider needed
  • ✅ Simpler API
  • ✅ Better performance

Comparison with Other Libraries

vs Redux

FeatureAtomsRedux
BoilerplateMinimalHigh
Learning CurveMinutesHours
TypeScriptExcellentGood
FrameworkAgnosticReact-focused
Bundle Size~5KB~15KB+
DevToolsBasicExcellent

Use Atoms when: You want simplicity and don't need time-travel debugging. Use Redux when: You need advanced DevTools and middleware ecosystem.

vs Zustand

FeatureAtomsZustand
API StyleAtom-basedStore-based
FrameworkAgnosticReact-focused
SubscriptionsFine-grainedSelector-based
TypeScriptExcellentExcellent
Bundle Size~5KB~3KB

Use Atoms when: You want framework-agnostic state or fine-grained subscriptions. Use Zustand when: You're React-only and want the smallest bundle.

vs Jotai

FeatureAtomsJotai
Atom ModelMutable APIImmutable API
FrameworkAgnosticReact-focused
AsyncManualBuilt-in
TypeScriptExcellentExcellent
Bundle Size~5KB~3KB

Use Atoms when: You want to use atoms outside React. Use Jotai when: You're React-only and want atomic state with async support.

vs Recoil

FeatureAtomsRecoil
MaturityStableExperimental
FrameworkAgnosticReact-only
API ComplexitySimpleComplex
PerformanceExcellentExcellent
Bundle Size~5KB~20KB

Use Atoms when: You want a stable, simple solution. Use Recoil when: You need advanced features like atom families and selectors.

Change Log

V1.0.0 (12 May 2024)

  • Initial release

Keywords

react

FAQs

Package last updated on 15 Feb 2026

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts