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

redux-sacala

v0.3.7

Published

A library for creating composable Redux blocks with state, actions, and effects

Readme

Redux Sacala

Tests

A library for creating composable Redux blocks with state, actions, and effects.

Terms Definition

  • ReduxBlock: A composable unit of Redux logic that encapsulates state, action creators, and effect handlers. It provides a structured way to define how state changes and how side effects are handled.
  • Action: A pure function that describes how the state changes in response to an event. In redux-sacala, actions are defined using .action() and they also serve as action creators.
  • Effect: A non-pure handler that can perform side effects such as asynchronous API calls, logging, or dispatching other actions. Effects are defined using .effects() and have access to a context object providing necessary dependencies.
  • Selector: A pure function that takes the state and returns a derived value. Selectors are defined using .selectors() and can be accessed via .select. For memoization, it is recommended to use createSelector from @reduxjs/toolkit. In compositions, selectors from child blocks are automatically "lifted" and available under the block's name.

Examples

Simple Block with Actions

import { ReduxBlock } from "redux-sacala";

const counterBlock = ReduxBlock.builder("counter", 0)
    .action("inc", (state: number) => state + 1)
    .action("add", (state: number, value: number) => state + value)
    .build();

// Usage:
// counterBlock.actions.inc() -> { type: "counter/inc" }
// counterBlock.actions.add(5) -> { type: "counter/add", payload: [5] }

Block with Effects

interface User { id: string; name: string; }

interface UserContext {
    fetchUser: (id: string) => Promise<User>;
    dispatch: (action: any) => void;
}

const userBlock = ReduxBlock.builder("user", { data: null as User | null, loading: false })
    .action("setLoading", (state, loading: boolean) => ({ ...state, loading }))
    .action("setData", (state, data: User) => ({ ...state, data, loading: false }))
    .effects((ctx: UserContext) => ({
        loadUser: async (id: string) => {
            ctx.dispatch(userBlock.actions.setLoading(true));
            const data = await ctx.fetchUser(id);
            ctx.dispatch(userBlock.actions.setData(data));
        }
    }))
    .build();

Blocks Composition

const rootBlock = ReduxBlock.composition("root")
    .block("counter", counterBlock)
    .block("user", userBlock)
    .build();

// rootBlock.actions.counter.inc()
// rootBlock.actions.user.loadUser("123")

Blocks Composition with an Extra Effect

const rootBlock = ReduxBlock.composition("root")
    .block("counter", counterBlock)
    .effects((ctx: { logger: (msg: string) => void, dispatch: (a: any) => void }) => ({
        logAndIncrement: () => {
            ctx.logger("Incrementing counter");
            ctx.dispatch(rootBlock.actions.counter.inc());
        }
    }))
    .build();

// Usage:
// rootBlock.actions.counter.inc()
// rootBlock.actions.counter.add(5)
// rootBlock.actions.logAndIncrement()

Selectors

Selectors allow you to extract and derive data from the state. They are defined using .selectors() and are available on the .select property of the built block.

const counterBlock = ReduxBlock.builder("counter", { count: 0 })
    .action("inc", (state) => ({ count: state.count + 1 }))
    .selectors({
        count: (state) => state.count,
        doubleCount: (state) => state.count * 2,
    })
    .build();

// Usage:
// counterBlock.select.count({ count: 5 }) -> 5
// counterBlock.select.doubleCount({ count: 5 }) -> 10

For complex or expensive derivations, it is recommended to use createSelector from @reduxjs/toolkit (or reselect) to enable memoization:

import { createSelector } from '@reduxjs/toolkit';

const counterBlock = ReduxBlock.builder("counter", { count: 0 })
    .selectors({
        count: (state) => state.count,
        // Memoized selector using createSelector
        tripleCount: createSelector(
            [(state: { count: number }) => state.count],
            (count) => count * 3
        ),
    })
    .build();

When blocks are composed, their selectors are automatically "lifted" to work with the composition's state.

const rootBlock = ReduxBlock.composition("root")
    .block("counter", counterBlock)
    .selectors({
        isPositive: (state) => state.counter.count > 0,
    })
    .build();

// Lifted selector from counterBlock:
// rootBlock.select.counter.count({ counter: { count: 5 } }) -> 5

// Composition selector:
// rootBlock.select.isPositive({ counter: { count: 5 } }) -> true

Context Mapping

You can change the context shape of a block using ReduxBlock.mapContext. This is useful when you want to adapt a block to a different environment or use a more convenient context structure.

interface OldContext {
    log: {
        error: (msg: string) => void;
        info: (msg: string) => void;
    };
}

const block = ReduxBlock.builder("test", { message: "" })
    .effects((ctx: OldContext) => ({
        logError: (msg: string) => ctx.log.error(msg),
    }))
    .build();

interface NewContext {
    log: (level: "error" | "info", msg: string) => void;
}

const mappedBlock = ReduxBlock.mapContext(
    block,
    (ctx: NewContext): OldContext => ({
        log: {
            error: (msg) => ctx.log("error", msg),
            info: (msg) => ctx.log("info", msg),
        },
    }),
);

// Now mappedBlock expects NewContext

Selector Mapping

You can adapt a block's selectors to work with a different state shape using ReduxBlock.mapSelectors. This is useful when you want to embed a block as part of a larger state structure or reuse a block with different state organization.

const counterBlock = ReduxBlock.builder("counter", { count: 0 })
    .action("inc", (state) => ({ count: state.count + 1 }))
    .selectors({
        count: (state) => state.count,
        doubleCount: (state) => state.count * 2,
    })
    .build();

// Original selector expects { count: number }
// counterBlock.select.count({ count: 5 }) -> 5

// Map selectors to work with a different state shape
interface AppState {
    myCounter: { count: number };
}

const mappedBlock = ReduxBlock.mapSelectors(
    counterBlock,
    (state: AppState) => state.myCounter
);

// Now selectors expect AppState
// mappedBlock.select.count({ myCounter: { count: 5 } }) -> 5
// mappedBlock.select.doubleCount({ myCounter: { count: 5 } }) -> 10

Note: In most cases, compositions handle selector lifting automatically. Use mapSelectors when you need explicit control over how selectors access state, such as when integrating with existing Redux stores or creating reusable blocks.

Minimal Redux Toolkit Example

import { configureStore } from '@reduxjs/toolkit';
import { ReduxBlock } from "redux-sacala";

const store = configureStore({
    reducer: rootBlock.reducer,
    middleware: (getDefaultMiddleware) => 
        getDefaultMiddleware().concat(
            ReduxBlock.middleware(rootBlock, {
                dispatch: (action) => store.dispatch(action),
                logger: console.log,
                fetchUser: async (id) => ({ id, name: "John Doe" })
            })
        ),
});

Dependency Injection with di-sacala

For larger applications, it is highly recommended to use di-sacala to manage and provide dependencies to your effects. It provides a clean way to define and resolve dependencies with full type safety.

import { configureStore } from '@reduxjs/toolkit';
import { ReduxBlock } from "redux-sacala";
import { DiContainer, DiService } from "di-sacala";

// 1. Define your services
class ApiService implements DiService<"api"> {
    name = "api" as const;
    async fetchUser(id: string) {
        return { id, name: "User " + id };
    }
}

class LoggerService implements DiService<"logger"> {
    name = "logger" as const;
    log(msg: string) {
        console.log(`[LOG]: ${msg}`);
    }
}

class DispatcherService implements DiService<"dispatch"> {
    name = "dispatch" as const;
    constructor() {
    }
    dispatch(action: Action) {
        // Dispatch action using Redux store
    }
}

// 2. Create and configure the container
const container = new DiContainer()
    .inject(ApiService)
    .inject(LoggerService)
    .inject(DispatcherService);

const store = configureStore({
    reducer: rootBlock.reducer,
    middleware: (getDefaultMiddleware) => 
        getDefaultMiddleware().concat(
            ReduxBlock.middleware(rootBlock, container)
        ),
});

License

MIT