effin-redux
TypeScript icon, indicating that this package has built-in type declarations

0.2.9 • Public • Published

Effin Redux

effin-redux logo

Extend redux-toolkit with effects and more.

Check out the live demo (ideally using Redux DevTools) and the code behind it.

Test status npm version

Features

Allow both horizontal and vertical combination of reducers

  • combineReducers combines reducers horizontally, with each of them getting their own slice of the state. This is great for organizing your state, but it provides no solution for when you need to access the state of a slice from another slice. The Redux FAQ itself has no single recommendation, but mentions writing a custom combineReducers implementation as a possible solution.
  • reduceReducers chains reducers vertically, letting them update the same piece of state, one after the other. This is great for acting on the same piece of state, but provides no help when it comes to modularizing the shape of your state.

effin-redux combines the two. This allows you to create slices that depend on the state returned from other slices.

Expand to read more...

Usage

When passing your reducers to configureStore(), use the custom combineSlices() implementation of effin-redux:

// app.ts
const slices = [counterSlice, infoSlice, fizzBuzzSlice] as const; // const is mandatory, and the order matters
const appReducer = combineSlices(slices);
export const store = configureStore({ reducer: appReducer });
export type AppState = ReturnType<typeof store.getState>

Make sure to also create the readAppState() helper:

export const { readAppState } = getHelpers<AppState>();

In your slices, you can use the readAppState() helper to get access to the root state, not just the slice's state:

// slices/fizzBuzz.ts
import { readAppState } from "$app";

export const fizzBuzzSlice = createSlice({
  name: "fizzBuzz",
  initialState,
  reducers: {},
  extraReducers: (builder) => builder.addMatcher(
    (action) => action.type.startsWith("counter"),
    (state) => {
      // state only refers to the state of this one slice
      // but with readAppState, you can get access to the state of other slices too:
      const appState = readAppState(state);
      const currentNumber = appState.counter.count;
      state.value = calculateFizzBuzz(currentNumber);
    },
  ),
});

References

Allow calculating side effects in the reducer

Redux Toolkit, the opinionated redux library includes the Thunk middleware by default and recommends using it for side effects. However, triggering side effects by dispatching thunks from within the application goes against what redux is trying to achieve!

The application's logic should live in its reducer. This is already true for state transitions. But calculating which side effect to trigger and with what arguments is part of the logic too, and it's deeply connected to those state transitions. Why should it be placed in UI components?

effin-redux adds helpers that let your reducer describe what side effects it wants to trigger, and then triggers them on your behalf. The effects themselves are described by the same thunks that redux-toolkit provides.

Expand to read more...

Usage

Use the provided configureStore() implementation to get side effects:

import { configureStore } from "effin-redux";

export const store = configureStore(appReducer);

Alternatively, if you need to use the original configureStore() function from redux-toolkit, you can patch your reducer manually via effin-redux's withEffects() helper:

import { withEffects } from "effin-redux";

export const store = configureStore<AppState>({ reducer: withEffects(appReducer) });

Define your effects for your slices:

import { createEffectInputs, createEffects, forSlice, addEffect } from "effin-redux";

const inputs = createEffectInputs<AppState>()({
  fetchExternalNumber: () => {
    return fetch("https://www.randomnumberapi.com/api/v1.0/random?count=1")
      .then((response) => response.json())
      .then((parsedResponse: [number]) => {
        return parsedResponse[0];
      });
  },
  setSpecificNumber: async ({ requestedNumber }: { requestedNumber: number }) => {
    return { requestedNumber };
  },
});

const effects = createEffects<CounterState>()(inputs, forSlice("counter"));

Then schedule them in your reducer:

export const counterSlice = createSlice({
  name: "counter",
  initialState,
  reducers: {
    externalNumberRequested: (state) => {
      addEffect(state, effects.fetchExternalNumber());
      addEffect(state, effects.consoleLog());
    },
    specificNumberRequested: (state, action: PayloadAction<{ requestedNumber: number }>) => {
      addEffect(state, effects.setSpecificNumber({ requestedNumber: action.payload.requestedNumber }));
    },
  }),

And handle them like any other async thunk:

  extraReducers: (builder) =>
    builder
      .addCase(effects.fetchExternalNumber.pending, (state) => {
        state.isWaitingForExternalNumber = true;
      })
      .addCase(effects.fetchExternalNumber.fulfilled, (state, action) => {
        state.isWaitingForExternalNumber = false;
        state.count = action.payload;
      })
      .addCase(effects.setSpecificNumber.fulfilled, (state, action) => {
        state.count = action.payload.requestedNumber;
      }),
  ),
});

Using the Redux Developer Tools, you will be able to inspect what effects has been scheduled by your reducer, and also when it is being executed.

References

Recover lost state type in createSlice reducers

When defining slices via createSlice(), the type of the state argument within the reducers object should be correctly inferred. Unfortunately, this feature broke with a TypeScript change.

effin-redux adds helpers to work around this - you still have to add the type explicitly, but now at least you only have to do it once, not in every case.

Expand to read more...

Usage

Just wrap your slices' reducers and extraReducers with the appropriate helpers:

import { createExtraReducers, createReducers } from "effin-redux";

export const counterSlice = createSlice({
  name: "counter",
  initialState,
  reducers: createReducers<CounterState>()({
    // ...case reducers
  }),
  extraReducers: createExtraReducers<CounterState>((builder) => {
    // ...extra reducers
  }),
});

References

Installation

npm install effin-redux

The package is distributed as CJS and comes with TypeScript type definitions included.

Readme

Keywords

none

Package Sidebar

Install

npm i effin-redux

Weekly Downloads

147

Version

0.2.9

License

MIT

Unpacked Size

190 kB

Total Files

123

Last publish

Collaborators

  • endreymarcell.npm