npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@jucie-state/reactive

v1.0.9

Published

Reactive plugin for @jucie-state/core - signals, computed values, and fine-grained reactivity

Readme

@jucie-state/reactive

Reactive programming plugin for @jucie-state/core providing signals, computed values (reactors), and declarative reactive surfaces with fine-grained dependency tracking.

Features

  • 🎯 Fine-Grained Reactivity: Automatic dependency tracking at the property level
  • Signals: Simple reactive values with getter/setter interface
  • 🧮 Computed Values (Reactors): Derived values that automatically update
  • 🏄 Surfaces: Component-like reactive contexts with lifecycle management
  • 🔄 Async Support: Built-in support for async computations
  • 📦 Batched Updates: Efficient batch processing of changes
  • 🎬 Effects: Run side effects when reactive values change
  • 🔌 State Integration: Seamless integration with @jucie-state/core
  • 🎨 Framework Adapters: Built-in React adapter support

Installation

npm install @jucie-state/reactive

Note: Requires @jucie-state/core as a peer dependency.

Quick Start

Signals

Simple reactive values:

import { createSignal } from '@jucie-state/reactive';

// Create a signal
const count = createSignal(0);

console.log(count()); // 0

// Update the signal
count(1);
console.log(count()); // 1

// Update based on current value
count(n => n + 1);
console.log(count()); // 2

Reactors (Computed Values)

Automatically computed values that track dependencies:

import { createSignal, createReactor } from '@jucie-state/reactive';

const firstName = createSignal('John');
const lastName = createSignal('Doe');

// Create a computed value
const fullName = createReactor(() => {
  return `${firstName()} ${lastName()}`;
});

console.log(fullName()); // "John Doe"

// Update dependencies - fullName automatically recomputes
firstName('Jane');
console.log(fullName()); // "Jane Doe"

Integration with State

import { State } from '@jucie-state/core';
import { Reactive, createReactor } from '@jucie-state/reactive';

const state = new State({
  user: { name: 'Alice', age: 30 }
});
state.install(Reactive);

// Create a reactor that depends on state
const userInfo = createReactor(() => {
  const user = state.get(['user']);
  return `${user.name} is ${user.age} years old`;
});

console.log(userInfo()); // "Alice is 30 years old"

// Update state - reactor automatically recomputes
state.set(['user', 'age'], 31);
console.log(userInfo()); // "Alice is 31 years old"

API Reference

Signals

createSignal(initialValue, config?)

Create a reactive signal.

const count = createSignal(0);
const name = createSignal('Alice', {
  debounce: 100,  // Debounce updates by 100ms
  effects: [() => console.log('Name changed!')],
  immediate: true  // Run effects immediately
});

Parameters:

  • initialValue (any): Initial value
  • config (object): Optional configuration
    • debounce (number): Debounce time in ms
    • effects (Function[]): Side effect functions
    • immediate (boolean): Compute immediately
    • onAccess (Function): Called when value is accessed
    • detatched (boolean): Don't track as dependency

Returns: Signal getter/setter function

Usage:

// Get value
const value = signal();

// Set value
signal(newValue);

// Update based on current value
signal(current => current + 1);

Reactors (Computed)

createReactor(fn, config?)

Create a computed reactive value.

const doubled = createReactor(() => count() * 2);

const asyncData = createReactor(async () => {
  const response = await fetch('/api/data');
  return response.json();
});

Parameters:

  • fn (Function): Computation function
  • config (object): Optional configuration
    • debounce (number): Debounce recomputation
    • effects (Function[]): Side effect functions
    • immediate (boolean): Compute immediately
    • initialValue (any): Initial cached value
    • context (any): Context passed to function
    • detatched (boolean): Don't track as dependency

Returns: Reactor getter function

Async Support:

const data = createReactor(async () => {
  const result = await fetchData();
  return result;
});

// Async reactors return promises
data().then(value => console.log(value));

destroyReactor(reactor)

Cleanup and destroy a reactor.

const computed = createReactor(() => value() * 2);
destroyReactor(computed);

Effects

addEffect(getter, effect)

Add a side effect to a reactive value.

const count = createSignal(0);

addEffect(count, (value) => {
  console.log('Count changed to:', value);
});

count(1); // Console: "Count changed to: 1"

Surfaces

Composable reactive contexts that combine state, computed values, and actions:

defineSurface(setupFn)

Create a reactive surface - a self-contained reactive unit similar to a component.

import { defineSurface } from '@jucie-state/reactive';

const useCounter = defineSurface((setup) => {
  // Create reactive value (signal)
  const count = setup.value(0);
  
  // Create computed value
  const doubled = setup.computed(() => count() * 2);
  
  // Create action
  const increment = setup.action(() => {
    count(count() + 1);
  });
  
  // Return what should be exposed
  return {
    count,
    doubled,
    increment
  };
});

// Use the surface - returns a reactor
const counter = useCounter();

// The reactor returns current snapshot with computed values
console.log(counter.count); // 0 (current value, not a function)
console.log(counter.doubled); // 0

// Actions are directly callable
counter.increment();
console.log(counter.count); // 1
console.log(counter.doubled); // 2

Setup Function API

The setup function receives an object with these methods:

Creating Reactive Values:

  • setup.value(initial) - Create a signal (reactive primitive)
  • setup.state(initial) - Create a state instance (reactive object/array)
  • setup.computed(fn) - Create a computed value
  • setup.action(fn) - Create an action function

Composition:

  • setup.extend(surface) - Extend another surface to inherit its values/actions
  • setup.destroy() - Manually destroy the surface

Usage Example:

const useUser = defineSurface((setup) => {
  const user = setup.value({ name: 'Alice', age: 30 });
  
  const displayName = setup.computed(() => {
    const u = user();
    return `${u.name} (${u.age})`;
  });
  
  const setName = setup.action((_, name) => {
    user(u => ({ ...u, name }));
  });
  
  return { user, displayName, setName };
});

Surface Return Value

The surface reactor, when called, returns a frozen snapshot with:

  • Values rendered: Signals, state, and computed values show their current values (not functions)
  • Actions available: Action functions are directly callable
  • Helper methods: Special $ prefixed methods for advanced operations

Surface Helper Methods

The returned surface includes these helper methods:

Value Access:

  • surface.$get(path) - Get value at a path (e.g., ['user', 'name'])
  • surface.$snapshot(path?) - Get frozen snapshot at path

Value Updates:

  • surface.$setValue(name, value) - Set a signal value by name
  • surface.$setState(path, value) - Set a state value at nested path
  • surface.$set(path, value) - Generic set for signals or state
  • surface.$dispatch(name, ...args) - Call an action by name

Subscriptions:

  • surface.$subscribe(listener) - Subscribe to any surface changes
  • surface.$bind(path) - Returns [getSnapshot, subscribe] for a specific path
  • surface.$adapter() - Returns [getSnapshot, subscribe] for framework integration

Utilities:

  • surface.$inject(overrides) - Create new surface with injected overrides
  • surface.$destroy() - Destroy the surface and cleanup

Example:

const counter = useCounter();

// Get nested value
counter.$get(['count']); // 0

// Set signal value
counter.$setValue('count', 5);

// Dispatch action
counter.$dispatch('increment');

// Subscribe to changes
const unsubscribe = counter.$subscribe((snapshot) => {
  console.log('Counter changed:', snapshot.count);
});

// Bind to specific path for framework integration
const [getCount, subscribe] = counter.$bind(['count']);
console.log(getCount()); // Current count value

Context in Computed and Actions

Computed functions and actions receive the surface context as the first parameter:

const useCalculator = defineSurface((setup) => {
  const num1 = setup.value(5);
  const num2 = setup.value(10);
  
  // Context gives access to all surface values
  const sum = setup.computed((context) => {
    return context.num1() + context.num2();
  });
  
  // Actions also receive context
  const updateNum1 = setup.action((context, newValue) => {
    context.num1(newValue);
  });
  
  return { num1, num2, sum, updateNum1 };
});

Extending Surfaces

Surfaces can extend other surfaces to compose functionality:

const useCounter = defineSurface((setup) => {
  const count = setup.value(0);
  const increment = setup.action(() => count(count() + 1));
  return { count, increment };
});

const useEnhancedCounter = defineSurface((setup) => {
  // Extend base counter - returns its context
  const counter = setup.extend(useCounter);
  
  // Add new functionality
  const incrementTwice = setup.action(() => {
    counter.increment();
    counter.increment();
  });
  
  return { incrementTwice };
});

// The enhanced surface has both original and new functionality
const enhanced = useEnhancedCounter();
console.log(enhanced.count); // 0 (from extended surface)
enhanced.increment(); // Available from extension
enhanced.incrementTwice(); // New functionality
console.log(enhanced.count); // 2

Framework Integration

React Integration with $adapter()

const useCounter = defineSurface((setup) => {
  const count = setup.value(0);
  const increment = setup.action(() => count(count() + 1));
  return { count, increment };
});

// In React component
function CounterComponent() {
  const counter = useCounter();
  const [snapshot, subscribe] = counter.$adapter();
  
  // Use with React.useSyncExternalStore
  const state = React.useSyncExternalStore(subscribe, snapshot);
  
  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={state.increment}>Increment</button>
    </div>
  );
}

Path Binding with $bind()

const useUser = defineSurface((setup) => {
  const user = setup.value({ 
    profile: { name: 'Alice', email: '[email protected]' } 
  });
  return { user };
});

const app = useUser();

// Bind to nested path
const [getName, subscribe] = app.$bind(['user', 'profile', 'name']);

console.log(getName()); // 'Alice'

const unsubscribe = subscribe((name) => {
  console.log('Name changed to:', name);
});

Advanced Usage

Debounced Reactors

const searchTerm = createSignal('');

const searchResults = createReactor(async () => {
  const term = searchTerm();
  if (!term) return [];
  
  const response = await fetch(`/api/search?q=${term}`);
  return response.json();
}, {
  debounce: 300  // Wait 300ms after last change
});

Conditional Dependencies

const showDetails = createSignal(false);
const details = createSignal({ name: 'Alice' });

const display = createReactor(() => {
  if (showDetails()) {
    return details().name;  // Only depends on details when showDetails is true
  }
  return 'Hidden';
});

Context Passing

const component = createReactor((context) => {
  return `User: ${context.userName}`;
}, {
  context: { userName: 'Alice' }
});

Multiple Effects

const count = createSignal(0, {
  effects: [
    (value) => console.log('Effect 1:', value),
    (value) => console.log('Effect 2:', value),
  ]
});

// Or add effects later
addEffect(count, (value) => {
  console.log('Effect 3:', value);
});

Surface with Complex State

const useTodoList = defineSurface((setup) => {
  // State
  const todos = setup.value([]);
  const filter = setup.value('all');
  
  // Computed
  const filteredTodos = setup.computed((context) => {
    const allTodos = context.todos();
    const currentFilter = context.filter();
    
    if (currentFilter === 'active') {
      return allTodos.filter(t => !t.done);
    }
    if (currentFilter === 'completed') {
      return allTodos.filter(t => t.done);
    }
    return allTodos;
  });
  
  const stats = setup.computed((context) => {
    const allTodos = context.todos();
    return {
      total: allTodos.length,
      active: allTodos.filter(t => !t.done).length,
      completed: allTodos.filter(t => t.done).length
    };
  });
  
  // Actions
  const addTodo = setup.action((context, text) => {
    context.todos(current => [...current, { text, done: false, id: Date.now() }]);
  });
  
  const toggleTodo = setup.action((context, id) => {
    context.todos(current => 
      current.map(t => t.id === id ? { ...t, done: !t.done } : t)
    );
  });
  
  const setFilter = setup.action((context, filterValue) => {
    context.filter(filterValue);
  });
  
  return {
    todos,
    filter,
    filteredTodos,
    stats,
    addTodo,
    toggleTodo,
    setFilter
  };
});

// Use it
const todoList = useTodoList();
console.log(todoList.stats); // { total: 0, active: 0, completed: 0 }
todoList.addTodo('Learn surfaces');
console.log(todoList.stats); // { total: 1, active: 1, completed: 0 }

Performance Tips

  1. Use signals for primitive values - They're optimized for simple get/set
  2. Debounce expensive computations - Prevent excessive recomputation
  3. Use detatched: true for values you read but don't want to track
  4. Batch updates when making multiple changes
  5. Destroy reactors when no longer needed to prevent memory leaks

Common Patterns with Surfaces

Composing Multiple Surfaces

// Create reusable surfaces
const useCounter = defineSurface((setup) => {
  const count = setup.value(0);
  const increment = setup.action(() => count(count() + 1));
  return { count, increment };
});

const useTodos = defineSurface((setup) => {
  const todos = setup.value([]);
  const addTodo = setup.action((_, todo) => {
    todos(current => [...current, todo]);
  });
  return { todos, addTodo };
});

// Combine them
const useApp = defineSurface((setup) => {
  const counter = setup.extend(useCounter);
  const todoContext = setup.extend(useTodos);
  
  const addTodoAndIncrement = setup.action((context, todo) => {
    todoContext.addTodo(todo);
    counter.increment();
  });
  
  return { addTodoAndIncrement };
});

const app = useApp();
console.log(app.count); // 0 (from counter)
console.log(app.todos); // [] (from todos)
app.addTodoAndIncrement('Task 1');
console.log(app.count); // 1
console.log(app.todos); // ['Task 1']

Using State within Surfaces

const useStore = defineSurface((setup) => {
  // Create a State instance (reactive object)
  const store = setup.state({ 
    user: { 
      profile: { name: 'Alice', age: 30 } 
    } 
  });
  
  const updateName = setup.action((_, name) => {
    store.set(['user', 'profile', 'name'], name);
  });
  
  return { store, updateName };
});

const app = useStore();

// On the surface, store is rendered as its current value
console.log(app.store); // { user: { profile: { name: 'Alice', age: 30 } } }
console.log(app.store.user.profile.name); // 'Alice'

// Update using helper or action
app.$setState(['store', 'user', 'profile', 'name'], 'Bob');
console.log(app.store.user.profile.name); // 'Bob'

Form State Management

const useForm = defineSurface((setup) => {
  const name = setup.value('');
  const email = setup.value('');
  const errors = setup.value({});
  
  const isValid = setup.computed((context) => {
    const currentName = context.name();
    const currentEmail = context.email();
    return currentName.length > 0 && currentEmail.includes('@');
  });
  
  const submit = setup.action(async (context) => {
    if (context.isValid()) {
      await fetch('/api/submit', {
        method: 'POST',
        body: JSON.stringify({
          name: context.name(),
          email: context.email()
        })
      });
    }
  });
  
  return { name, email, errors, isValid, submit };
});

// Use it
const form = useForm();
form.$setValue('name', 'Alice');
form.$setValue('email', '[email protected]');
console.log(form.isValid); // true
await form.submit();

License

See the root LICENSE file for license information.

Related Packages