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/matcher

v1.0.8

Published

Matcher plugin for @jucie-state/core - path-based change tracking

Readme

@jucie-state/matcher

Path-based change tracking plugin for @jucie-state/core that allows you to watch specific paths in your state tree and react to changes with automatic batching and smart consolidation.

Features

  • 🎯 Path Matching: Watch specific paths in your state tree
  • 🌳 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-state/core

Installation

npm install @jucie-state/matcher

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

Quick Start

import { createState } from '@jucie-state/core';
import { Matcher, createMatcher } from '@jucie-state/matcher';

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

// 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 fires
state.set(['user', 'name'], 'Bob');
// Console: "User changed: { name: 'Bob', age: 30 }"

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

API Reference

Creating Matchers

createMatcher(path, handler)

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

import { createMatcher } from '@jucie-state/matcher';

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

Parameters:

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

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 '@jucie-state/core';
import { Matcher } from '@jucie-state/matcher';

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

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

// Later: remove the matcher
unsubscribe();

Returns: Unsubscribe function

state.matcher.addMatcher(matcher)

Add an existing matcher to the plugin.

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

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:

Exact Match

Watches the exact path specified:

const matcher = createMatcher(['user', 'profile'], (changes) => {
  console.log('Exact profile change:', changes);
});

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

Parent Match

Fires when a parent path changes:

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

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

Child Match

When a parent path is matched, child changes are consolidated:

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

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

// Both changes are batched and consolidated:
// Console: "Users changed: { alice: { name: 'Alice' }, bob: { name: 'Bob' } }"

Configuration

Initialize with Matchers

import { createState } from '@jucie-state/core';
import { Matcher, createMatcher } from '@jucie-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 '@jucie-state/core';
import { Matcher } from '@jucie-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'], (user) => {
  console.log('User changed:', user);
});

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

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

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

Dynamic Matcher Management

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

// 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'], (user) => {
  console.log('Entire user object:', user);
});

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

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

state.set(['user', 'profile', 'name'], 'Alice');
// All three matchers fire with their respective scopes

Batching and Consolidation

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

// 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 all changes:
// Console: "Items changed: ['item1', 'item2', 'item3']"

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 (user) => {
  try {
    await fetch('/api/user', {
      method: 'PUT',
      body: JSON.stringify(user)
    });
    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 | |---------|---------|----------| | Scope | Specific paths | Global changes | | Batching | Automatic | Manual | | Consolidation | Smart path-based | No consolidation | | Performance | Optimized for specific paths | All changes | | Use Case | Watch specific data | Track all changes |

Use Matcher when you want to watch specific parts of your state tree. Use OnChange when you need to track all state changes.

License

See the root LICENSE file for license information.

Related Packages