0.1.1 • Published 6 years ago

redux-action-chain v0.1.1

Weekly downloads
4
License
MIT
Repository
github
Last release
6 years ago

redux-action-chain npm version Build Status

redux-action-chain is a middleware that aims to easily manage side effects of application.

new ActionChain()
    .chain(pingAction, pongAction)
    .chain(userRequest, async ({ userId }) => userFetched(await fetchUser(userId)))
    .chain(userFetched, postsRequest)
    .chain(postsRequest, attach(async (action, { dispatch, getState }) => {
        const { post } = getState();
        if (post.isFetched) return;

        dispatch(postsFetchStart())
        try {
            const posts = await fetchPosts()
            dispatch(postsFetchSuccess(posts));
        } catch (e) {
            dispatch(postsFetchFailed(e));
        }
    }));

Comparison

Compared to redux-thunk / redux-promise

React component can concentrate on dispatching actions such as events and user interactions without knowing what ActionCreator is doing.

  • redux-thunk/redux-promise
React Component --> (pure action) ActionCreator --> Reducer
React Component --> (async logic  ) ActionCreator
React Component --> (other side effects) ActionCreator
  • redux-action-chain
React Component --> (pure action) ActionCreator --> Reducer

(pure action) ActionCreator  <-- ActionChain (async logic / other side effects)

Compared to redux-saga / redux-observable

redux-action-chain provides only two features.

  • chaining actions. dispatch action after another action finished.
  • simple dispatch based flow control (like thunk)

It is simple and intuitive, suitable for creating small to medium scale applications.

Basic example

new ActionChain()
    .chain("PING", () => ({ type: "PONG" }))
    .chain("USER_REQUEST", (payload) => fetchUser(payload.userId)
                                       .then(user => ({ type: "USER_FETCHED", payload: user })))
    .chain("USER_FETCHED", () => ({ type: "POSTS_REQUEST" }))
    .chain("POSTS_REQUEST", attach(async (action, { dispatch, getState }) => {
        const { post } = getState();
        if (post.isFetched) return;

        dispatch({ type: "POSTS_FETCH_START" })
        try {
            const posts = await fetchPosts()
            dispatch({ type: "POSTS_FETCH_SUCCEEDED", user: user });
        } catch (e) {
            dispatch({ type: "POSTS_FETCH_FAILED", message: e.message });
        }
    }));
  • for easy understanding. not recommended. please use FSA style.

FSA (Flux Standard Action) Style

If you adopt FSA using redux-actions. it can be made simply.

//after
new ActionChain()
    .chain(userRequestAction, userFetchAction )

//before
new ActionChain()
   .chain("USER_REQUEST", (payload, action)=> {type:"USER_FETCH", payload});
//same
new ActionChain()
    .chain(userRequestAction.toString(), (payload, action) => userFetchAction(payload))

TypeScript FSA

using TypeScript FSA allow you to write type-safe.

new ActionChain()
    .chain(userRequestAction, userFetchAction )

Install

npm install --save redux-action-chain

Usage

"combine" and "applyMiddleware". Like "reducer".

import { ActionChain, attach, combineActionChains, createActionChainMiddleware } from "redux-action-chain";

//pingpong.js
const chainPingPong = new ActionChain()
    .chain("PING", () => ({ type: "PONG" }));

//user.js
const chainUser = new ActionChain()
    .chain("REQUEST_USER", async (payload) => fetchUser(payload.userId))
    .chain("FETCH_USER_SUCCESS", () => fetchBlogPosts())
    .chain("FETCH_USER_SUCCESS", () => ({ type: "PING" }));


//index.js
export const rootActionChain = combineActionChains(chainPingPong, chainUser);


//store.js
import { createActionChainMiddleware } from "redux-action-chain";

const actionChainMiddleware = createActionChainMiddleware(rootActionChain);

const store = createStore(
    reducer,
    applyMiddleware(actionChainMiddleware)
)


// component.js
class UserComponent extends React.Component {
    ...
    onSomeButtonClicked() {
        const { userId, dispatch } = this.props
        dispatch({ type: 'REQUEST_USER', payload: { userId } })
    }
    ...
}

Recipes

basic

new ActionChain()
    .chain("PING", () => ({ type: "PONG" }))
    .chain("REQUEST_USER", (payload, action) => {
        const userId = payload.userId;
        return { type: "USER_ID", payload: { userId } }
    });

new ActionChain()
    .chain(requestUser, (payload) =>  userId(payload.userId))
    .chain(requestUser, fetchUser)

async

new ActionChain()
    .chain("REQUEST_USER", async (payload, action) => {
        const { userId } = payload;
        const user = await fetchUser(userId)
        return { type: "USER_FETCHED", payload: { user } }
    })

new ActionChain()
    .chain(requestUser, async ({userId}) => userFetched(await fetchUser(userId))

attach

new ActionChain()
    .chain("REQUEST_USER", attach(async (action, { dispatch, getState }) => {
        const { userId } = action.payload;
        const { userState } = getState();
        if (userState.isFetched) return;

        dispatch({ type: "FLOW_START" })
        try {
            const user = await fetchUser(userId);
            dispatch({ type: "USER_FETCH_SUCCEEDED", payload: { user } });
        } catch (e) {
            dispatch({ type: "USER_FETCH_FAILED", message: e.message });
        }
    }));


new ActionChain()
    .chain(requestUser, attach(async (action, { dispatch, getState }) => {
        const { userId } = action.payload;
        const { userState } = getState();
        if (userState.isFetched) return;

        dispatch(flowStart())
        try {
            const user = await fetchUser(userId);
            dispatch(userFetchSuccess(user));
        } catch (e) {
            dispatch(userFetchError(e));
        }
    }));

throttle/sleep

// throttle
import _ from 'lodash'

const throttledUpdateSearchWord = _.throttle(100, (action, { dispatch }) =>{ dispatch(updateSearchWord()); });
new ActionChain()
    .chain(changeInput, attach(throttledUpdateSearchWord))
    .chain(updateSearchWord, searchQuery);


// sleep
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
new ActionChain()
    .chain(pingAction, () => sleep(1000).then(pongAction));

For Test

chainPingPong = new ActionChain();
  .chain(...)

chainPingPong.get(action) => Array<HandlerObject>
chainPingPong.handle(action, {dispatch, getState}) => Array<Action> //next action
chainPingPong.dispatch(action, {dispatch, getState}) =>  dispatch called

//async
await chainPingPong.dispatch(action, {dispatch, getState};