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

redux-rsi

v5.0.0

Published

Utility & helper functions for reducing the boilerplate necessary when creating redux reducers & actions

Downloads

11

Readme

Redux RSI

Reduce your risk of repetitive strain injury when using Redux

rsi

Utility & helper functions for reducing the boilerplate necessary when creating redux reducers, actions, & stores. It also includes an implementation of a reducer registry to aid in the creation of redux modules when composing large applications.

Install

yarn add redux-rsi

or

npm install redux-rsi --save

API Reference

  1. createReducer
  2. createAjaxAction
  3. asyncifyAction
  4. mergeWithCurrent
  5. registerReducer
  6. createStore

createReducer(initialState, handlers)

As your app grows more complex, a switch statement in your reducer no longer cuts it when trying to handle actions. The createReducer function takes the initial state of your reducer and will autowire actions to the object you pass in as the handlers parameter. e.g. If an action of type MessageSend is dispatched, it will be autowired to a function with the same name with the on prefix applied, e.g. onMessageSend. Internally, it uses lodash's string methods to wire the methods, so dispatching with type messageSend, message-send, or MESSAGE_SEND will still work.

createReducer assumes your actions are all Flux Standard Actions. If an action handler cannot be found, the current state will be returned to the caller unmodified.

import { Immutable } from "seamless-immutable"; //using seamless-immutable for immutable state in our reducer
import { createReducer } from "redux-rsi";

const users = createReducer(Immutable({
	isAuthenticating: false,
	isAuthenticated: false,
	user: null
}), {
	//this will respond to USER_AUTHENTICATE
	onUserAuthenticate(state, credentials) {
		return state.merge({
			isAuthenticating: true,
			isAuthenticated: false,
			user: null
		});
	},

	//this will respond to USER_AUTHENTICATE_COMPLETED
	onUserAuthenticateCompleted(state, user) {
		return state.merge({
			isAuthenticating: false,
			isAuthenticated: true,
			user: user
		});
	},

	handleError(state, type, err) {
		//do something nice with this error
		console.log(err);
		return state;
	}
});

createAjaxAction(action, getPromise)

If you are using the redux-thunk middleware to make AJAX calls, you might find yourself constantly writing action creators similar to the following:

export function fetchUser(username) {
	return (dispatch, getState) => {
		if (!!getState().users.get(username)) {
			//don't fetch if the user is already loaded in the current state
			return;
		}

		api.fetchUser(username) //this is a separate API lib which will make the AJAX call and return a promise
			.then(response => dispatch(fetchCompleted(response))
			.catch(err => dispatch(fetchFailed(err));

		dispatch({
			type: "USERS_FETCH",
			payload: username
		});
	};
}

function fetchCompleted(response) {
	return {
		type: "USERS_FETCH_COMPLETED",
		payload: response.body
	};
}

function fetchFailed(err) {
	return {
		type: "USERS_FETCH_FAILED",
		payload: err
	};
}

createAjaxAction will help you reduce this boilerplate when making an AJAX call:

import { createAjaxAction } from "redux-rsi";

export function fetchUser(username) {
	return createAjaxAction({
		type: "USERS_FETCH",
		payload: username
	}, getState => {
		if (!!getState().users.get(username)) {
			//don't fetch if the user is already loaded in the current state
			return;
		}

		return api.fetchUser(username);
	});
}

The first argument is the action you want to dispatch. The action type will be used to create two new action types (one with a _COMPLETED appended and one with a _FAILED appended).

The second argument is your function that will return a Promise. If nothing is returned, nothing will be dispatched (e.g. in this case, if the user has already been fetched).

If you have no reason to check the current state, this example can be reduced further:

export function fetchUser(username) {
	return createAjaxAction({
		type: "USERS_FETCH",
		payload: username
	}, () => api.fetchUser(username));
}

asyncifyAction(action)

This will "asyncify" an action by returning to you the completed & failed action types by appending _COMPLETED and _FAILED to your original action type.

import { asyncifyAction } from "redux-rsi";

const fetchActions = asyncifyAction({
	type: "USERS_FETCH",
	payload: username
});

export function fetchUser(username) {
	// assuming using redux-thunk here
	return dispatch => {
		// dispatch the original action USERS_FETCH
		dispatch(fetchActions.request);

		// simulate an ajax request & dispatch USERS_FETCH_COMPLETED after 2 seconds
		setTimeout(fetchActions.completed({ hello: "from the server" }), 2000);
		
		// ...OR...

		// simulate a failed ajax request & dispatch USERS_FETCH_FAILED after 2 seconds
		setTimeout(fetchActions.failed({ message: "error from the server" }), 2000);
	};
}

The failed & completed actions have the payload from the original action set as their meta (see Flux Standard Actions).

This is what createAjaxAction uses internally.

mergeWithCurrent(state, key, data, [initFn])

If your state tree is a keyed object, you may find yourself writing a lot of code that looks something like this:

export default createReducer(Immutable({}), {
	onSomethingHappened(state, data) {
		// first, get the current item in our state because we don't want to overwrite anything
		// if no item exists, just create an empty item
		let item = state[data.key] || initEmptyItem(data.key);

		// merge the new data from the server (while not overwriting anything the server didn't send back)
		item = item.merge(data);

		// set the new data on the state and return it
		return state.set(data.key, item);
	}
});

// our function to init an empty item
function initEmptyItem(key) {
	return Immutable({
		key,
		isLoading: false,
		isLoaded: false,
		items: []
	})
}

mergeWithCurrent will help you reduce this boilerplate:

import { mergeWithCurrent } from "redux-rsi";

export default createReducer(Immutable({}), {
	onSomethingHappened(state, data) {
		return mergeWithCurrent(
			state,			//the state tree
			data.key,		//the key of the item we want to merge
			data,			//the new data to merge with any existing data
			initEmptyItem 	//an optional init function if the data doesn't already exist (if none passed, defaults to {})
		);
	}
});

The above two examples are equivalent. The initFn will receive the state key as its only argument.

registerReducer(name, reducer)

In order to effectively encapsulate the state tree structure from the rest of your app, it is advantageous to colocate your selector functions with your reducers. As your app grows, using this approach you will find that you end up with a massive root reducer file filled with repetetive selector functions just drilling down the state tree to child reducers. The static definition of the reducer/state tree does not scale. If you find yourself in this situation, it can help to introduce a reducer registry to implement the redux module concept.

registerReducer registers a reducer function with the registry with a given name so that any selector functions defined with that reducer will know where they are located within the state tree. This effectively inverts the control of the structure of the state tree. Instead of the application consuming the reducers defining what the state tree looks like, it is the reducers themselves that are defining where they will live within the tree.

// reducer.js
import { registerReducer } from "redux-rsi";

const REDUCER_NAME = "app/counter";

const reducer = (state, { type }) => {
	if (state === undefined) return 0;

	if (type === "INCREMENT") return state + 1;
	if (type === "DECREMENT") return state - 1;

	return state;
};

registerReducer(REDUCER_NAME, reducer);

export const getCount = state => state[REDUCER_NAME];

createStore([initialState], [enhancer], [combineFn])

This API is meant to mirror the official redux createStore API except that it uses the reducers from the reducer registry rather than accepting a reducer parameter.

All parameters are optional and match the behavior of the official API.

The combineFn, if specified, will override the function to use to combine the reducers together. By default, if not specified, it will use the combineReducers function from redux. Depending on the needs of your app, you may want to use a different function (e.g. combineReducers from redux-seamless-immutable).

// app.js
import { createStore } from "redux-rsi";
import { getCount } from "./reducer"; // using the example above from registerReducer

const store = createStore();

this.store.dispatch({ type: "INCREMENT" });

console.log(getCount(this.store.getState())); // 1

Any reducers registered after the store has been created will cause the store to reload and add the new reducers dynamically. This allows for incremental loading and code splitting of reducers.

Contributing

Clone the repo and run yarn to install dependencies.

Linting

To run the linter:

yarn run lint

and to automatically fix fixable errors:

yarn run lint --fix

Testing

The tests are written BDD-style using Mocha. To run them locally:

yarn test

To debug the tests:

yarn test --debug-brk

You can then use Visual Studio Code (or whatever other debugger you desire) to debug through the tests.