Important: This documentation covers Yarn 1 (Classic).
For Yarn 2+ docs and migration guide, see yarnpkg.com.

Package detail

nucleux

martyroque174ISC1.3.0TypeScript support: included

Simple, atomic hub for all your React application's state management needs. No providers, no boilerplate, just state that works.

react, state-management, atomic, hooks, typescript, redux-alternative, zustand-alternative, no-providers, boilerplate-free, reactive, persistence, react-native, dependency-injection, framework-agnostic, store

readme

Simple, atomic hub for all your React application's state management needs.

Current npm package version.


Why Nucleux?

  • Zero boilerplate - Write less, do more
  • No providers - Use state anywhere without wrapping components
  • Atomic updates - Only subscribed components re-render
  • Framework agnostic - Works with or without React

Installation

npm install nucleux

Quick Start

Create a store with atomic state:

import { Store } from 'nucleux';

class CounterStore extends Store {
  count = this.atom(0);

  increment() {
    this.count.value += 1;
  }
}

Use it in React components:

import { useStore, useValue } from 'nucleux';

function Counter() {
  const store = useStore(CounterStore);
  const count = useValue(store.count);

  return <button onClick={store.increment}>Count: {count}</button>;
}

That's it! No providers, no reducers, no dispatch.

Core Concepts

Atoms

Atoms are reactive pieces of state. When you change an atom's value, only components subscribed to that specific atom will re-render.

class TodoStore extends Store {
  todos = this.atom([]);
  filter = this.atom('all');

  addTodo(text) {
    this.todos.value = [
      ...this.todos.value,
      { id: Date.now(), text, done: false },
    ];
  }
}

Three Ways to Use State

1. useStore - Get store methods

const todoStore = useStore(TodoStore);
// Access: todoStore.addTodo(), todoStore.toggleTodo(), etc.

2. useValue - Subscribe to specific atoms

const todos = useValue(todoStore.todos);
// Or directly: const todos = useValue(TodoStore, 'todos');

3. useNucleux - Get everything at once

const todo = useNucleux(TodoStore);
// Access: todo.todos, todo.filter, todo.addTodo(), etc.

Advanced Features

Memoization

Further improve atom updates and component re-renders:

class AppStore extends Store {
  // Shallow memoization (default) - compares by reference
  count = this.atom(0);

  // Deep memoization - compares object content
  user = this.atom(
    { name: 'John', age: 30 },
    { memoization: { type: 'deep' } },
  );

  // Custom memoization - use your own comparison logic
  product = this.atom(
    { name: 'Laptop', price: 999.99, discount: 10 },
    {
      memoization: {
        type: 'custom',
        compare: (a, b) => a.name === b.name && a.price === b.price,
      },
    },
  );
}

Works with derived atoms too:

class TodoStore extends Store {
  todos = this.atom([]);
  filter = this.atom('all');

  // Further improve updates when filtered result content is the same
  filteredTodos = this.deriveAtom(
    [this.todos, this.filter],
    (todos, filter) => todos.filter((t) => t.status === filter),
    { type: 'deep' },
  );
}

Persistence

Save state automatically:

class UserStore extends Store {
  // Simple persistence
  theme = this.atom('dark', { persistence: { persistKey: 'theme' } });

  // With custom storage
  profile = this.atom(
    { name: '', email: '' },
    {
      persistence: {
        persistKey: 'profile',
        storage: AsyncStorage,
      },
    },
  );
}

Derived State

Compute values from multiple atoms:

class TodoStore extends Store {
  todos = this.atom([]);
  filter = this.atom('all');

  filteredTodos = this.deriveAtom(
    [this.todos, this.filter],
    (todos, filter) => {
      if (filter === 'done') return todos.filter((t) => t.done);
      if (filter === 'pending') return todos.filter((t) => !t.done);
      return todos;
    },
  );
}

Store Dependencies

Inject other stores:

class NotificationStore extends Store {
  userStore = this.inject(UserStore);
  notifications = this.atom([]);

  constructor() {
    super();
    this.watchAtom(this.userStore.currentUser, (user) => {
      if (user) this.loadNotifications(user.id);
    });
  }
}

Custom Storage (React Native)

Set storage for entire store:

import AsyncStorage from '@react-native-async-storage/async-storage';

class AppStore extends Store {
  storage = AsyncStorage;

  settings = this.atom(
    { notifications: true },
    { persistence: { persistKey: 'settings' } },
  );
}

Or per atom:

class AppStore extends Store {
  settings = this.atom(
    { notifications: true },
    {
      persistence: {
        persistKey: 'settings',
        storage: AsyncStorage,
      },
    },
  );
}

Debugging

Track atom changes during development:

class TodoStore extends Store {
  todos = this.atom([]);
  filter = this.atom('all');

  constructor() {
    super();

    // Enable debugging in development
    if (process.env.NODE_ENV === 'development') {
      this.enableDebug();
    }
  }

  addTodo(text) {
    this.todos.value = [...this.todos.value, { id: Date.now(), text }];
  }
}

React Native Setup

Install the polyfill and import it before Nucleux:

npm install react-native-get-random-values
// App.js - Import this first!
import 'react-native-get-random-values';
import { useStore, useValue } from 'nucleux';

Complete Example

import React from 'react';
import { Store, useNucleux } from 'nucleux';

class TodoStore extends Store {
  todos = this.atom([]);

  addTodo(text) {
    const newTodo = { id: Date.now(), text, done: false };
    this.todos.value = [...this.todos.value, newTodo];
  }

  toggleTodo(id) {
    this.todos.value = this.todos.value.map((todo) =>
      todo.id === id ? { ...todo, done: !todo.done } : todo,
    );
  }
}

function TodoApp() {
  const { todos, addTodo, toggleTodo } = useNucleux(TodoStore);
  const [input, setInput] = React.useState('');

  const handleAdd = () => {
    if (input.trim()) {
      addTodo(input.trim());
      setInput('');
    }
  };

  return (
    <div>
      <div>
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          placeholder="Add todo..."
        />
        <button onClick={handleAdd}>Add</button>
      </div>

      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            <label>
              <input
                type="checkbox"
                checked={todo.done}
                onChange={() => toggleTodo(todo.id)}
              />
              {todo.text}
            </label>
          </li>
        ))}
      </ul>
    </div>
  );
}

Try It Live

View on CodeSandbox

API Reference

Store Methods

this.atom(initialValue, options?)

Create reactive state.

Options:

  • persistence - Auto-save to storage
    • persistKey: string - Storage key
    • storage?: SupportedStorage - Custom storage (defaults to localStorage)
  • memoization - Control when updates trigger
    • type: 'shallow' | 'deep' | 'custom' - Comparison strategy
    • compare?: (a, b) => boolean - Custom comparator (for type: 'custom')
// Simple atom
count = this.atom(0);

// With persistence
theme = this.atom('dark', {
  persistence: { persistKey: 'app-theme' },
});

// With memoization
user = this.atom(
  { name: 'John' },
  {
    memoization: { type: 'deep' },
  },
);

// Combined options
profile = this.atom(
  { name: '', email: '' },
  {
    persistence: { persistKey: 'profile' },
    memoization: { type: 'deep' },
  },
);

this.deriveAtom(atoms[], computeFn, memoization?)

Create computed state that updates when source atoms change.

filteredTodos = this.deriveAtom(
  [this.todos, this.filter],
  (todos, filter) => todos.filter((t) => t.status === filter),
  { type: 'deep' }, // Optional memoization
);

this.inject(StoreClass)

Inject another store as a dependency.

userStore = this.inject(UserStore);

this.watchAtom(atom, callback, immediate?)

Watch atom changes within the store.

constructor() {
  super();
  this.watchAtom(this.user, (newUser, prevUser) => {
    console.log('User changed:', newUser);
  });
}

this.enableDebug()

Enable console logging for all atom changes in the store.

constructor() {
  super();

  // Enable debugging in development
  if (process.env.NODE_ENV === 'development') {
    this.enableDebug();
  }
}

React Hooks

useStore(StoreClass)

Get store instance with methods.

const todoStore = useStore(TodoStore);
todoStore.addTodo('New task');

useValue(atom) or useValue(StoreClass, 'atomKey')

Subscribe to atom value.

// Direct atom access
const todos = useValue(todoStore.todos);

// Store + key access
const todos = useValue(TodoStore, 'todos');

useNucleux(StoreClass)

Get all methods and atom values.

const { todos, addTodo, removeTodo } = useNucleux(TodoStore);

Requirements: Node ≥14, React ≥16.9.0 (optional)

Author

Marty Roque

License

ISC License

Copyright © 2025 Marty Roque.

changelog

Change Log

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog and this project adheres to Semantic Versioning.

[Unreleased]

[1.3.0] - 2025-06-22

Added

  • Added social media meta tags to docs for improved link previews.
  • Added favicon to the docs.
  • Added docs analytics.
  • Added layered memoization support to atoms and derived atoms with shallow (default), deep, and custom comparator options to further improve atom updates and component re-renders.
  • Added enableDebug() to Store class for console-based debugging.
  • Enhanced atom subscribe method to support previous value in callbacks.
  • Console logging for atom changes with previous/current values and timestamps.
  • Added initialValue property to atom interfaces for accessing the original atom value.
  • Added isServerSnapshot parameter to getStoreProxy utility function for SSR support.

Changed

  • Updated logo view-box and reduce logo size in docs.
  • Updated atom subscribe callback signature to optionally include previous value.
  • Improved atom subscriber error handling.
  • Updated all hooks (useStore, useValue, useNucleux) to provide proper server-side snapshots for SSR.
  • Moved use-sync-external-store as a peer dependency.

Fixed

  • Fixed NextJS SSR compatibility by adding getServerSnapshot parameter to useSyncExternalStore hooks.
  • Fixed SSR hydration mismatches for persisted atoms by using initial values on server-side rendering.

[1.2.2] - 2025-05-31

Added

  • Added immediate callback option to Atom subscribe method.

Changed

  • Improved the README content
  • Updated description and keywords

[1.2.1] - 2025-05-17

Added

  • Documentation

Fixed

  • Fix and improve useNucleux hook

[1.2.0] - 2025-05-10

Added

  • New useNucleux hook for accessing all store methods and atom values with a single hook
  • Overloaded useValue hook to support direct atom access via store constructor and key name
  • Comprehensive JSDoc documentation for all hooks to improve developer experience

Changed

  • Improved type safety for atom access with StoreProxyAtoms type constraints
  • Enhanced error handling for invalid atom access

Fixed

  • Fixed potential memory leaks in subscription cleanup logic
  • Improved type inference for nested atom values

[1.1.1] - 2025-03-31

Changed

  • Update the readme logo size

[1.1.0] - 2025-03-31

Changed

  • Renamed library to Nucleux.
  • Improved method naming:
    • value() → atom()
    • subscribeToValue() → watchAtom()
    • computedValue() → deriveAtom()
  • Updated README with consistent terminology and improved examples.
  • Enhanced documentation clarity and formatting.
  • Kept hook names useStore and useValue for community familiarity.

[1.0.0] - 2025-03-29

Changed

  • Migrate from apps-digest for simplicity and improvements.
  • Simplify all interfaces, classes and helpers names.

[0.4.0] - 2023-04-13

Added

  • Add value getters and setters for a simplified usage.

Changed

  • Shorten hook names for a common/known pattern.

[0.3.0] - 2023-04-13

Added

  • Add support for React Native.
  • Add support for custom persistency storage.

[0.2.0] - 2023-02-17

Changed

  • Abstract the store definition generation to the store container by adding a getStoreDefinition method to the store prototype, making it simpler to just export the stores instead of manually generating the store definition before exporting or using the store.

Removed

  • Remove generateStoreDefinition from library exports.

[0.1.0] - 2022-12-09

Changed

  • Update custom hooks to consume useSyncExternalStore.
  • Generate unique store IDs upon store generation instead of the static getStoreName method, which could be prone to issues on refactors.

[0.0.5] - 2022-10-21

Changed

  • Fixed README examples and added a live demo.
  • Removed the apps_digest_flow.jpeg image to reduce package size.

[0.0.4] - 2022-09-04

Added

  • Added computed values.

Changed

[0.0.3] - 2022-08-18

Added

  • Added persistency feature to store values.

[0.0.2] - 2022-06-04

Fixed

  • Fixed the store collision issues in production due to functions becoming one-letter when minified. Added getStoreName static method as part of the store constructable to enforce store name definition.

[0.0.1] - 2022-05-25

Added

  • Initial release.