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 🙏

© 2024 – Pkg Stats / Ryan Hefner

@lecstor/redux-helpers

v5.0.0

Published

Redux boilerplate reduction and lazy loading of store slices

Downloads

18

Readme

Just another redux boilerplate reducing helper module

Redux is good.

Reducers in all their variations often feel more complicated than they could be.

Writing actions creators that just pass through arguments to those reducers is boring.

We still need to be able write the tricky bits without having to dig through layers of abstraction.

Apps using dynamic imports should be able to load just the store slices they need.

Goals:

  • write reducers as a set of plain old functions
  • automatically generate action creators for those reducer functions
  • easily to add more complex (async) action creators
  • easily create secondary reducers that handle actions belonging to other reducers
  • have the option of lazy-loading reducers for apps that use dynamic imports

Regular Redux Store

app/state/index.ts

import { createActionCreator } from "../../../src";

type ActionType = string;
type ReducerFnName = string;
type Slice = string;

// customise action types
const actionTypeCreator = (slice: Slice) => (
  action: ReducerFnName
): ActionType => `${slice}/${action}`;

const createAction = (slice: Slice) =>
  createActionCreator(actionTypeCreator(slice));

export { actionTypeCreator, createAction };

app/state/session/reducer.ts

Write reducer functions as plain functions and export them.

Actions are FSA compatible in that they have a type and payload. (and error: true if payload is an error)

An action creator will be generated for each reducer function.

import { SliceState, User } from "./types";
import { Action } from "@lecstor/redux-helpers";

export type SetUser = Action<User>;

function setUser(state: SliceState, { payload: user }: SetUser): SliceState {
  return {
    ...state,
    user
  };
}

export { setUser };

app/state/session/actions.ts

Create default action creators automatically then add more complex ones manually.

import { Dispatch } from "redux";

import { createAction } from "../index";

const action = createAction("session");

export const setUser = action<User>("setUser");

export const logIn = () => (dispatch: Dispatch) =>
  Promise.resolve({ id: "abc123", firstname: "Fred" }).then(user =>
    dispatch(setUser(user))
  );

app/state/session/selectors.ts

Write selectors however you please.

function getFirstname(state) {
  return state.session.user.firstname;
}

export { getFirstname };

app/state/session/index.js

Import reducer functions and wrap them into a reducer for the state slice. Export all the things.

import { createReducer } from "@lecstor/redux-helpers";
import { actionTypeCreator } from "../index";

import * as actions from "./actions";
import * as fns from "./reducer";
import * as selectors from "./selectors";

import { SliceState } from "./types";

const initialState = {};

const reducer = createReducer<SliceState>(
  "session",
  fns,
  initialState,
  actionTypeCreator
);

export { actions, reducer, selectors };

app/index.js

import * as React from "react";
import { Provider } from "react-redux";
import { applyMiddleware } from "redux";
import { composeWithDevTools } from "redux-devtools-extension";
import thunk from "redux-thunk";

import { createStore } from "@lecstor/redux-helpers";
import { AppState } from "./state/types";

import App from "./app";

import { reducer as session } from "./state/session";

const store = createStore<AppState>(
  { session },
  composeWithDevTools(applyMiddleware(thunk))
);

const AppContainer = () => (
  <Provider store={store}>
    <App />
  </Provider>
);

export default AppContainer;

app/app.js

import * as React from "react";
import { connect } from "react-redux";
import { bindActionCreators } from "redux";

import { actions, selectors } from "./state/session";
import { AppState, Dispatch } from "./state/types";

interface OwnProps {
  firstname?: string;
}

interface StateProps {
  firstname?: string;
}

// relies on currently unpublished version of redux-thunk
// https://github.com/reduxjs/redux-thunk/pull/224
// https://github.com/reduxjs/redux-thunk/commit/4bfa41ceb4281131ccbe9eeda87c07aeaf63b014
// https://github.com/reduxjs/redux-thunk/issues/213
const mapDispatchToProps = (dispatch: Dispatch) =>
  bindActionCreators({ logIn: actions.logIn }, dispatch);

// with a previous redux-thunk..
// const mapDispatchToProps = (dispatch: Dispatch) => ({
//   logIn: () => actions.logIn()(dispatch),
//   setUser: user => dispatch(actions.setUser(user))
// });

type DispatchProps = ReturnType<typeof mapDispatchToProps>;

type Props = OwnProps & StateProps & DispatchProps;

function mapStateToProps(state: AppState, ownProps: OwnProps) {
  return { firstname: selectors.getFirstname(state) || ownProps.firstname };
}

function App({ firstname, logIn }: Props) {
  return (
    <div>
      <div>Hello {firstname || ""}</div>
      <button onClick={() => logIn()}>Log In</button>
    </div>
  );
}

const connected = connect(
  mapStateToProps,
  mapDispatchToProps
);

export default connected(App);

Lazy-load Store Slices

reducer.js, actions.js, selectors.js, and app.js remain the same, we just do things a bit differently when it comes to putting it all together.

app-lazy/state/session/initial-state.js

Break out initial state so we can use it when creating the store without importing the rest of the slice code.

export default {
  user: null
};

app-lazy/state/session/index.js

The only change is to import the initial state rather than declaring it here.

import { createLazyReducer } from "@lecstor/redux-helpers";

import actions from "./actions";
import initialState from "./initial-state";
import * as fns from "./reducer";
import * as selectors from "./selectors";

const reducer = createLazyReducer("session", fns, initialState);

export { actions, reducer, selectors };

app-lazy/state/initial-state.js

Export initial state for all store slices for initialising the lazy store.

import session from "./session/initial-state";
import products from "./products/initial-state";

export default { session, products };

app-lazy/index.js

Initialise the store state with initial state of all slices to keep Redux happy.

Import any slices we want loaded by default, they'll self-register with the reducer-registry.

import * as React from "react";
import { Provider } from "react-redux";
import { applyMiddleware } from "redux";
import { composeWithDevTools } from "redux-devtools-extension";
import thunk from "redux-thunk";

import { createLazyStore } from "@lecstor/redux-helpers";
import App from "./app";

import initialState from "./state/initial-state";

import "./state/session";

const store = createLazyStore(
  initialState,
  composeWithDevTools(applyMiddleware(thunk))
);

const AppContainer = () => (
  <Provider store={store}>
    <App />
  </Provider>
);

export default AppContainer;

API

createActionCreator(slice: string | Function)

slice

  • string: the key for the store property for this store slice
  • function: takes a reducer name and returns an action type

returns:

function createAction<Payload>(reducerFnName: string)

eg

const createAction = createActionCreator("session");

const setUser = createAction<User>("setUser")

equivalent to:

const setUser = payload => {
  const type = "app/session/setUser";
  if (payload instanceof Error) {
    return { type, payload, error: true };
  }
  return { type, payload };
}
const createAction = createActionCreator(reducer => `my/session/${reducer}`);

const setUser = createAction<User>("setUser")

equivalent to:

const setUser = payload => {
  const type = "my/session/setUser";
  if (payload instanceof Error) {
    return { type, payload, error: true };
  }
  return { type, payload };
}

createReducer(sliceName: string, reducerFunctions: Object, initialState: Object, actionTypeCreator?: Function)

sliceName: the key for the store property for this slice

reducerFunctions: the functions that make up the reducer for this slice

initialState: the initial state for this store slice

actionTypeCreator?: custom function to use to generate action types

returns: a reducer function

eg

const fns = {
  setUser(state, { payload }) {
    return { ...state, user: payload };
  }
};

const initialState = {
  user: null
}

createReducer(
  "session",
  fns,
  initialState,
  (slice: string) => (action: string): ActionType =>
    `my/${slice}/${action}`
);

returns:

const reducerObj = {
  "my/session/setUser": function(state, { payload }) {
    return { ...state, user: payload };
  }
}

return function reducer(state = initialState, action) {
  if (state === null) {
    return initialState;
  }
  if (!(action && reducerObj[action.type])) {
    return state;
  }
  return reducerObj[action.type](state, action);
};