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

verdux

v0.6.8

Published

Type-safe, reactive, immutable state management

Readme

verdux

Redux + RxJS + TypeScript = ❤️

Model your application as a reactive, directed acyclic graph. A graph is composed of vertices. Hence the name (vertex + redux = verdux). Duh.

Table of Contents

Motivation

After years of maintaining frontend applications, we have come to realize that :
  • The human brain is naturally wired to process tree-like structures
  • UI applications are most intuitively respresented as hierarchical graphs
  • Asynchronicity is an essential dimension of frontend apps, and the source of most accidental complexity
  • Reactive programming is just too powerful a tool not to use

What is verdux ?

verdux is a state management + asynchronous orchestration library that :
  • Models UI application state as a flow of data streaming through a directed acyclic graph
  • Allows better separation of concern, each vertex in the graph is self-contained and self-sufficient
  • Is designed from the ground up to provide maximum type safety
  • Embraces asynchronicity in its core using the power of RxJS
  • Reduces accidental complexity for most typical front UI cases
  • Prevents unnecessary re-rendering, without any manual memoization

Features

  • Integration with vanilla redux and redux-toolkit
  • Declarative data fetching supporting cascade loading
  • Automatic refetching and error propagation
  • Epics, just like redux-observable, our favorite redux middleware
  • Some other cool stuff

Examples

https://github.com/couzic/verdux-examples

Claude Code plugin

verdux ships a Claude Code plugin with React-focused skills — graph design, dependency injection, operations, React integration, and testing. Install it from the couzic marketplace:

/plugin marketplace add couzic/claude-marketplace
/plugin install verdux@couzic

The skills trigger automatically when you ask Claude about verdux.

DevTools (WIP)

DevTools screenshot

But redux sucks, right ?

A lot of people have complained about redux, some with good reason. Many have been drawn to other state management solutions.

Don't throw the baby with the bathwater.

Although we agree there must be a better way than classical redux, we are not willing to sacrifice all of the redux goodness you've heard so much about.

Making redux great again !

Install

npm install verdux @reduxjs/toolkit rxjs

Create

configureRootVertex()

Create a root vertex configuration. Only one by graph.

import { createSlice } from '@reduxjs/toolkit'
import { configureRootVertex } from 'verdux'

// Just a regular redux-toolkit slice
const slice = createSlice({
   name: 'root',
   initialState: {},
   reducers: {}
})

const rootVertexConfig = configureRootVertex({ slice })

createGraph()

Create a graph with vertex configurations.

import { createGraph } from 'verdux'
import { userVertex } from './userVertex'
import { todosVertex } from './todosVertex'

const appGraph = createGraph({
   vertices: [rootVertexConfig]
})

const rootVertex = appGraph.getVertexInstance(rootVertexConfig)

Options

  • vertices — the vertex configurations that make up the graph.
  • logger (optional) — a sink for verdux's internal diagnostics (the [verdux] … threw messages emitted when a reaction/sideEffect/fieldsReaction callback throws, or when the graph fails fast). Any object with an error(message, error?) method satisfies it — including console (the default), winston, or pino. Every method is optional and falls back to the matching console method, so a partial logger is fine.
const appGraph = createGraph({
   vertices: [rootVertexConfig],
   logger: { error: (message, error) => myLogger.error(message, error) }
})

Computed values (synchronous)

computeFromFields()

Compute derived values from vertex fields.

import { createSlice } from '@reduxjs/toolkit'
import { configureRootVertex } from 'verdux'

const userVertexConfig = configureRootVertex({
   slice: createSlice({
      name: 'user',
      initialState: {
         firstName: 'John',
         lastName: 'Doe'
      },
      reducers: {}
   })
}).computeFromFields(['firstName', 'lastName'], {
   fullName: ({ firstName, lastName }) => `${firstName} ${lastName}`
})

computeFromFields$() (async)

The asynchronous sibling of computeFromFields(). Each computer receives an Observable of the picked fields and returns an Observable of the computed value, so you can apply any RxJS operator (debounce, throttle, combineLatest, …).

import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { debounceTime, map } from 'rxjs'
import { configureRootVertex } from 'verdux'

const searchVertexConfig = configureRootVertex({
   slice: createSlice({
      name: 'search',
      initialState: {
         query: ''
      },
      reducers: {
         queryChanged: (state, action: PayloadAction<string>) => {
            state.query = action.payload
         }
      }
   })
}).computeFromFields$(['query'], {
   debouncedQuery: query$ =>
      query$.pipe(
         debounceTime(300),
         map(({ query }) => query.trim())
      )
})

Loading data

load()

Immediately initialize loaded data.

import { createSlice } from '@reduxjs/toolkit'
import { ajax } from 'rxjs/ajax'
import { configureRootVertex } from 'verdux'

const todosVertexConfig = configureRootVertex({
   slice: createSlice({
      name: 'todos',
      initialState: {},
      reducers: {}
   })
}).load({
   items: ajax.getJSON(`https://www.base.url/todos`)
})

loadFromFields()

Load data based on vertex fields.

const todoVertexConfig = configureRootVertex({
   slice: createSlice({
      name: 'todo',
      initialState: {
         id: '123'
      },
      reducers: {}
   })
}).loadFromFields(['id'], {
   details: ({ id }) => ajax.getJSON(`https://www.base.url/todos/${id}`)
})

loadFromFields$() (async)

The asynchronous sibling of loadFromFields(). Each loader receives an Observable of the picked fields instead of a single snapshot, so you control how overlapping inputs are handled — switchMap cancels the in-flight request when the input changes again before it resolves.

import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { debounceTime, switchMap } from 'rxjs'
import { ajax } from 'rxjs/ajax'
import { configureRootVertex } from 'verdux'

const todoVertexConfig = configureRootVertex({
   slice: createSlice({
      name: 'todo',
      initialState: {
         id: '123'
      },
      reducers: {
         idChanged: (state, action: PayloadAction<string>) => {
            state.id = action.payload
         }
      }
   })
}).loadFromFields$(['id'], {
   details: id$ =>
      id$.pipe(
         debounceTime(300),
         switchMap(({ id }) => ajax.getJSON(`https://www.base.url/todos/${id}`))
      )
})

Loadable status & error handling

load() and loadFromFields() produce loadable fields. Each carries a status of 'loading' | 'loaded' | 'error' alongside its value, readable through vertex.currentLoadableState:

const todoVertex = appGraph.getVertexInstance(todoVertexConfig)
const { status, value, errors } = todoVertex.currentLoadableState.fields.details

If a loader's observable errors, only that field goes to status: 'error' (with the error captured in errors) — the vertex and its other fields stay alive. loadFromFields() rebuilds its loader on every input change, so a later run that succeeds restores the field to 'loaded'.

Consuming the state

vertex.currentState

Synchronous read of vertex current state.

const todoVertex = appGraph.getVertexInstance(todoVertexConfig)
console.log(todoVertex.currentState.id) // '123'

vertex.state$

Observable state

const todoVertex = appGraph.getVertexInstance(todoVertexConfig)
todoVertex.state$.subscribe(state => {
   console.log(state.details) // Logs the return from AJAX call
})

Actions and Reactions

reaction()

Dispatch an action in reaction to another action.

const slice = createSlice({
   name: 'counter',
   initialState: {
      count: 0
   },
   reducers: {
      buttonClicked: () => {},
      increment: state => {
         state.count++
      }
   }
})

const { buttonClicked, increment } = slice.actions

export const counterVertexConfig = configureRootVertex({
   slice
}).reaction(buttonClicked, () => increment())

reaction$() (async)

Handle asynchronous reactions with RxJS.

import { createSlice } from '@reduxjs/toolkit'
import { map, pipe, throttleTime } from 'rxjs'
import { configureRootVertex } from 'verdux'

const slice = createSlice({
   name: 'counter',
   initialState: {
      count: 0
   },
   reducers: {
      buttonClicked: () => {},
      increment: state => {
         state.count++
      }
   }
})

const { buttonClicked, increment } = slice.actions

export const counterVertexConfig = configureRootVertex({
   slice
}).reaction$(
   buttonClicked,
   pipe(
      throttleTime(500),
      map(() => increment())
   )
)

fieldsReaction()

React to field changes rather than to an action: whenever one of the listed fields changes (and once on the initial run), the mapper runs and the action it returns is dispatched. Return null to dispatch nothing.

import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { configureRootVertex } from 'verdux'

const slice = createSlice({
   name: 'user',
   initialState: {
      name: 'John',
      nameLength: 0
   },
   reducers: {
      setName: (state, action: PayloadAction<string>) => {
         state.name = action.payload
      },
      setNameLength: (state, action: PayloadAction<number>) => {
         state.nameLength = action.payload
      }
   }
})

const { setNameLength } = slice.actions

export const userVertexConfig = configureRootVertex({
   slice
}).fieldsReaction(['name'], ({ name }) => setNameLength(name.length))

sideEffect()

Declare side effects to be synchronously executed in response to actions.

const slice = createSlice({
   name: 'analytics',
   initialState: {
      events: []
   },
   reducers: {
      trackEvent: (state, action) => {
         state.events.push(action.payload)
      }
   }
})

export const analyticsVertexConfig = configureRootVertex({
   slice
}).sideEffect(slice.actions.trackEvent, action => {
   trackingApi.sendEvent(action.payload)
})

Downstream vertices

A graph is more than one root vertex. A downstream vertex owns its own slice and pulls selected fields from its parent(s), forming the directed acyclic graph at the heart of verdux. Every vertex config — root or downstream — must be passed to createGraph({ vertices: [...] }).

configureDownstreamVertex()

Wire a child onto a parent. List the parent fields the child depends on in upstreamFields: those fields become readable on the child, and the child's subgraph re-runs whenever one of them changes. Omit upstreamFields and parent changes won't reach the child.

import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { ajax } from 'rxjs/ajax'
import { configureRootVertex } from 'verdux'

const rootVertexConfig = configureRootVertex({
   slice: createSlice({
      name: 'root',
      initialState: {
         userId: '123'
      },
      reducers: {
         userIdChanged: (state, action: PayloadAction<string>) => {
            state.userId = action.payload
         }
      }
   })
})

export const profileVertexConfig = rootVertexConfig.configureDownstreamVertex({
   slice: createSlice({
      name: 'profile',
      initialState: {},
      reducers: {}
   }),
   // Pull `userId` from the parent — it is now a field on this vertex, usable in
   // any operation, and a change to it re-runs this subgraph.
   upstreamFields: ['userId']
}).loadFromFields(['userId'], {
   profile: ({ userId }) => ajax.getJSON(`https://www.base.url/users/${userId}`)
})

configureVertex()

When a vertex has more than one upstream (a true DAG, not a tree), build it with configureVertex and declare each parent via addUpstreamVertex(config, { fields }).

import { createSlice } from '@reduxjs/toolkit'
import { configureRootVertex, configureVertex } from 'verdux'

const rootVertexConfig = configureRootVertex({
   slice: createSlice({ name: 'root', initialState: {}, reducers: {} })
})

const userVertexConfig = rootVertexConfig.configureDownstreamVertex({
   slice: createSlice({
      name: 'user',
      initialState: { name: 'John' },
      reducers: {}
   })
})

const cartVertexConfig = rootVertexConfig.configureDownstreamVertex({
   slice: createSlice({
      name: 'cart',
      initialState: { itemCount: 0 },
      reducers: {}
   })
})

// Pulls `name` from one parent and `itemCount` from another.
export const summaryVertexConfig = configureVertex(
   { slice: createSlice({ name: 'summary', initialState: {}, reducers: {} }) },
   builder =>
      builder
         .addUpstreamVertex(userVertexConfig, { fields: ['name'] })
         .addUpstreamVertex(cartVertexConfig, { fields: ['itemCount'] })
).computeFromFields(['name', 'itemCount'], {
   summary: ({ name, itemCount }) => `${name} has ${itemCount} item(s)`
})

Dependency injection

Instead of importing services directly, a vertex declares its dependencies as factories and receives the resolved instances at graph-creation time. This is what makes a vertex testable — the same config runs against real services in production and fakes in tests.

withDependencies()

Declare dependencies on the config, then consume them: the callback receives the resolved instances plus a vertex you chain operations onto. Everything inside uses injected services rather than hardcoded imports.

import { createSlice } from '@reduxjs/toolkit'
import { configureRootVertex } from 'verdux'
import { createApiClient } from './apiClient'

export const userVertexConfig = configureRootVertex({
   slice: createSlice({
      name: 'user',
      initialState: {
         id: '123'
      },
      reducers: {}
   }),
   dependencies: {
      // A factory: an arrow returning the service instance.
      apiClient: createApiClient
   }
}).withDependencies(({ apiClient }, vertex) =>
   vertex.loadFromFields(['id'], {
      details: ({ id }) => apiClient.getUser(id)
   })
)

A downstream vertex derives its own dependencies from its parent's via the dependencies option of configureDownstreamVertex(), so injected services flow down the graph.

injectedWith()

Swap a vertex's real dependencies for fakes — most often in tests — without touching the config. It takes a partial dependency object; anything omitted keeps its declared factory. Pass the wrapped config to createGraph.

const graph = createGraph({
   vertices: [userVertexConfig.injectedWith({ apiClient: fakeApiClient })]
})

Injected dependencies are inherited by downstream vertices, so injecting once at the root is enough to stub a service for the whole subgraph.

Testing

Testing an action creator, a reducer and a selector in isolation.

Man in three pieces. Legs running in place. Torso doing push-ups. Head reading.

"Looks like it’s working !"

Testing in redux usually implies testing in isolation the pieces that together form the application's state management system. It seems reasonable, since they are supposed to be pure functions.

Testing in verdux follows a different approach. A vertex is to be considered a cohesive unit of functionality. We want to test it as a whole, by interacting with it like the UI component would. We do not want to test its internal implementation details.

Test Setup

Each test should run in isolation, therefore we need to create a new graph for each test.

describe('Counter Vertex', () => {
   let graph: Graph
   let counterVertex: Vertex<typeof counterVertexConfig>
   beforeEach(() => {
      graph = createGraph({
         vertices: [counterVertexConfig]
      })
      counterVertex = graph.getVertexInstance(counterVertexConfig)
   })

   it('should increment counter', () => {
      // Most tests should limit themselves to dispatching actions and verifying that the state has correctly updated.
      graph.dispatch(counterActions.increment())
      expect(counterVertex.currentState.count).to.equal(1)
   })
})