Skip to content

The simplest global state system for React. A tiny reactive state manager with global reach and local clarity. Built for modern React. No stale bugs. No mental gymnastics. Full support for storage persistence, auto-optimization, and more.

Notifications You must be signed in to change notification settings

daltonlet/scope-state

Repository files navigation

Scope State

The simplest global state system for React.

A tiny reactive state manager with global reach and local clarity. Built for modern React. No stale bugs. No mental gymnastics. Full support for storage persistence, auto-optimization, and more.

Built for developers who hate Redux and love clarity.

Why Scope State?

Scope State gives you global reactive state with:

  • Zero reducers, zero contexts
  • Zero boilerplate
  • Zero spreads, zero selectors
  • No need for setState
  • And most importantly — no stale value bugs

Just write:

import { useScope, configure } from 'scope-state';

const $ = configure({
  initialState: {
    counter: {
      count: 0,
    },
    user: { 
      name: 'John', 
      age: 30
    }
  }
});

const CounterComponent = () => {
  
  const count = useScope(() => $.counter.count); // subscribe to only the count

  const handleCountIncrement = () => {
    // You can mutate the state directly
    $.counter.count++;
    // OR use the updater function
    $.counter.$update("count", (count) => count + 1);
  }

  const resetCount = () => {
    // use the $set method to replace the entire object
    $.counter.$set({ count: 0 }); 
    // OR use the $merge method to merge the new properties with the existing ones
    $.counter.$merge({ count: 0 });
    // OR use the $reset method to reset the entire object
    $.counter.$reset();
    // OR literally replace the count with a direct assignment
    $.counter.count = 0;
  }
  
  return (
    <div>
      <h1>{count}</h1>
      <button onClick={handleCountIncrement}>+ Increment</button>
      <button onClick={resetCount}>🔄 Reset</button>
    </div>
  )
}

That's it. It tracks dependencies automatically and re-renders only what changed.

What Makes It Different?

  • Fully reactive — inspired by proxies, not reducers
  • Intuitive reads and writes — no .get() or .set() syntax hell
  • Mutate like it's a regular object or ref — works with objects, arrays, numbers, everything
  • Fine-grained tracking — no wasted renders
  • Built-in debug tools
  • Feels like magic
  • Read and set states independently outside of functional components or custom hooks

Getting Started

Installation

npm install scope-state

Quick Start

// store.ts
import { configure } from 'scope-state';
export const $ = configure({
  initialState: {
    user: { name: 'John', age: 30 }
  }
});
// UserProfile.tsx
import { useScope } from 'scope-state';
import { $ } from './store';

export const UserProfile = () => {
  const name = useScope(() => $.user.name);
  return <h1>{name}</h1>;
}

Basic Usage

import { useScope, configure } from 'scope-state';

// RECOMMENDED: Configure with your initial state
// It's best to configure your initial store in a separate file.
// The usage of the dollar sign ($) is optional; just a way to keep it brief
// and easy to identify.

export const $ = configure({
  initialState: {
    user: { name: 'John', age: 30 },
    todos: [],
    theme: 'dark'
  }
});

// Use in components
import { useScope } from 'scope-state';

export const UserProfileComponent = () => {

  const user = useScope(() => $.user);
  
  return (
    <div>
      <h1>{user.name}</h1>
      <button 
        onClick={() => {
          user.age += 1
        }}
      >
          Age: {user.age}
      </button>
    </div>
  );
}

const TodoList = () => {
  const todos = useScope(() => $.todos);
  
  const addTodo = () => {
    $.todos.push({ id: Date.now(), text: 'New todo', done: false });
  };
  
  return (
    <div>
      {todos.map(todo => (
        <div key={todo.id}>{todo.text}</div>
      ))}
      <button onClick={addTodo}>Add Todo</button>
    </div>
  );
}

📚 API Reference

Core Functions

useScope(selector)

Subscribe to reactive state changes.

// Subscribe to entire object
const user = useScope(() => $.user);

// Subscribe to specific property
const userName = useScope(() => $.user.name);

// Subscribe to computed value
const isAdmin = useScope(() => $.user.role === 'admin');

// Subscribe to array
const todos = useScope(() => $.todos);

configure(options)

Configure Scope State with custom settings.

import { configure, presets } from 'scope-state';

// Use a preset
configure(presets.production());

// Custom configuration
const $ = configure({
  initialState: { /* your state */ },
  monitoring: { enabled: true },
  proxy: { maxDepth: 3 }
});

// Then access any item in your state by scoping it using the main hook:
const restaurants = useScope(() => $.restaurants || []) // optional fallback

Object Methods

All objects in the store have these reactive methods:

$merge(newProps)

Merge new properties without removing existing ones.

$.user.$merge({ name: 'John' }); // Updates only name

$set(newProps)

Replace object with new properties.

$.user.$set({ name: 'John', age: 25 }); // Replaces entire user object

raw()

Get plain, serializable JavaScript object without any function references (the reactivity methods removed).

const plainUser = $.user.raw(); 

This is helpful when you need to serialize the state for storage, API calls, or debugging. Otherwise, it's not necessary.


Array Methods

Arrays have enhanced methods that trigger reactivity:

todos.push({ id: 1, text: 'Buy milk' });  // This will trigger a re-render

todos.splice(0, 1);                       // This will trigger a re-render

$.todos = [/* new array */]               // You can also directly assign a new array to the property in the global state itself ($).

Utility Functions

Create reactive local state (not global).

import { useLocal } from 'scope-state';

function MyComponent() {
  const localState = useLocal({ count: 0 });
  
  return (
    <button onClick={() => localState.count + 1}>
      Count: {localState.count}
    </button>
  );
}

Configuration

Presets

import { configure, presets } from 'scope-state';

// Development: Enhanced debugging
configure(presets.development());

// Production: Optimized performance
configure(presets.production());

// Minimal: Memory-constrained environments
configure(presets.minimal());

// Full-featured: All features enabled
configure(presets.full());

Custom Configuration

configure({
  initialState: {
    // Your app's initial state
  },
  proxy: {
    maxDepth: 5, // How deep to proxy objects
    smartArrayTracking: true, // Optimize array operations
  },
  monitoring: {
    enabled: true, // Enable debug logging
    verboseLogging: false, // Detailed logs
    autoLeakDetection: true, // Detect memory leaks
  },
  persistence: {
    enabled: true, // Enable state persistence
    paths: ['user', 'settings.theme'], // Which paths to persist (leave as undefined to persist all paths)
  }
});

Philosophy

React's core primitives like useState, useReducer, and useContext work well for many use cases.

But when your app grows in complexity…

  • deeply nested objects,
  • shared state across pages,
  • state persistence,
  • or fine-grained reactivity,

suddenly you're spending time wiring reducers, spreading props, memoizing selectors, and debugging re-renders.

Scope State simplifies that.

You write and read state directly, just like a ref or a signal, but with full reactivity, automatic tracking, and global accessibility.

This library was built out of frustration with every other state system:

  • Redux is too bloated
  • Recoil is too verbose
  • Zustand still forces manual updates
  • Legend State is performant (and deserves significant respect) but has a higher learning curve and confusing API. It's simply ahead of its time.

The mental model is simple: write and read directly. Like useRef, but global, reactive, and tracked.

Best Practices

1. Keep Selectors Simple

// ✅ Good
const name = useScope(() => $.user.name);

// ❌ Avoid complex computations in selectors
const expensiveData = useScope(() => $.data.map(/* heavy computation */));

// Instead...
const data = useScope(() => $.data);
const expensiveCalculation = useMemo(() => data.map(/* heavy computation */), [data])

2. Use Direct Assignment & Flexible Methods for Updates

// Option 1: Direct assignment
$.user.name = "John";

$.todos.push({ title: "Do Laundry", date: new Date().toISOString() })

// Option 2: Shallow merge a new value without changing existing properties
$.user.$merge({ name: 'John' });

// Option 3: Use the updater function [NEW!]
$.user.$update("age", (age) => age + 1);

3. Configure Persistence (Optional)

// ✅ Configure before your app starts
configure(presets.production());

function App() {
  // Your app components
}

Created by Dalton Letorney

If you like this, feel free to star the repo! If you love it, use it in production. If it breaks, open a PR so we can make this even more epic.


Scope State is minimal by design — the goal is not to reinvent React, but to make it finally feel clean again.

License

MIT © Dalton Letorney

About

The simplest global state system for React. A tiny reactive state manager with global reach and local clarity. Built for modern React. No stale bugs. No mental gymnastics. Full support for storage persistence, auto-optimization, and more.

Topics

Resources

Stars

Watchers

Forks

Packages

No packages published