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

deuce2

v0.1.19

Published

A library to make real-world Redux usage easier and to reduce boilerplate.

Downloads

4

Readme

Deuce

A library to make real-world Redux usage easier and to reduce boilerplate.

Pain points we hope to address:

  1. Creating selectors that are colocated with the associated reducer is difficult, since the selector needs to know the full path to that part of the state. If you want to change some part of the state tree at a higher level, you then need to update all selectors that depend on that state path.

  2. When you have a reducer that has any sort of loading from the server, you're constantly reimplementing the same loading state tracking and error tracking.

  3. In a single-page app, if you're keeping each page's state in the tree, you need a way to reset a page's state across page navigation. Otherwise, the next time they come back to the page, its old state will persist.

  4. Almost all reducers are composed of one big switch statement, which requires the same repetitive boilerplate for assigning a default value, getting the payload of the action, and switching on action.type. Any boilerplate that can be reduced in Redux is a win.

What's inside Deuce?

  • combineReducersAndSelectors(): A replacement for combineReducers that also packages in simple selectors. These selectors are wrapped with the path to the specific part of the state, so they can be written in the same file as the reducer and operate on the same small state object.
  • loadable(): A higher-order reducer that upgrades a simple reducer with loading state and error tracking, for a given async action.
  • resettable(): A higher-order reducer that upgrades a simple reducer with the ability to reset when a special action is dispatched.
  • createReducer(): A helper function for building simple reducers that would otherwise be created with a switch statement on action.type and may use the action's payload.

combineReducersAndSelectors()

Problem

Let's say you have a state shape like this:

root
  |- score
  |- team

And you're working on a simple score reducer. It would be really nice to have any selector functions live inside your score.js reducer file, since you're actually defining the shape of the score state inside that file.

The problem is that your reducer will need to look something like:

const selectScorePart = (state: RootState) => state.path.to.score.scorePart;

Now, if we ever change the path to the score reducer, we'll have to go to that file and change all of the selector functions to know about the new path to the reducer from root (and likely change a bunch of other affected reducer files as well).

Solution

Let's say we wanted to make the root reducer above, using Redux's combineReducers() function. We'd do something like this:

const rootReducer = combineReducers({
    score: scoreReducer,
    team: teamReducer,
});

The combineReducersAndSelectors() function works a lot like combineReducers(), except that instead of taking a map of reducers, it takes a map of objects of the shape

{ 
    reducer: Reducer,
    selectors: {
        [selectorName: string]: Selector },
    }
}

I'll talk about those selectors in a minute. But let's just recreate what we did with combineReducers using combineReducersAndSelectors:

const { reducer as rootReducer } = combineReducersAndSelectors({
    score: {
        reducer: scoreReducer,
        selectors: {}, // none for now
    },
    team: {
        reducer: teamReducer,
        selectors: {}, // none for now
    }
});

That would make the exact same rootReducer as before (it actually uses combineReducers under the hood).

Now, on to these selectors. The selectors are simple functions that take a part of the state (like score or team above) and select something from it. The don't need to know anything about where that part of the state is in the root tree.

const selectScorePart = (state: scoreState) => state['the part you want'];

When you put these selectors in combineReducersAndSelectors(), it auto-magically provides them with the part of the state they care about, just like combineReducers passes our reducer the part of the state it cares about.

Here's our combineReducersAndSelectors setup with some selectors added:

const { reducer as rootReducer } = combineReducersAndSelectors({
    score: {
        reducer: rootReducer,
        selectors: {
            selectHomeScore, // These names need to be globally unique from other selectors
            selectAwayScore,
        }
    },
    team: {
        reducer: teamReducer,
        selectors: {
            selectHomeTeamName, // Also unique, even from the selectors in `score`
            selectAwayTeamName,
        }
    }
})

It's important that you give your selector a unique name, unique from any other selector in the entire state tree.

Here's where it gets cool. combineReducersAndSelectors() returns an object with the same shape as its argument. It's an object with a reducer key and selectors.

The reducer key is the result of combineReducers on all the reducers passed to combineReducersAndSelectors.

The selectors are upgraded versions of all the selectors you passed to combineReducersAndSelectors that take the global root state as their input. They are smart enough to take the root state and pass just the part that your original selector cared about to it.

Here's an example:

// Your original selector
const selectHomeScore = (state: ScoreState) => state.homeScore;

// Your upgraded selector, created automatically with combineReducersAndSelectors().
// It now takes the root state, so you can use it in your container components.
// But, it has the exact same name as the original.
const selectHomeScore = (state: ScoreState) => state.score.homeScore;

Now, you can use these selectors in your connected container components. If you want to change the state tree hierarchy, all you do is change the object you pass to combineReducersAndSelectors — none of your reducers or selectors need to change!

For example, if you renamed score to points in the Redux tree, your selectHomeScore selector above would automatically now select state.points.homeScore.

loadable()

Problem

Most reducers are really simple at their core. They're often tracking very few things and making really simple mutations. But, as soon as they start needing to support asynchronous updates with loading states (because users need to see a loading spinner) and error states (because users are human), your reducer balloons in complexity.

Solution

Let's say our score reducer, from the example in the combineReducersAndSelectors section, had a state with this shape:

const defaultState = {
    homeScore: 1,
    awayScore: 2,
}

And, let's say the reducer for this is really simple — it listens for one action type with new scores to apply:

const scoreReducer = (score: ScoreState, action) => {
    if (action.type === 'score/SET_SCORES') {
        return action.payload; // { homeScore: #, awayScore: # }
    }

    return score; // For any other action, do nothing
}

Now, let's imagine we want to load these scores from an async call to the server, which is done with a helper function called getScores().

We might put it in an action like this, relying on the redux-promise-middleware to recognize it's an action and dispatch the appropriate _PENDING, _FULFILLED, or _REJECTED actions for us:

const getScoresAction = () => ({
    type: 'score/SET_SCORES',
    payload: getScores(), // Our async server call that returns a Promise
});

It would be pretty annoying to make a bunch of changes to our dead-simple reducer to listen for all three action types that redux-promise-middleware dispatches. Then our reducer would be more about loading than the actual scores!

Instead, we can just upgrade it with loadable():

const wrappedReducer = loadable(scoreReducer, 'score/SET_SCORES'); // The second arg is the base action to make loadable. Can be a string or an array of multiple strings if you want to make multiple actions loadable.

Now, our new wrappedReducer has the same shape as our previous scoreReducer, but with two special keys tracked by loadable:

  • loading: a boolean for whether we are currently waiting on the server
  • error: either null, or the payload of the action if our Promise is rejected

So, now our reducer state would have a default value like this:

{
    loading: false,
    error: null,
    homeScore: 1,
    awayScore: 2,
}

loadable() keeps up with the Promise, updating loading and error for us. Here's how those keys look acrosss the different dispatches from redux-promise-middleware:

  • '[our action key]_PENDING': loading is true, error is null
  • '[our action key]_FULFILLED': loading is false, error is null, and an action is given to our reducer matching { type: '[our action key]', payload: (same payload as the resolved promise) }
  • '[our action key]_REJECTED': loading is false, error is the payload of the rejected action (from our Promise).

NOTE: the original reducer is not called when there's an error, for two reasons:

  • You can check if there's an error now, and render the UI accordingly
  • It's often useful to still know the previous state when there's an error. For example, if we had a scoreboard component using our score reducer, then we could keep the old scores in place while notifying the user that they weren't able to be updated. That's better than showing nothing (although you could, if you wanted to, by listening for the error key).

In our example, keep in mind our original reducer didn't have to change at all. It still listens for score/SET_SCORES and updates the homeScore and awayScore like it used to.

When connecting this new wrapped loadable reducer with a container component, you can use the provided selectors isLoading and getError, which take the state tracked by the reducer as an argument and return the contents of the loading and error keys created by loadable.

Listening for other actions to reset loadable state

From time to time, you may want to listen for other actions that should reset the loading and error state. For example, if your user was attempting to save the name of an object and you got an error from the server that the name was a duplicate, you might want to clear the error message once they edit the name.

resettable()

Problem

In a single-page app, we often want various states to reset when the user does certain things. For example, we might want to clear the state used by a page when the user navigates away so that it isn't the same if they come back.

Solution

Assuming we had the same state tree as before:

root
  |- score
  |- team

Let's say we want score to reset when the user navigates away, but team can stay cached. We could make a custom action for score that gets dispatched. But that would get tedious in a real app, because we could have dozens of states for different pages that each need the same boilerplate.

To make that easier, you can use the resettable() higher-order reducer. It will add handling for a special action that, when dispatched, will cause the state to reset (this is done by dispatching an action to your reducer with an undefined state, so that the default value will be returned).

The special action can be created with the resetStates() action creator. Just dispatch the action returned by the function, and any reducer that has been wrapped in resettable will reset itself.

For the use case of resetting states when the user leaves a page, you could have a shared component on all pages dispatch the action in its componentWillUnmount.

createReducer()

Problem

Redux reducers are tedious. You have to remember to make a default state and to return the state unchanged if there's an action you're not listening for. You have to make a switch for action.type, and remember to get the contents of action.payload, over and over again every time you build a reducer.

Solution

The createReducer() helper removes all this boilerplate.

It takes a gnarly reducer file like this:

const getDefaultState = ({
    score: 1,
});

const reducer(state = getDefaultState(), action) = {
    switch(action.type) {
        case 'SUM':
            return { score: state.score + action.payload };
        case 'DIFFERENCE':
            return { score: state.score - action.payload };
        case 'DOUBLE':
            return { score: state.score * 2 };
        case 'NEW_SCORE':
            return { score: action.payload };
        default:
            return state;
    }
}

and lets you replace it with this svelt file:

const reducer = createReducer(
    { score: 1 }, // default state to use
    {
        'SUM' /* the action.type */: ({ score }, payload) => { score: score + payload },
        'DIFFERENCE': ({ score }, payload) => { score: score - payload },
        'DOUBLE': ({ score }) => { score: score * 2 },
        'NEW_SCORE': (_, payload) => { score: payload },
    },
);

Your skinny new reducer will automatically return its default state when initiated, listen for the appropriate action.type, and return the state unchanged when there are actions you don't care about.