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

@fillament/redux

v2.0.0

Published

Optional Redux bridge for Fillament — mirror form state into an existing Redux store. Redux is an optional peer; nothing else in Fillament depends on it.

Readme

@fillament/redux

Optional Redux bridge for Fillament. Mirror form values, errors, or full state into an existing Redux (or Redux Toolkit) store. Redux is an optional peer dependency — nothing else in Fillament needs it.

pnpm add @fillament/redux
# you bring your own:
pnpm add redux @reduxjs/toolkit

If you're not already on Redux, don't add it just for forms. Use @fillament/persist for draft restore, the core subscribeFormState for analytics, and component state for the rest.


Quick start

import { configureStore } from "@reduxjs/toolkit";
import { useForm } from "@fillament/react";
import { createReduxBridge, fillamentReducer } from "@fillament/redux";

const store = configureStore({
  reducer: { fillament: fillamentReducer },
});

const form = useForm({
  schema,
  plugins: [
    createReduxBridge({
      store,
      slice: "checkoutForm",
      mode: "values-only",
    }),
  ],
});

The bridge dispatches { type: "fillament/checkoutForm/SET", slice: "checkoutForm", payload: { values } } on every value change.


Exports

| Export | Kind | Purpose | | --- | --- | --- | | createReduxBridge(options) | factory | Returns a FillamentPlugin that mirrors form state into the store. | | fillamentReducer(state, action) | reducer | A slice-aware reducer that handles fillament/* actions. Drop into configureStore. | | createFillamentSlice(slice) | factory | Per-slice helper — returns { name, actionType, reducer, setAction } for RTK-style consumers. | | StoreLike<TState> | type | Minimal store contract — getState / dispatch / subscribe. Structurally compatible with the real Redux Store. | | ReduxBridgeMode | type | "values-only" \| "values-and-errors" \| "full-state". | | ReduxBridgeOptions<TValues, TState> | type | Options accepted by createReduxBridge. | | FillamentReducerState | type | The shape fillamentReducer produces. | | FormState | type | Re-exported from @fillament/core. |


createReduxBridge(options)

ReduxBridgeOptions<TValues, TState>

| Option | Type | Default | Notes | | --- | --- | --- | --- | | store | StoreLike<TState> | required | Any object satisfying the StoreLike contract — the real Redux Store, RTK's store, or a structurally compatible one. | | slice | string | required | Slice name. Becomes the action's slice field and is used to address the reducer state. | | mode | ReduxBridgeMode | "values-only" | What to mirror. See below. | | actionType | string | `fillament/${slice}/SET` | Override if you need to namespace differently. | | debounceMs | number | 0 | Coalesce dispatches. Recommended for very large forms. | | hydrate | boolean | false | When true, read state[slice].values on mount and setValues into the form. | | selectValues | (state: TState) => Partial<TValues> \| undefined | — | Custom selector for hydration. Defaults to state[slice].values. |

Modes

ReduxBridgeMode chooses what's dispatched:

  • "values-only" (default) — { values }. Cheap, no render-y state.
  • "values-and-errors"{ values, errors }. Useful for showing form errors elsewhere in the UI.
  • "full-state"{ state }, the entire FormState<TValues> (values, errors, touched, dirty, isSubmitting, …). Use for full-state-driven UIs.

Returned plugin

A standard FillamentPlugin with onInit, onValuesChange, onSubmitSuccess, onReset. The bridge does not subscribe to the store after the initial hydration — sync is one-way (form → store) to avoid the classic infinite loop where store updates re-enter the form, which re-dispatches, etc.

If you genuinely need two-way sync, wire a separate subscription in your component with explicit conflict resolution.


StoreLike<TState>

interface StoreLike<TState = any> {
  getState(): TState;
  dispatch(action: { type: string; [k: string]: unknown }): unknown;
  subscribe(listener: () => void): () => void;
}

The real Redux Store and Redux Toolkit's store both satisfy this trivially. No casts required.


Action shape

Dispatched actions look like:

{
  type: "fillament/<slice>/SET",
  slice: "<slice>",
  payload: { values?, errors?, state? }   // depends on `mode`
}

You can listen for them in your own reducers or middleware — they're plain Redux FSAs.


fillamentReducer

A pre-built reducer that handles every fillament/*/SET action and stores the payload under state[slice]:

import { configureStore } from "@reduxjs/toolkit";
import { fillamentReducer } from "@fillament/redux";

const store = configureStore({
  reducer: { fillament: fillamentReducer },
});

// After typing into a form wired with slice "checkoutForm":
store.getState();
// { fillament: { checkoutForm: { values: { … } } } }

fillamentReducer ignores actions whose type doesn't start with fillament/ and actions missing a slice field, so it composes safely alongside your own reducers.

FillamentReducerState

interface FillamentReducerState {
  [slice: string]: { values?: any; errors?: any; state?: any };
}

createFillamentSlice(slice)

A per-slice helper for RTK consumers who'd rather wire each form's slice individually:

import { configureStore } from "@reduxjs/toolkit";
import { createFillamentSlice, createReduxBridge } from "@fillament/redux";

const checkoutSlice = createFillamentSlice("checkout");

const store = configureStore({
  reducer: {
    [checkoutSlice.name]: checkoutSlice.reducer,
  },
});

useForm({
  schema,
  plugins: [createReduxBridge({ store, slice: "checkout" })],
});

Returns:

interface FillamentSlice {
  name: string;          // the slice name you passed in
  actionType: string;    // `fillament/<slice>/SET`
  reducer: (state, action) => MirroredPayload;
  setAction(payload): { type, slice, payload };  // create the action manually
}

Use slice.setAction({ values }) if you want to dispatch a synthetic update from elsewhere (e.g. tests, "load saved order" buttons that pre-populate a form via the store).


Hydration pattern

createReduxBridge({
  store,
  slice: "checkoutForm",
  hydrate: true,
  selectValues: (state) => state.checkoutForm?.savedDraft?.values,
});

Hydration runs once during onInit. If selectValues returns undefined or an empty object, hydration is skipped silently.


Avoiding infinite loops

The bridge is one-way after hydration. If you build a separate subscription pushing store changes back into the form (e.g. for "load this saved order"), gate it so the same dispatch path doesn't re-fire:

let suppress = false;
store.subscribe(() => {
  if (suppress) return;
  const next = selectValues(store.getState());
  if (shouldApply(next)) {
    suppress = true;
    form.setValues(next);
    setTimeout(() => { suppress = false; }, 0);
  }
});

The simpler pattern is: only push store→form on explicit user actions (a "Load saved order" button), not on every store change.


Debouncing

For large forms, batch dispatches:

createReduxBridge({ store, slice: "x", debounceMs: 100 });

The bridge cancels and replaces pending dispatches when newer ones arrive, so you only ever pay for the latest.


Testing

The bridge is exercised against a tiny StoreLike in the package's own tests — useful as a template:

function makeStore() {
  let state = {};
  const listeners = new Set<() => void>();
  const actions: any[] = [];
  return {
    actions,
    getState: () => state,
    dispatch: (action) => {
      actions.push(action);
      state = fillamentReducer(state, action);
      listeners.forEach((l) => l());
      return action;
    },
    subscribe: (l) => { listeners.add(l); return () => listeners.delete(l); },
  };
}

Then assert on store.actions after driving the form.


License

MIT © headlessButSmart