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-saga-mate

v3.0.0-alpha.1

Published

Allow you building react and redux based web apps with less pain.

Downloads

5

Readme

Redux Saga Mate

Allow you building react and redux based web apps with less pain, by removing the needs for writing lots of action types, reducers.

npm version Build Status Coverage Status npm download npm bundle size

You should know before go on reading

Layers or Moments

 ------------------------------
|   presentational components  |       ^
 -------------------------------       |
|  container components (auto) |       |
 -------------------------------       |
|    react-redux connect()     |       |
 -------------------------------       |
|       selectors (js)         |       |
 -------------------------------       |
|   redux store (state) (js)   |       |
 -------------------------------       |
|      redux reducers (js)     |       |
 -------------------------------   data flow
|  redux action payloads (js)  |       ^
 -------------------------------       |
|       normalization (js)     |       |
 -------------------------------       |
|     remote API calls (js)    |       |
 -------------------------------       |
| Web API, WebSocket Endpoints |       |
 -------------------------------       |
|         Server State         |       |
 ------------------------------
  • actions are about what happend, it's not about "what should be done", even if they were named in verbs.
  • It is reducer's job, that about "what should be done" and "how it should be done".
  • The container files you put in the "containers" directory are not actual containers, they are just connecting logics, actual containers are created automatically by connect(YourComponent), you can only see them in the browser's Developer Tools.
  • In most situations, you should try hard to prevent putting JSX codes in the container files. Because they are about the UI.
  • redux-thunk changes the origin conceptual model of the action, by functions, and functions always about "what should be done", or "how it should be done".
  • The action is not equal to action types. Action Type + Action Payload = Action Instance.
  • Tutorials or documentations of redux, redux-thunk, redux-saga, tell you track the async action state by action type, this is not what you want, in most of the time.
  • Actions you dispatch are always with payloads. Infomations in the payload affect the final call like http requests, and so the responses.
  • Track async action states in store, it also means your components are fully controlled components, the states and callbacks(handlers) are all passed as props.
  • Infomation synchronisation is the most difficult part in the computer science, normalization strategy is mean to solve this problem, even if that may not work perfectly. I hope you know how to use the normalizr library.

Demo

https://hanzhixing.github.io/redux-saga-mate/

Installation

Install the package.To use with node:

$ npm install redux-saga-mate --save

Install peer dependencies, you may already have these be installed.

npm install react redux redux-saga recompose reselect redux-actions

Recommended directory structure

src/
├── actions
│   └── types.js
├── api
│   └── index.js
├── components
│   ├── App
│   │   └── index.jsx
│   └── PostList
│       ├── index.jsx
│       └── index.module.scss
├── config.js
├── connects
│   └── PostList
│       ├── index.js
│       └── selectors.js
├── index.css
├── index.js
├── reducers
│   ├── index.js
│   └── ui
│       ├── index.js
│       └── posts.js
├── sagas
│   └── index.js
├── store
│   ├── configureStore.js
│   └── index.js
└── utils
    └── index.js

Recommended state shape

{
    session: {            <--- current session based infomations
        username: ...,
    },
    entities: {           <--- normalized entities, again, learn to use the normalizr library
        posts: {
            1: {
                ...
            }
            2: {
                ...
            }
        }
    },
    ui: {                 <--- relation infomations between the entities and the UI.
        home: {
            latestPosts: {
                ...
            }
        }
    }
    actions: {            <--- all action infomations
    }
}

Something about internal implementation

Action (enhanced FSA for async)

{
    type: 'YOUR_ACTION_TYPE',
    payload: {...any infomation as object...},
    error: true or false,
    meta: { // this infomation will be managed automatically
        id: uniq_hash(type + payload),
        pid: parentOf(id), // not used yet
        ctime: ISO8601,
        utime: ISO8601,
        phase: 'started'|'running'|'finished',
        progress: integer between 1~100
        uniq: true or false,
    }
}

Normalized payloads

Recommend normalized your api data in the API layer.

{
    request: {
        data: {...},    // for POST, PUT, PATCH body (should be plain object)
        params: {...},  // hint: react-router params
        query: {...},   // hint: querystring.parse(location.search)
    },
    response: {
        ...normalize(data, schema), // see normalizr
    }
}

Usage (Highly recommended you to read the source of demo)

actions/type.js

export const CLEANUP = 'CLEANUP';
// You need not split this to ASYNC_GET_MANY_POST_REQUEST, ASYNC_GET_MANY_POST_SUCCESS, ASYNC_GET_MANY_POST_FAILURE
export const ASYNC_GET_MANY_POST = 'ASYNC_GET_MANY_POST';

api/index.js

Normalize your data in the API layer. It's the only right place.

export const restfulGetManyPosts = args => fetch(...).then(data => normalize(data, YOUR_SCHEMA))

reducers/index.js

import {combineReducers} from 'redux';
import {concat, difference} from 'lodash/fp';
import {createActionsReducer, createEntitiesReducer, groupByComposeByEntityType} from 'redux-saga-mate/lib/reducer';
// there are only these two operations for state updating.
import {UPDATE, DELETE} from 'redux-saga-mate/lib/operation';
import * as ActionTypes from '../actions/types'; // It's ok, if you want to import action types explicitly.

// The keys is your entities keys in the store.
const EntityActionMap = {
    posts: {
        // the value part can be one single OPERATION(string), or tuple [OPERATION, yourMergeFunction]
        [ActionTypes.ASYNC_GET_MANY_POST]: [
            UPDATE,
            // @see the 'mergeDeepWith' from 'ramda'
            (k, l, r) => (k === 'commenters' ? concat(l, difference(r, l)) : r),
        ],
        [ActionTypes.ASYNC_DELETE_ONE_POST]: DELETE,
        [ActionTypes.ASYNC_PATCH_ONE_POST]: UPDATE,
        ...
    },
    users: {
        ...
    },
    ...
    // add your mapping rules instead of writing reducers
};

const locators = {
    // define possible paths to entities in your action payload
    UPDATE: [
        ['response', 'entities'],
        ['entities'],
        ['entities'],
    ],
    // paths to primaryKey in your action payload, which will be used to delete the entity
    DELETE: [
        ['request', 'params', 'id'],
    ],
};

export default combineReducers({
    // tuple [ACTION_TYPE_FOR_CLEANUP, YOUR_ASYNC_ACTION_TYPE_REGEX]
    actions: createActionsReducer([ActionTypes.CLEANUP, /^ASYNC_/]),
    entities: combineReducers(
        groupByComposeByEntityType(
            createEntitiesReducer(locators, EntityActionMap),
            {
               ...
               /// put your own legacy reducers here, they will executed at the end of reducing
               ...
            },
        ),
    ),
    ...
    // If you are creating new app, codes above can be written like bellow
    entities: combineReducers(createEntitiesReducer(locators, EntityActionMap)),
    ...
});

sagas/index.js

import {all, takeEvery} from 'redux-saga/effects';
import {makeCreateDefaultWorker} from 'redux-saga-mate/lib/saga';
import * as ActionTypes from '../actions/types';
import * as Api from '../api';

// you need to tell the Error Type for failure situation of the async action.
const createDefaultWorker = makeCreateDefaultWorker([MyError, ActionTypes.CLEANUP]);

// If you want to clear action state when success, you pass option object as the second argument.
// const createDefaultWorker = makeCreateDefaultWorker([MyError, ActionTypes.CLEANUP], {autoclear: true});

// Notice!
// If you need more complicated logic controls then the default worker saga,
// you need to implement your own worker sagas.
export default function* () {
    yield all([
        // create a worker saga with your remote call promise, you need only one line code.
        takeEvery(ActionTypes.ASYNC_GET_MANY_POST, createDefaultWorker(Api.restfulGetManyPosts)),
        // If you need infomations from state, before run the promise, you can prepare the payload.
        // What you return will pass in to the remote call.
        takeEvery(ActionTypes.ASYNC_GET_ONE_USER_BY_POST_ID, createDefaultWorker(
            Api.getOneUser,
            (state, action) => {
                const {postId} = action.payload;
                const {author} = state.entities.posts[postId];
                return {id: author};
            },
            // If you want to disable action state autoclearing just for this worker
            // {autoclear: false}
        )),
    ]);
}

connects/PostList/index.js (or containers/PostList/index.js)

imports

import {connect} from 'react-redux';
import {compose, lifecycle, withState, mapProps} from 'recompose';
import {createSelector} from 'reselect';
import {createAction} from 'redux-actions';
import {createAsyncAction, idOfAction} from 'redux-saga-mate/lib/action';
import {
    // You can use this,
    withAsyncActionStateHandler,
    // or this.
    createAsyncActionContext,
    // How they are different from each other, go on reading to the end.
} from 'redux-saga-mate/lib/hoc';
import {createSelectActions} from 'redux-saga-mate/lib/selector';
import PostList from '../../components/PostList';
import {selectPosts, selectPostsBuffer, selectModalAuthor} from './selectors';
import * as ActionTypes from '../../actions/types';

mapStateToProps

// The selector below is the same as the selector you got from reselect's createSelector.
const selectActions = createSelectActions(
    (state, props) => state.actions, // provide actions selector from store
    (state, props) => props.actionIds, // provide actionIds selector maybe from props
);

const makeSelectProps = () => createSelector(
    selectPosts,
    // Once your component is wrapped with 'withAsyncActionStateHandler', you can select out the actions.
    // So as when you wrapped with 'withAsyncActionContextConsumer' created by 'createAsyncActionContext'.
    selectActions,
    (items, transients) => ({
        items: posts,
        transients, // in the ui component, you can examine the action by 'transients.onPage[page]'
        ...
    }),
);

const makeMapStateToProps = () => {
    const selectProps = makeSelectProps();
    return (state, props) => selectProps(state, props);
};

mapDispatchToProps

const mapDispatchToProps = (dispatch, {onTrackAsyncAction}) => ({
    onPage: page => {
        // 1. Make your action Async with 'createAsyncAction'.
        // 2. dispatch it.
        // 3. take the action id with 'idOfAction'
        const action = dispatch(createAsyncAction(ActionTypes.ASYNC_GET_MANY_POST)({
            page,
        }));
        // you can pass single string, or path in array form for the first argument
        // Seconds is the Action Id.
        onTrackAsyncAction(['onPage', page], idOfAction(action));
    },
});

const withRedux = connect(makeMapStateToProps, mapDispatchToProps);

export default compose(
    ...
    withRedux,
    ...
)(PostList);

enhance with aysnc action tracking

You have two options.

Option1

Use withAsyncActionStateHandler

const withAsyncAction = withAsyncActionStateHandler(({actionIds, setActionId, unsetActionId}) => ({
    actionIds,
    onTrackAsyncAction: setActionId,
    onUntrackAsyncAction: unsetActionId,
}));

export default compose(
    ...
    withAsyncAction,
    ...
    withRedux,
    ...
)(PostList);
Option2 Use createAsyncActionContext
// You may want to create these two hoc from a seperated file and import the provider or consumer.
// The benefit use context is you need not pass the props along the tree.
const {withAsyncActionContextProvider, withAsyncActionContextConsumer} = createAsyncActionContext();

export default compose(
    ...
    withAsyncActionContextProvider,
    ...
    withAsyncActionContextConsumer,
    mapProps(({actionIds, setActionId, unsetActionId}) => ({ // It is just recompose's mapProps
        actionIds, // off course the 'actionIds' must be matched with the key in the action selector: selectActions
        onTrackAsyncAction: setActionId, // You can map the props like this.
        onUntrackAsyncAction: unsetActionId,
    }))
    withRedux,
    ...
)(PostList);

Use different prop names

const mapActionProps = ({actionIds, setActionId, unsetActionId}) => ({
    actionIds, // off course the 'actionIds' must be matched with the key in the action selector: selectActions
    onTrackAsyncAction: setActionId, // You can map the props like this.
    onUntrackAsyncAction: unsetActionId,
})

export default compose(
    ...
    withAsyncActionContextConsumer,
    mapProps(mapActionProps), // It is just recompose's mapProps, you can use withProps or mapProps.
    withRedux,
    ...
)(PostList);