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.io/state-matcher

v1.0.13

Published

Matcher plugin for @jucie.io/state with pattern matching and subscriptions

Readme

@jucio.io/state/matcher

Path-based state watching plugin for @jucio.io/state that allows you to watch specific paths in your state tree and react to changes. Your handlers receive the current data at the watched path, not change objects.

Features

  • 🎯 Path Matching: Watch specific paths in your state tree
  • 📊 Data Values: Handlers receive actual data at the path, not change objects
  • 🌳 Hierarchical Watching: Match exact paths, parent paths, or child paths
  • 📦 Automatic Batching: Changes are automatically batched and debounced
  • 🔄 Smart Consolidation: Multiple changes to the same path are consolidated
  • 🎬 Declarative Setup: Define matchers during state initialization
  • 🔌 Plugin Architecture: Seamlessly integrates with @jucie.io/state

Installation

npm install @jucio.io/state

Note: Matcher plugin is included in the main package.

Quick Start

import { createState } from '@jucio.io/state';
import { Matcher, createMatcher } from '@jucio.io/state/matcher';

// Create a matcher
const userMatcher = createMatcher(['user'], (userData) => {
  console.log('User data:', userData);
});

// Create state and install matcher plugin
const state = createState({
  user: { name: 'Alice', age: 30 },
  settings: { theme: 'dark' }
});

// Install with initial matchers
state.install(Matcher.configure({
  matchers: [userMatcher]
}));

// Change user data - matcher receives the NEW data
state.set(['user', 'name'], 'Bob');
// Console: "User data: { name: 'Bob', age: 30 }"

// Change settings - matcher doesn't fire (different path)
state.set(['settings', 'theme'], 'light');

API Reference

Creating Matchers

createMatcher(path, handler)

Create a matcher that watches a specific path in the state tree.

The handler receives the current data at the watched path, not change objects.

import { createMatcher } from '@jucio.io/state/matcher';

const matcher = createMatcher(['users', 'profile'], (profileData) => {
  console.log('Profile is now:', profileData);
});

Parameters:

  • path (Array): Path to watch (e.g., ['user'], ['users', 'profile'])
  • handler (Function): Callback function that receives the current data at the path

Returns: Matcher function that can be added to the plugin

Plugin Actions

When using the Matcher plugin with a state instance, you get access to these actions:

state.matcher.createMatcher(path, handler)

Create and automatically register a matcher.

import { createState } from '@jucio.io/state';
import { Matcher } from '@jucio.io/state/matcher';

const state = createState({ user: { name: 'Alice' } });
state.install(Matcher);

const unsubscribe = state.matcher.createMatcher(['user'], (userData) => {
  console.log('User is now:', userData);
});

// Later: remove the matcher
unsubscribe();

Returns: Unsubscribe function

state.matcher.addMatcher(matcher)

Add an existing matcher to the plugin.

const matcher = createMatcher(['user'], (userData) => {
  console.log('User:', userData);
});

state.matcher.addMatcher(matcher);

state.matcher.removeMatcher(matcher)

Remove a matcher from the plugin.

state.matcher.removeMatcher(matcher);

Match Types

Matchers use hierarchical matching with three types. The handler always receives the current data at the matched path:

Exact Match

Watches the exact path specified:

const matcher = createMatcher(['user', 'profile'], (profileData) => {
  console.log('Profile data:', profileData);
});

state.set(['user', 'profile'], { bio: 'Hello' }); // ✅ Fires with { bio: 'Hello' }
state.set(['user', 'profile', 'bio'], 'Hi');       // ✅ Fires with { bio: 'Hi' }
state.set(['user'], { profile: { bio: 'Hi' } });   // ✅ Fires with { bio: 'Hi' }
state.set(['user', 'settings'], {});                // ❌ Doesn't fire

Parent Match

Fires when a parent path or any descendant changes:

const matcher = createMatcher(['user'], (userData) => {
  console.log('User data:', userData);
});

state.set(['user', 'name'], 'Alice');               // ✅ Fires with entire user object
state.set(['user', 'profile', 'bio'], 'Hello');     // ✅ Fires with entire user object
state.set(['user'], { name: 'Bob' });               // ✅ Fires with { name: 'Bob' }

Child Match

When watching a parent and children change, child changes are consolidated:

const matcher = createMatcher(['users'], (usersData) => {
  console.log('Users data:', usersData);
});

state.set(['users', 'alice'], { name: 'Alice' });
state.set(['users', 'bob'], { name: 'Bob' });

// Both changes are batched. Handler receives the full current state:
// { alice: { name: 'Alice' }, bob: { name: 'Bob' } }

Configuration

Initialize with Matchers

import { createState } from '@jucio.io/state';
import { Matcher, createMatcher } from '@jucio.io/state/matcher';

const userMatcher = createMatcher(['user'], (user) => {
  console.log('User:', user);
});

const settingsMatcher = createMatcher(['settings'], (settings) => {
  console.log('Settings:', settings);
});

const state = createState({ user: {}, settings: {} });
state.install(Matcher.configure({
  matchers: [userMatcher, settingsMatcher]
}));

Define Matchers as Objects

import { createState } from '@jucio.io/state';
import { Matcher } from '@jucio.io/state/matcher';

const state = createState({ user: {}, settings: {} });
state.install(Matcher.configure({
  matchers: [
    {
      path: ['user'],
      handler: (user) => console.log('User:', user)
    },
    {
      path: ['settings'],
      handler: (settings) => console.log('Settings:', settings)
    }
  ]
}));

Advanced Usage

Multiple Matchers on Same Path

const logger = createMatcher(['user'], (userData) => {
  console.log('User changed:', userData);
});

const validator = createMatcher(['user'], (userData) => {
  if (!userData.email) {
    console.warn('User has no email!');
  }
});

state.matcher.addMatcher(logger);
state.matcher.addMatcher(validator);

state.set(['user'], { name: 'Alice' });
// Both matchers fire with { name: 'Alice' }

Dynamic Matcher Management

// Add matcher conditionally
if (process.env.NODE_ENV === 'development') {
  const debugMatcher = state.matcher.createMatcher(['*'], (data) => {
    console.log('DEBUG: State changed:', data);
  });
}

// Add/remove based on user settings
function toggleAuditLog(enabled) {
  if (enabled) {
    const auditMatcher = createMatcher(['data'], (data) => {
      logToServer('data-change', data);
    });
    state.matcher.addMatcher(auditMatcher);
    return () => state.matcher.removeMatcher(auditMatcher);
  }
}

Nested Path Watching

// Watch different levels of nesting
const userMatcher = createMatcher(['user'], (userData) => {
  console.log('Entire user object:', userData);
});

const profileMatcher = createMatcher(['user', 'profile'], (profileData) => {
  console.log('Just profile:', profileData);
});

const nameMatcher = createMatcher(['user', 'profile', 'name'], (name) => {
  console.log('Just name:', name);
});

state.set(['user', 'profile', 'name'], 'Alice');
// All three matchers fire:
// - userMatcher gets the entire user object
// - profileMatcher gets just the profile object  
// - nameMatcher gets just 'Alice'

Batching and Consolidation

const matcher = createMatcher(['items'], (itemsData) => {
  console.log('Items:', itemsData);
});

// Multiple rapid changes are batched
state.set(['items', 'item1'], { value: 1 });
state.set(['items', 'item2'], { value: 2 });
state.set(['items', 'item3'], { value: 3 });

// Single callback with current state:
// { item1: { value: 1 }, item2: { value: 2 }, item3: { value: 3 } }

Common Patterns

Form Field Validation

const emailMatcher = createMatcher(['form', 'email'], (email) => {
  const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
  state.set(['form', 'errors', 'email'], isValid ? null : 'Invalid email');
});

state.matcher.addMatcher(emailMatcher);

Persistence

const persistMatcher = createMatcher(['user', 'preferences'], (preferences) => {
  localStorage.setItem('preferences', JSON.stringify(preferences));
});

state.matcher.addMatcher(persistMatcher);

Analytics Tracking

const analyticsMatcher = createMatcher(['analytics', 'events'], (events) => {
  Object.entries(events).forEach(([key, event]) => {
    trackEvent(event.name, event.properties);
  });
});

state.matcher.addMatcher(analyticsMatcher);

Derived State Updates

// Update derived state when source changes
const cartMatcher = createMatcher(['cart', 'items'], (items) => {
  const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  state.set(['cart', 'total'], total);
});

state.matcher.addMatcher(cartMatcher);

API Synchronization

const syncMatcher = createMatcher(['user'], async (userData) => {
  try {
    await fetch('/api/user', {
      method: 'PUT',
      body: JSON.stringify(userData)
    });
    console.log('User synced to server');
  } catch (error) {
    console.error('Failed to sync user:', error);
  }
});

state.matcher.addMatcher(syncMatcher);

Performance Considerations

  1. Automatic Batching: Matchers automatically batch changes using setTimeout(fn, 0), so multiple synchronous changes trigger the handler only once

  2. Smart Consolidation: Multiple changes to the same path are consolidated into a single update

  3. Efficient Matching: Uses marker comparison for fast path matching

  4. Cleanup: Always unsubscribe matchers when they're no longer needed to prevent memory leaks

// Good: Clean up when done
const unsubscribe = state.matcher.createMatcher(['temp'], handler);
// ... later
unsubscribe();

Comparison with OnChange Plugin

| Feature | Matcher | OnChange | |---------|---------|----------| | What handler receives | Current data at path | Change objects with metadata | | Scope | Specific paths | Global changes | | Batching | Automatic | Automatic | | Consolidation | Smart path-based | By change address | | Performance | Optimized for specific paths | Tracks all changes | | Use Case | Watch specific data, get values | Track all changes, get metadata |

Use Matcher when: You want the current data at specific paths Use OnChange when: You need change metadata (from/to values, method, etc.)

License

See the root LICENSE file for license information.

Related Packages