duckfactory

Factory for creating redux ducks

Usage no npm install needed!

<script type="module">
  import duckfactory from 'https://cdn.skypack.dev/duckfactory';
</script>

README

Duckfactory

Redux ducks, even simpler

Redux / react redux are pretty great, but as the code grows, things easily get cumbersome, complex and fragmented. Redux ducks is an interesting proposal for handling this: organizing the code in bundles of action creators, action types and reducers that belong together, in the very common cases where there's a one-to-one (-to-one) relationship between them.

Duckfactory is a library for wrapping and simplifying this even more. It auto-generates action creators and action types and encapsulates them, only exposing them when needed for side purposes like unit testing. A lot of the boilerplate is abstracted away, the project code gets more minimal, clean and readable, and you, the brilliant developer, gets happier.

How does it work?

In short, give it a prefix string to group the actions, an initial state, an object with the names of action creators and the reducers the actions should trigger. It will then create an object that exposes ordinary redux action creators, reducers and action types.

Example

Let's define a reducer in an example file users.js:

import DuckFactory from 'duckfactory';

// Just an example initial redux state (userId → userObject):
const initialState = {
    42: {
        name: "Arthurarthurdent",
        age: "30-ish",
        occupation: "Sandwich maker"
    },
};

// Defining two ducks in the collection 'myapp/userducks':
const userDucks = new DuckFactory("myapp/userducks", initialState, {
    setName: (state, {userId, newName}) => {
        const newUser = {...state[userId]};
        if (!newUser) {
            return state;
        }
        return {
            ...state,
            [userId]: {
                ...newUser,
                name: newName
            }
        }
    },
    
    addUser: (state, {userId, name, age, occupation}) => {
        return {
            ...state,
            [userId]: { name, age, occupation }
        }
    }
});

About the syntax here: I prefer the ES6 arrow notation and destructuring the action argument. But the more classic syntax like this should also work fine, so you can define it like this instead:

    ///...
    
    setName: function (state, action) {
        const newUser = {...state[action.userId]};
        if (!newUser) {
            return state;
        }
        return {
            ...state,
            [action.userId]: {
                ...newUser,
                name: action.newName
            }
        }
    },
    
    addUser: (state, action) => {
        return {
            ...state,
            [action.userId]: { 
                name: action.name, 
                age: action.age, 
                occupation: action.occupation 
            }
        }
    }
    
    ///...

Both of these syntaxes are fine: the action creators are autogenerated in runtime, and the action creator arguments are deduced from the arguments in the reducer functions in the definition. That way, once you've defined it as above, you can get the action creators and use them directly:

const actionCreators = userDucks.getActionCreators();

const action1 = actionCreators.setName("Arthur Dent");
const action2 = actionCreators.addUser(43, "Marvin", "Will it never end?", "Paranoid android");

In this example, these two actions will look like this, ready to dispatch and will trigger regular reducers:

console.log(action1);
//    {
//      type: "myapp/userducks/setName",
//      newName: "Arthur Dent"
//    }


console.log(action2);
//   {
//     type: "myapp/userducks/addUser",
//     userId: 42,
//     name: "Marvin",
//     age: "Will it never end?",
//     occupation: "Paranoid android",
//  }

Hooking it up: exporting to combine reducers

If you've used redux before, this should be simple. At the bottom of users.js, export the necessary:

export default userGeese.getReducers();
export const actionCreators = userGeese.getActionCreators();

...and then use the default export in combinereducers as usual, with other duckfactories if you like:

import { combineReducers } from 'redux';

import userReducer from './users'; // <-- This is the default export from above
import gameReducer from './game';

export default combineReducers({
    game: gameReducer,
    users: userReducer
});

I haven't tried, but I can't see any reason it shouldn't work to mix reducers from duckfactories with reducers that are created in other ways, e.g. regular redux if you need to. You probably want to make sure the action names are all unique, though.

Constructor:

new DuckFactory(actionTypePrefix, initialState, actionAndReducerMap, checkAndWarn, logBuilt)
  • actionTypePrefix: a prefix string added before each action creator name, to produce the action type strings. I've used the redux-ducks suggestion - app/reducer - in the examples, but it's up to you. It should be globally unique if you use more than one duckfactory (and/or goosefactory if you use them together). If empty or missing, no prefix will be added and the actioncreator names will be used as action types - then it's up to you to ensure your action types become unique. A slash is added at the end, in line with the redux-ducks suggestion.

  • initialState: the reducer's initial state (for the state subtree that the duck should cover),

  • actionAndReducerMap: an object where the keys become the names of action creators, and the values are anonymous functions that become the corresponding reducer to the action creator. The reducer's arguments are: state as the first argument, and the second argument should be either missing (for reducers that don't require data from the action), or be an action object - that may be destructured into its keys.

  • checkAndWarn: an optional boolean (default: true) that sets whether to check the duck for consistency: are the arguments correct? Does it produce actionTypes that are globally unique? Serious errors throw Errors, less serious ones only log a warning to the console.

  • logBuilt: a last optional boolean (default: false) that sets whether to log some details about the produced action creators and actions. Handy for development, no need for it in prod.

Exposed after creation:

After creation, the resulting duck exports as JS objects:

  • .getActionCreators(): actionCreator-name → actionCreator-function, as described above.
  • .getReducer(): actionType → reducer-function
  • .getTypes(): actionCreator name → actionType. (This differs from the original redux-ducks suggestion, in that the types that are exported here don't guarantee the duck convention: app/reducer/ACTION_TYPE, since the content here depends on your choice of prefix and the keys in actionAndReducerMap.

Installation

npm install duckfactory --save

or

yarn add duckfactory

NOTE: if your app uses minification/uglification, version 1.3.0 should be okay, but don't use the versions below that. My own testing has been done with webpack 2 and yarn. If your mileage varies, please let me know.

Contributions

Suggestions, improvements, corrections, bug notifications, etc... all is welcome on github or espen42@gmail.com. Special thanks to NorwegianKiwi for awesome help!

Using it with redux-sagas

The produced actions can of course be used to trigger redux-sagas too. But duckfactory can only use reducer functions, not saga generators. Luckily, here's a sibling library that does the same thing for sagas: Goosefactory. Duckfactories and Goosefactories play nicely with each other.