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 🙏

© 2025 – Pkg Stats / Ryan Hefner

rx-tiny-flux

v1.0.30

Published

A lightweight, minimalist state management library for pure JavaScript projects, inspired by NgRx and Redux, and built with RxJS.

Readme

Rx-Tiny-Flux

Rx-Tiny-Flux is a lightweight, minimalist state management library for pure JavaScript projects, heavily inspired by the patterns of NgRx and Redux. It leverages the power of RxJS for reactive state management.

The primary dependencies are rxjs and jsonpath.

A Case Study on AI-Assisted Development: This entire library was developed in just a few hours as a case study to explore the capabilities of Gemini Code Assist. It demonstrates how modern AI coding assistants can significantly accelerate the development process, from initial scaffolding to complex feature implementation and refinement.

Core Concepts

The library is built around a few core concepts:

  • Store: A single, centralized object that holds the application's state.
  • Actions: Plain objects that describe events or "things that happened" in your application.
  • Reducers: Pure functions that determine how the state changes in response to actions.
  • Effects: Functions that handle side effects, such as API calls, which can dispatch new actions.
  • Selectors: Pure functions used to query and derive data from the state.

Actions

Actions are the only source of information for the store. You dispatch them to trigger state changes. Use the createAction factory function to create them.

import { createAction } from 'rx-tiny-flux';

// An action creator for an event without a payload
const increment = createAction('[Counter] Increment');

// An action creator for an event with a payload
const add = createAction('[Counter] Add');

// Dispatching the actions
store.dispatch(increment()); // { type: '[Counter] Increment' }
store.dispatch(add(10));     // { type: '[Counter] Add', payload: 10 }

Reducers

Reducers specify how the application's state changes in response to actions. A reducer is a pure function that takes the previous state slice and an action, and returns the next state slice.

Use createReducer along with the on and anyAction helpers to define your reducer logic.

// Library imports
import { createReducer, on, anyAction } from 'rx-tiny-flux';

// Your application's action imports
import { increment, decrement, incrementSuccess } from './actions';

// This reducer manages the 'counter' slice of the state.
const counterReducer = createReducer(
  // 1. The jsonpath to the state slice
  '$.counter',
  // 2. The initial state for this slice
  { value: 0, lastUpdate: null },
  // 3. Handlers for specific actions
  on(increment, incrementSuccess, (state) => ({
    ...state,
    value: state.value + 1,
    lastUpdate: new Date().toISOString(),
  })),
  on(decrement, (state) => ({
    ...state,
    value: state.value - 1,
    lastUpdate: new Date().toISOString(),
  }))
);

// This reducer manages the 'log' slice and reacts to ANY action.
const logReducer = createReducer(
  '$.log',
  [], // Initial state for this slice
  // Using the `anyAction` token to create a handler that catches all actions.
  on(anyAction, (state, action) => [...state, `Action: ${action.type} at ${new Date().toISOString()}`])
);

Effects

Effects are used to handle side effects, such as asynchronous operations (e.g., API calls). An effect listens for dispatched actions, performs some work, and can dispatch new actions as a result.

Use createEffect and the ofType operator to build effects.

// Core library imports
import { createEffect, ofType, from, map, concatMap } from 'rx-tiny-flux';

// Your application's action imports
import { incrementAsync, incrementSuccess } from './actions';

// A function that simulates an API call (e.g., fetch) which returns a Promise
const fakeApiCall = () => new Promise(resolve => setTimeout(() => resolve({ success: true }), 1000));

const incrementAsyncEffect = createEffect((actions$) =>
  actions$.pipe(
    // Listens only for the 'incrementAsync' action
    ofType(incrementAsync),
    // Use concatMap to handle the async operation.
    // It waits for the inner Observable (from the Promise) to complete.
    concatMap(() =>
      from(fakeApiCall()).pipe( // `from` converts the Promise into an Observable
        map(() => incrementSuccess()) // On success, map the result to a new action
      )
    )
  )
);

Selectors

Selectors are pure functions used for obtaining slices of store state. The library provides two factory functions for this:

  1. createFeatureSelector: Selects a top-level slice of the state using a jsonpath expression.
  2. createSelector: Composes multiple selectors to compute derived data.
import { createFeatureSelector, createSelector } from 'rx-tiny-flux';

// 1. We use `createFeatureSelector` with a jsonpath expression to get a top-level slice of the state.
const selectCounterSlice = createFeatureSelector('$.counter');

// 2. We use `createSelector` to compose and extract more granular data from the slice.
const selectCounterValue = createSelector(
  selectCounterSlice,
  (counter) => counter?.value // Added 'optional chaining' for safety
);

const selectLastUpdate = createSelector(
  selectCounterSlice,
  (counter) => counter?.lastUpdate
);

// Using the selector with the store
store.select(selectCounterValue).subscribe((value) => {
  console.log(`Counter value is now: ${value}`);
});

Putting It All Together: The Store

The Store is the central piece that brings everything together. You instantiate it, register your reducers and effects, and then use it to dispatch actions and select state.

// Library imports
import { Store } from 'rx-tiny-flux';

// Import all your application's reducers, effects, and actions
import { counterReducer, logReducer } from './path/to/your/reducers';
import { incrementAsyncEffect } from './path/to/your/effects';
import { increment, incrementAsync } from './path/to/your/actions';

// 1. The Store can start with an empty state.
const initialState = {};

// 2. Create a new Store instance
const store = new Store(initialState);

// 3. Register all reducers. The store will build its initial state from them.
store.registerReducers(counterReducer, logReducer);

// 4. Register all effects.
store.registerEffects(incrementAsyncEffect);

// 5. Dispatch actions to trigger state changes and side effects.
console.log('Dispatching actions...');
store.dispatch(increment());
store.dispatch(increment());
store.dispatch(incrementAsync()); // Will trigger the effect

// 6. Select and subscribe to state changes.
// It's crucial to capture the subscription object so we can unsubscribe later.
const counterSubscription = store.select(selectCounterValue).subscribe((value) => {
  console.log(`Counter value is now: ${value}`);
});

// 7. Clean Up (Unsubscribe)
// In any real application, you must unsubscribe to prevent memory leaks.
// When the subscription is no longer needed (e.g., a component is destroyed),
// call the .unsubscribe() method.
counterSubscription.unsubscribe();

ZeppOS Integration (via ZML)

For developers using the ZML library on the ZeppOS platform, rx-tiny-flux offers an optional plugin that seamlessly integrates the store with the BaseApp and BasePage component lifecycle.

This plugin injects dispatch and subscribe methods into your component's instance. Most importantly, the subscribe method is lifecycle-aware: it automatically tracks all subscriptions and unsubscribes from them when the component's onDestroy hook is called, preventing common memory leaks.

How to Use

  1. Create your store instance in app.js.
  2. Import the storePlugin from rx-tiny-flux.
  3. Register the plugin on BaseApp, passing the store instance: BaseApp.use(storePlugin, store).
  4. Register the same plugin on BasePage (without the store): BasePage.use(storePlugin). The plugin will automatically find the store from the App.
  5. For a Side Service, you can create a separate store or use the same one, but you must pass it during registration, just like with BaseApp: BaseSideService.use(storePlugin, serviceStore).
  6. Use this.dispatch() and this.subscribe() inside your App, Pages, and Side Service.

Here is a complete example:

// app.js - Your application's entry point
import { BaseApp } from '@zeppos/zml';
import { Store } from 'rx-tiny-flux';
import { storePlugin } from 'rx-tiny-flux';

// 1. Import your reducers, actions, etc.
import { counterReducer } from './path/to/reducers';

// 2. Create your store instance
const store = new Store({});
store.registerReducers(counterReducer);

// 3. Register the plugin on BaseApp, providing the store.
BaseApp.use(storePlugin, store);

App(BaseApp({
  // ... your App config
}));
// page/index.js - An example page
import { BasePage, ui } from '@zeppos/zml';
import { selectCounterValue } from '../path/to/selectors';
import { increment } from './path/to/actions';

// 4. Register the plugin on BasePage, without providing the store. It will be retrieved from the App.
BasePage.use(storePlugin);

Page(BasePage({
  build() {
    const myText = ui.createWidget(ui.widget.TEXT, { /* ... */ });

    // 5. Use `this.subscribe` to listen to state changes
    this.subscribe(selectCounterValue, (value) => {
      myText.setProperty(ui.prop.TEXT, `Counter: ${value}`);
    });

    // Use `this.dispatch` to dispatch actions
    ui.createWidget(ui.widget.BUTTON, {
      // ...
      click_func: () => this.dispatch(increment()),
    });
  },

  onDestroy() {
    // No need to unsubscribe manually!
    // The storePlugin will do it for you automatically.
    console.log('Page destroyed, subscriptions cleaned up.');
  }
}));

Accessing Component Context in Effects

The storePlugin automatically injects the component instance (this from BasePage or BaseApp or BaseSideService) into every dispatched action under the context property. This powerful feature allows your effects to access other plugins or methods available on the component instance, such as a logger, a toast notification service, or the router.

This enables better separation of concerns, where effects can trigger UI-related side effects without being tightly coupled to specific UI components.

Example: Showing a Toast Notification from an Effect

Imagine you have a toast plugin registered on your BasePage. You can create an effect that listens for a success action and uses the injected context to show a notification.

// effects/toast.effect.js
import { createEffect, ofType, tap } from 'rx-tiny-flux';
import { operationSuccess } from '../actions';

export const showSuccessToastEffect = createEffect(
  (actions$) =>
    actions$.pipe(
      ofType(operationSuccess),
      // The action now contains the 'context' of the Page that dispatched it
      tap((action) => {
        // Check if the context and the toast plugin exist before using it
        if (action.context && action.context.toast) {
          action.context.toast.show({ text: 'Operation successful!' });
        }
      })
    ),
  // This effect does not dispatch a new action, so we set dispatch: false
  { dispatch: false }
);

Environment-Specific Effects with isApp and isSideService

When building complex ZeppOS applications, you often need effects that run exclusively on the watch face (App/Page) or in the background service (Side Service). To simplify this, rx-tiny-flux provides two custom RxJS operators: isApp and isSideService.

These operators filter the action stream based on the execution environment, making your effects cleaner and more declarative.

  • isApp(): Allows actions to pass through only when running on the App/Page.
  • isSideService(): Allows actions to pass through only when running in the Side Service.

Propagating Actions and Preserving Context

A powerful feature of the ZeppOS integration is the automatic management of context. When you dispatch an action, the storePlugin attaches the current component instance (App, Page, or Service) to the action as action.context.

Crucially, when an effect creates a new action (e.g., a Success action in response to a Request action), the library automatically preserves this context.

This means you can create chains of effects across different contexts (like App -> Side Service -> App) and the final resulting action will still have the context of the component that initiated the entire chain. This allows you to easily perform UI updates (like showing a toast) in response to a background task completing.

Example: Requesting data from the Side Service and showing a toast on completion

// Import operators and factories
import { createEffect, ofType, isApp, isSideService, propagateAction, map, tap } from 'rx-tiny-flux';
import { fetchData, fetchDataSuccess } from './actions'; // Your actions

// 1. Effect on the App: When `fetchData` is dispatched, propagate it to the Side Service.
const requestDataEffect = createEffect(actions$ => actions$.pipe(
  ofType(fetchData),   // Listen for the initial request
  isApp(),             // Run only on the App/Page side
  propagateAction()    // Send the action to the Side Service
), { dispatch: false });

// 2. Effect on the Side Service: When `fetchData` is received, perform a task and dispatch a result.
const handleDataRequestEffect = createEffect(actions$ => actions$.pipe(
  ofType(fetchData),     // Listen for the propagated action
  isSideService(),       // Run only on the Side Service
  // Perform async work... then map the result to a success action.
  // You can use the standard `map` operator. The context is handled automatically!
  map(() => fetchDataSuccess({ payload: 'data from service' }))
));

// 3. Effect on the App: Listen for the final success action and use its context to update the UI.
const showSuccessToastEffect = createEffect(actions$ => actions$.pipe(
  ofType(fetchDataSuccess),
  isApp(),
  tap(action => {
    // This works because the `context` from the original `fetchData` action was
    // automatically attached to the `fetchDataSuccess` action by the store.
    if (action.context && action.context.toast) {
      action.context.toast.show({ text: 'Data loaded!' });
    }
  })
), { dispatch: false });

Accessing State within Effects using withLatestFromStore

A common requirement for effects is to access the current state to make decisions. For example, an effect might need the current user's ID to fetch data. The withLatestFromStore operator is designed for this purpose, especially in ZeppOS where the store instance isn't readily available when defining effects.

It works by safely using the subscribe method injected into the action's context by the storePlugin.

Example: Fetching data using a value from the state

// Import all operators from the main entry point
import { createEffect, ofType, withLatestFromStore, switchMap, map, catchError, from, of } from 'rx-tiny-flux';
import { fetchData, fetchDataSuccess, fetchDataError } from './actions';
import { selectCurrentUserId } from './selectors';

const fetchDataEffect = createEffect(actions$ => actions$.pipe(
  ofType(fetchData),
  // Combines the action with the latest value from the store using the selector
  withLatestFromStore(selectCurrentUserId),
  // The next operator receives an array: [action, userId]
  switchMap(([action, userId]) =>
    from(api.fetchDataForUser(userId)).pipe(
      map(data => fetchDataSuccess(data)),
      catchError(error => of(fetchDataError(error)))
    ))
));