Home

Awesome

Goosefactory

Simple creation and use of 'geese' - a redux-saga analogy to redux ducks.

Background

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.

I made a library for wrapping and simplifying this even more: Duckfactory. It auto-generates action creators and action types and encapsulates them, only exposing them when needed for side purposes like unit testing. This way, you, the brilliant developer, can focus on what's going on in the reducers instead of on the boilerplate wiring everything together.

What's a goose?

redux-sagas is a really handy library for handling asynchronous segments or just bundling chains of redux actions. I've used it for a while, but I have the same impression here as with general redux: most of the time actions, action creators, sagas and the take-effects that control them (takeEvery, takeLatest etc) belong together as a unit.

A goose is that tuple: {action, action creator, saga, take-effect}.

Goosefactory aims to make life as easy as possible when working with these units.

Why "goose'? Well, ducks are named after the last syllable of redux. And while goose isn't quite as similar to the last syllable in sagas, it is a bit similar to a duck.

How does it work?

Short version: give it a prefix string to group the actions, an object with the names of action creators, and the sagas they trigger, where the argument names in the saga mirror the names of the fields in the action. In return, it will create an object that exposes ordinary redux action creators, saga generators and action types:

Example

import { put, call } from 'redux-sagas';
import GooseFactory from 'goosefactory';

// Let's assume we have some resources, for example:
import { searchNamesAPI, getUserAPI, getAdminAPI } from './myAPIs'; 
import { displayNames, setCurrentUser } from './someReduxActions';

// Defining two geese in one collection called 'users':
const userGeese = new GooseFactory("myapp/usergeese", {
    listUsers: function* ({nameSearchString}) {
        const nameList = yield call(searchNamesAPI, nameSearchString);
        yield put(displayNames(nameList));
    },
    
    fetchUser: function* ({userId, isAdmin}) {
        const api = isAdmin ? getAdminAPI : getUserAPI;
        const user = yield call(api, userId);
        yield put(setCurrentUser(user));
    }
});

Note the argument syntax in the saga generator: destructuring an action argument. It's also possible to just use a single action argument instead:

	///...
	
	listUsers: function* (action) {
        const nameList = yield call(searchNamesAPI, action.nameSearchString);
        yield put(displayNames(nameList));
     },
        
    fetchUser: function* (action) {
        const api = action.isAdmin ? getAdminAPI : getUserAPI;
        const user = yield call(api, action.userId);
        yield put(setCurrentUser(user));
    }
    
    ///...

Both these syntaxes are fine (I prefer the first one).

This constructs a goose bundle with the two sagas listUsers and fetchUser. You can now directly create regular redux actions like this:


const actionCreators = userGeese.getActionCreators();

const action1 = actionCreators.listUsers("Arthur");
const action2 = actionCreators.fetchUser(42, true);

In this example, these two actions will look like this:

console.log(action1);
//    {
//      type: "myapp/usergeese/listUsers",
//      nameSearchString: "Arthur"
//    }


console.log(action2);
//   {
//     type: "myapp/usergeese/fetchUser",
//     userId: 42,
//     isAdmin: true,
//  }

Note that the fields nameSearchString, userId and isAdmin are the names defined in the constructor above (derived from the destructured arguments, or in the 'action' syntax: derived from the way subfields of action are used in the generator). For this reason, it won't allow the sagas to have action arguments named type: that name's reserved for the action.

These actions are ready to dispatch, and will trigger the sagas as in regular redux-sagas.

Hooking it up: creating a root saga

In order to use the actions like this, the sagas must be hooked up to the redux-sagas middleware in the ordinary way. To create a root saga for that, use the named export createRootSaga: it takes as argument an array of created goose collections and returns one rootSaga covering all of them.

import { createRootSaga } from 'gooseFactory';

const rootSaga = createRootSaga([userGoose, anotherGoose]);

Constructor:

new GooseFactory(actionTypePrefix, actionAndSagaMap, defaultTakeEffect, checkAndWarn, logBuilt)

After creation:

Although the goose encapsulates some moving parts such as action types, they are available as exported maps (regular JS objects):

Installation

npm install --save goosefactory

...or

yarn add goosefactory

NOTE: if your app uses minification/uglification, version 1.3.0 should be okay, but don't use the versions below. 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!