GithubHelp home page GithubHelp logo

redux-pack's Introduction

Redux Pack

Sensible promise handling and middleware for redux

redux-pack is a library that introduces promise-based middleware that allows async actions based on the lifecycle of a promise to be declarative.

Async actions in redux are often done using redux-thunk or other middlewares. The problem with this approach is that it makes it too easy to use dispatch sequentially, and dispatch multiple "actions" as the result of the same interaction/event, where they probably should have just been a single action dispatch.

This can be problematic because we are treating several dispatches as all part of a single transaction, but in reality, each dispatch causes a separate rerender of the entire component tree, where we not only pay a huge performance penalty, but also risk the redux store being in an inconsistent state.

redux-pack helps prevent us from making these mistakes, as it doesn't give us the power of a dispatch function, but allows us to do all of the things we were doing before.

To give you some more context into the changes, here are some examples/information about the old way and new way of doing things below:

Ready to use it? Jump straight to the How-To and API doc

Data Fetching with redux-thunk (old way)

Before this change, you would create individual action constants for each lifecycle of the promise, and use redux-thunk to dispatch before the promise, and when it resolves/rejects.

// types.js
export const LOAD_FOO_STARTED = 'LOAD_FOO_STARTED';
export const LOAD_FOO_SUCCESS = 'LOAD_FOO_SUCCESS';
export const LOAD_FOO_FAILED = 'LOAD_FOO_FAILED';
// actions.js
export function loadFoo(id) {
  return dispatch => {
    dispatch({ type: LOAD_FOO_STARTED, payload: id });
    return Api.getFoo(id).then(foo => {
      dispatch({ type: LOAD_FOO_SUCCESS, payload: foo });
    }).catch(error => {
      dispatch({ type: LOAD_FOO_FAILED, error: true, payload: error });
    });
  };
}

In the reducer, you would handle each action individually in your reducer:

// reducer.js
export function fooReducer(state = initialState, action) {
  const { type, payload } = action;
  switch (type) {
    case LOAD_FOO_STARTED:
      return {
        ...state,
        isLoading: true,
        fooError: null
      };
    case LOAD_FOO_SUCCESS:
      return {
        ...state,
        isLoading: false,
        foo: payload
      };
    case LOAD_FOO_FAILED:
      return {
        ...state,
        isLoading: false,
        fooError: payload
      };
    default:
      return state;
  }
}

Note: The example uses { ...state } syntax that is called Object rest spread properties. If you'd prefer the API of Immutable.js, you could write code like the following:

switch (type) {
  case LOAD_FOO_STARTED:
    return state
      .set('isLoading', true)
      .set('fooError', null);
  case LOAD_FOO_SUCCESS:
    // ...
}

Data fetching with redux-pack (new way)

With redux-pack, we only need to define a single action constant for the entire promise lifecycle, and then return the promise directly with a promise namespace specified:

// types.js
export const LOAD_FOO = 'LOAD_FOO';
// actions.js
export function loadFoo(id) {
  return {
    type: LOAD_FOO,
    promise: Api.getFoo(id),
  };
}

In the reducer, you handle the action with redux-pack's handle function, where you can specify several smaller "reducer" functions for each lifecycle. finish is called for both resolving/rejecting, start is called at the beginning, success is called on resolve, failure is called on reject, and always is called for all of them.

// reducer.js
import { handle } from 'redux-pack';

export function fooReducer(state = initialState, action) {
  const { type, payload } = action;
  switch (type) {
    case LOAD_FOO:
      return handle(state, action, {
        start: prevState => ({
          ...prevState,
          isLoading: true,
          fooError: null
        }),
        finish: prevState => ({ ...prevState, isLoading: false }),
        failure: prevState => ({ ...prevState, fooError: payload }),
        success: prevState => ({ ...prevState, foo: payload }),
      });
    default:
      return state;
  }
}

Logging (before/after)

Often times we want to log whether an action succeeded or not etc. We are able to handle this now using the onSuccess or onFailure meta options:

Before:

// actions.js
export function loadFoo(id) {
  return dispatch => {
    dispatch(loadFooStart());
    Api.getFoo(id).then(response => {
      dispatch(loadFooSucceeded(response);
      logSuccess(response);
    }).catch(error => dispatch(loadFooFailed(error)));
  };
}

After:

// actions.js
export function loadFoo(id) {
  return {
    type: LOAD_FOO,
    promise: Api.getFoo(id),
    meta: {
      onSuccess: (response) => logSuccess(response)
    },
  };
}

How to

Install

The first step is to add redux-pack in your project

npm install -S redux-pack

# or

yarn add redux-pack

Setup the middleware

The redux-pack middleware is the heart of redux-pack. As the following example shows, it installs like most middlewares:

import { createStore, applyMiddleware } from 'redux'
import { middleware as reduxPackMiddleware } from 'redux-pack'
import thunk from 'redux-thunk'
import createLogger from 'redux-logger'
import rootReducer from './reducer'

const logger = createLogger()
const store = createStore(
  rootReducer,
  applyMiddleware(thunk, reduxPackMiddleware, logger)
)

Note that it should probably be one of the first middleware to run, here it would run just after thunk and before logger.

Using the handle() helper

Let's start with the function signature: handle(state, action, handlers) โ†’ newState

As you can see, it takes 3 arguments:

  1. state: the current state in your reducer
  2. action: the action that should be handled
  3. handlers: a object mapping the promise lifecycle steps to reducer functions
  • the steps names are: start, finish, failure, success and always
  • every handler function should be of the form: state => state

Here is a minimalist example:

import { handle } from 'redux-pack';
import { getFoo } from '../api/foo';

const LOAD_FOO = 'LOAD_FOO';
const initialState = {
  isLoading: false,
  error: null,
  foo: null,
};

export function fooReducer(state = initialState, action) {
  const { type, payload } = action;
  switch (type) {
    case LOAD_FOO:
      return handle(state, action, {
        start: prevState => ({ ...prevState, isLoading: true, error: null, foo: null }),
        finish: prevState => ({ ...prevState, isLoading: false }),
        failure: prevState => ({ ...prevState, error: payload }),
        success: prevState => ({ ...prevState, foo: payload }),
        always: prevState => prevState, // unnecessary, for the sake of example
      });
    default:
      return state;
  }
}

export function loadFoo() {
  return {
    type: LOAD_FOO,
    promise: getFoo(),
  }
}

Note: The example uses { ...state } syntax that is called Object rest spread properties.

Adding side-effects with event hooks

You might want to add side effects (like sending analytics events or navigate to different views) based on promise results.

redux-pack lets you do that through event hooks functions. These are functions attached to the meta attribute of the original action. They are called with two parameters:

  1. the matching step payload (varies based on the step, details below)
  2. the getState function

Here are the available hooks and their associated payload:

  • onStart, called with the initial action payload value
  • onFinish, called with true if the promise resolved, false otherwise
  • onSuccess, called with the promise resolution value
  • onFailure, called with the promise error

Here is an example usage to send analytics event when the user doesFoo:

import { sendAnalytics } from '../analytics';
import { doFoo } from '../api/foo';

export function userDoesFoo() {
  return {
    type: DO_FOO,
    promise: doFoo(),
    meta: {
      onSuccess: (result, getState) => {
        const userId = getState().currentUser.id;
        const fooId = result.id;
        sendAnalytics('USER_DID_FOO', {
          userId,
          fooId,
        });
      }
    }
  }
}

Testing

At the moment, testing reducers and action creators with redux-pack does require understanding a little bit about its implementation. The handle method uses a special KEY.LIFECYCLE property on the meta object on the action that denotes the lifecycle of the promise being handled.

Right now it is suggested to make a simple helper method to make testing easier. Simple test code might look something like this:

import { LIFECYCLE, KEY } from 'redux-pack';
import FooReducer from '../path/to/FooReducer';

// this utility method will make an action that redux pack understands
function makePackAction(lifecycle, { type, payload, meta={} }) {
  return {
    type,
    payload,
    meta: {
      ...meta,
      [KEY.LIFECYCLE]: lifecycle,
    },
  }
}

// your test code would then look something like...
const initialState = { ... };
const expectedEndState = { ... };
const action = makePackAction(LIFECYCLE.START, { type: 'FOO', payload: { ... } });
const endState = FooReducer(initialState, action);
assertDeepEqual(endState, expectedEndState);

redux-pack's People

Contributors

am-niceday avatar andrewmclagan avatar arnaudrinquin avatar hshoff avatar jdreesen avatar lelandrichardson avatar lencioni avatar marr avatar matheusrocha89 avatar mqklin avatar supasate avatar valscion avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

redux-pack's Issues

Pass in payload for start handler

Is it possible to pass in a payload to be used in the start handler? My use case is I have a function that does a fetch for a specific item in a list, and I want it to display that particular item as loading in the list, so start needs to know which item was passed in. I can achieve this by creating a synchronous action and dispatch it alongside the async one, but this seems to add some bloat to the set of actions; is there a better way? Thanks!

Question about multiple dispatches / renders

This can be problematic because we are treating several dispatches as all part of a single transaction, but in reality, each dispatch causes a separate rerender of the entire component tree, where we not only pay a huge performance penalty, but also risk the redux store being in an inconsistent state.

I see redux-pack does 3 dispatches in the middleware
https://github.com/lelandrichardson/redux-pack/blob/master/src/middleware.js#L28
https://github.com/lelandrichardson/redux-pack/blob/master/src/middleware.js#L40
https://github.com/lelandrichardson/redux-pack/blob/master/src/middleware.js#L55

I don't understand why redux-pack will not cause separate rerenders for each lifecycle stage (start, success, failure).

P.S. I was about to submit this when I thought I could try it out. I created this example project: https://github.com/saltycrane/redux-pack-example After dispatching an action, I see 2 renders when using redux-thunk and 2 renders when using redux-pack. This makes me think redux-pack does not address the issues in the above quote. Please let me know if I'm misunderstanding.

Add dist to npm

Seems like all the other redux packages we have installed have their dist folder included, when installing with npm. Could we also provide that here please?

Linting requires eslint-plugin-jsx-a11y, not in package.json

Forked the repo. Ran:

npm install

then:

npm run test

Got an error that boils down to:

ESLint couldn't find the plugin "eslint-plugin-jsx-a11y".

My guess is that you have it installed globally on your machine, as I didn't see it in the package.json. Thought this might be something you wanted to add as a devDependency.

Failed to compile project when importing redux-pack

Node version: v7.2.0
Project created via create-react-app with react-scripts v0.7.0

I've tried to install with both npm install -S redux-pack and yarn add redux-pack, but when I'm importing redux-pack into project:
import { middleware as reduxPackMiddleware } from 'redux-pack';

I'm receiving this error:

Failed to compile.

Error in ./~/redux-pack/lib/handle.js
Module not found: 'deline' in [path to project]/node_modules/redux-pack/lib

 @ ./~/redux-pack/lib/handle.js 1:1303-1320

I've already tried manually install 'deline' module and it appears in 'node_modules' folder, but haven't succeed.

Can someone please share any thoughts on possible solutions and I will try to fix it?

Willing to take a PR to add TypeScript types?

This project looks very interesting, and I'd like to try it out with TypeScript. However, there are currently no official types available.

There are two ways to do this.

  1. Publish types to DefinitelyTyped.
  2. Include types with this package.

Including types with this package would involve adding a types directory and modifying the package.json.

Would you take a PR adding types to this package?

Default handling on hooks

Would it be possible to add a possibility to configure the middleware?

Let's say I want to send user to the login page every time I get 401 Unauthorized from the response. I have to do this in every single actions now, but with the default onFailure hook I could do this just once.

[Question] Caching / Aborting actions based on state

So a common pattern in redux-thunk land is to do something like:

// pseudo code

const loadListingsWithFilter = (filter) => (dispatch, getState) => {
   // bailout early if the data is fresh
   if (getTimeLastFetchedByFilter(getState(), filter) < 15 minutes) {
       return;
   }
   // normal thunk stuff
   ...
}

How does this work with redux-pack?

Thunk promise not returning a value?

I'm trying to get the thunk promise example working, but I'm unable to get a result back from the promise. What am I missing?

The following test demonstrates my issue:

/* 
 * mocha; chai-expect defined in global scope
 */

import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import {middleware as packMiddewlare} from 'redux-pack';

const middlewares = [
  thunk,
  packMiddewlare,
];

const createStore = configureStore(middlewares);

const action = dispatch => dispatch({
  type: 'FOO',
  promise: new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('done');
    }, 500);
  })
})


describe('Pack Promises', function () {
  it('never gets the promise value', () => {
    const store = createStore();
    return store.dispatch(action).then(result => {
      /** What should the result be here??? **/
      expect(result).to.be.ok;
    })
  });
});

Use-case of having the previous state in handlers params

Hi,

The handler functions provided to handle always take the previous state as their only parameter. I feel that I might be misunderstanding something here, can't I use the state from the reducer's first parameter instead?

Thanks!

Actions Are Called Twice

Very cool library, I like the approach. Thanks for building and OSing it!

I've swapped in redux-pack for redux-thunk and rewritten an action called getBroadcasts from this redux-thunk action:

export function getBroadcasts() {
  return async (dispatch) => {
    dispatch({ type: 'GET_BROADCASTS_INITIATED' });

    try {
      const { data: { results } } = await api.get('/broadcasts');
      dispatch({ type: 'GET_BROADCASTS_SUCCEEDED', payload: results });
    } catch (error) {
      dispatch({ type: 'GET_BROADCASTS_FAILED', payload: error });
    }
  };
}

to this redux-pack action:

export function getBroadcasts() {
  return {
    type: 'GET_BROADCASTS',
    promise: api.get('/broadcasts')
  }
}

What I observe in the console when the relevant page loads is that while the redux-thunk action was called only once:

thunk

the redux-pack action is called twice, and returns once with an undefined payload, and once with a successful payload:

pack

I assume this is expected behavior, I just wanted to confirm.

Actions should be FSA-complient

While not critical, I think it'd be better if actions were FSA (flux-standard-action) compliant.

Instead of dispatching actions such as:

{
  type: LOAD_FOO,
  promise: Api.getFoo(id),
};

I suggest the actions to be on the form of either:

{
  type: LOAD_FOO,
  payload: Api.getFoo(id),
};

or

{
  type: LOAD_FOO,
  meta: {
    promise: Api.getFoo(id),
  },
};

The changes required on the middleware would be small and back-compatibility could be provided.

How does the library avoid triggering multiple dispatches?

Hi,

I was wondering how redux-pack avoids multiple dispatches / re-renders on async actions.. In the read me comparison with redux-thunk it says

This can be problematic because we are treating several dispatches as all part of a single transaction, but in reality, each dispatch causes a separate rerender of the entire component tree, where we not only pay a huge performance penalty, but also risk the redux store being in an inconsistent state.

How is that the case if its still needs to update the state in the different lifecycles of the promise allowing the components to update accordingly as shown in the example code ?

case LOAD_FOO:
return handle(state, action, {
start: prevState => ({
...prevState,
isLoading: true,
fooError: null
}),
finish: prevState => ({ ...prevState, isLoading: false }),
failure: prevState => ({ ...prevState, fooError: payload }),
success: prevState => ({ ...prevState, foo: payload }),
});

Thanks

Couldn't find preset "airbnb"

I don't use babel-preset-airbnb in my current project.

Seem your dependencies deline required airbnb preset.
Hum...
I got this error.
screen shot 2017-03-16 at 12 00 12 pm

I don't know where should i raise issue (this one or deline one)?

I resolve this issue by:

  • yarn add --dev babel-preset-airbnb (or npm install --save-dev babel-preset-airbnb)
  • add airbnb to presets array in .babelrc

No way to get the promise result in 'success'

// actions.js
export function loadFoo(id) {
  return {
    type: LOAD_FOO,
    promise: Api.getFoo(id),
  };
}
// reducer.js
import { handle } from 'redux-pack';

export function fooReducer(state = initialState, action) {
  const { type, payload } = action;
  switch (type) {
    case LOAD_FOO:
      return handle(state, action, {
        start: prevState => ({
          ...prevState,
          isLoading: true,
          fooError: null
        }),
        finish: prevState => ({ ...prevState, isLoading: false }),
        failure: prevState => ({ ...prevState, fooError: payload }),
        success: prevState => ({ ...prevState, foo: payload }),
      });
    default:
      return state;
  }
}

So, how to use/get the result of Api.getFoo(id) within the reducer?

Inconsistent usage of state and action variables in handler

At first thank you for your work I really like the interface you've built to handle actions in reducers.

I have a proposal to improve it. Here is an example of handler usage:

// reducer.js
import { handle } from 'redux-pack';

export function fooReducer(state = initialState, action) {
  const { type, payload } = action;
  switch (type) {
    case LOAD_FOO:
      return handle(state, action, {
        start: s => ({
          ...s,
          isLoading: true,
          fooError: null
        }),
        finish: s => ({ ...s, isLoading: false }),
        failure: s => ({ ...s, fooError: payload }),
        success: s => ({ ...s, foo: payload }),
      });
    default:
      return state;
  }
}

Here state is provided as handler parameter and action is taken from closure. I think the usage of this variables should be consistent either via handler params or from closure.

  1. Handler params:
...
success: (s, a) => ({ ...s, foo: a.payload }),
...
  1. Closure:
reducer(state = initialState, action) {
  ...
  success: () => ({ ...state, foo: action.payload }),
}

I like first variant more for a few reasons:

  1. It doesn't break current api.
  2. We will be able to extract handler functions and reuse across one or more reducers.

Please let me know about your opinion and I will create a pull request with the change.

A typo in README

There is the following typo in README.md:

The handle method uses a special KEY.LIFECICLE property on the meta object

"Failed building JavaScript bundle" when importing redux-pack

Hi!

I just created a new app using expo and the packager fails to build when I import redux-pack.

Steps to reproduce:

  • npx create-react-native-app my-app && cd my-app
  • yarn add redux-pack
  • edit App.js, add import { middleware } from 'redux-pack';
  • yarn start
  • launch the app with the expo client

Environment:

software version
react 16.3.0-alpha.1
react-native 0.54.0
expo ^26.0.0
redux-pack ^0.1.5

Only tested on Android

error being thrown by redux-pack when using react-extension-redux

I'm running into a problem using redux-pack within a chrome extension background file, which is throwing the You used redux-pack's handle(...) function on the action ACTION, however, it doesn't appear to be an action that was dispatched by redux-pack. error. I usually have seen this when I don't have a promise key on the action, but in this case it's there.

I tried looking through the source to see why the error was happening, but can't find the source. When I put a console.log on the params being passed into handle I get the following objects:

startingState { ...myState }
action { promise: Object, type: "ACTION_NAME", _sender: Object }
handlers success: function success(prevState)

startingState and handlers look normal to me here, but there might be a problem with the action which is triggering the error. promise and type seem OK to me, but the _sender contains values about my extension, and looks like it's tacked onto the action by the react-extension-redux wrapStore function, which allows me to run these actions through the extension background.

Would this cause a problem in redux-pack and cause it to throw this error in the handle function, and is there a way to get around it?

Misleading ReadMe

"This can be problematic because we are treating several dispatches as all part of a single transaction, but in reality, each dispatch causes a separate rerender of the entire component tree, where we not only pay a huge performance penalty, but also risk the redux store being in an inconsistent state.

redux-pack helps prevent us from making these mistakes, as it doesn't give us the power of a dispatch function, but allows us to do all of the things we were doing before."

This statement builds a notion in my mind that by using this library, I will have less rerenders of the component tree, which is not true, in an async action atleast two rerenders will happen, once for loading state being set to true, another when loading is done and data is available.

Each dispatch doesn't cause rerenders, change in app state does, which happens in this library's case also.

Although I enjoy the less boilerplate, more simple and readable code made possible by this library, but I don't see how it optimizes the process.

Examples of how to test reducer

Could you perhaps provide an example of how you test the reducer for actions that are created with redux-pack? Do you have to create an instance of the actual store and dispatch it as you would in the app?

[Question] How to organize complex scenarios

How would you organize complex scenarios involving multiple async stuff and multiple action types?

With redux-saga its superb easy once one gets comfortable with generators and with redux-observable for a skillful observable user it should be quite easy too (although didnt use the latter personally).

Wondering how to orchestrating smth more complex would look like with the proposed approach of those self-contained descriptive actions in redux-pack.

Would be cool if such example be in the docs.

Nice lib nonetheless!

module not found

How to import the middleware via nom ?
looks like there is an issue in your package.json file at

"main": "lib/index.js"

dispatch action in side effect hooks

Adding side-effects with event hooks

...

redux-pack lets you do that through event hooks functions. These are functions attached to the meta attribute of the original action. They are called with two parameters:

  1. the matching step payload (varies based on the step, details below)
  2. the getState function

It would be nice to be able to dispatch an action inside the side effect handlers. Is there a good reason for not passing dispatch along with getState here? Thanks.

dispatch the same action type

about this code:

https://github.com/lelandrichardson/redux-pack/blob/master/src/middleware.js#L41

I think it is not friendly to other middleware.
consider about this case.
I try to storage the response data after user click login.
here's a middleware

export const storageMiddleware = store => next => action => {
    switch (action.type) {
        case USER_ACTION.LOGIN: filterPack(action, () => storage(STORAGE_KEY.USER, action)); break;
        case USER_ACTION.LOGOUT: clear(STORAGE_KEY.USER); break;
        default: ;
    }
    return next(action)
}

It is confuse about filterPack
we get 2 actions which type = LOGIN but actually different.
so I have to write these code to judge request success or not.

export const filterPack = (action, callback, status = LIFECYCLE.SUCCESS) => {
    const { meta } = action;
    const lifecycle = meta ? meta[KEY.LIFECYCLE] : null;
    if (lifecycle === status) {
        return callback(meta);
    }
    return action;
}

It should be part of this repo and It seems like how your handle works in reducer.
So i guess you miss something here.

In fact, i hope it will be auto splited into ${action.type}_${lifecycle.toUpperCase()} and config it in createMiddleware

(thanks for reading :> )

Documentation request: how are exceptions handled?

From regular promise handling, I'm used to exceptions in the then handler being caught and handled in the catch handler. Does this also work with the success callback of redux-pack's handle() function? In other words, if I throw an exception in the success callback, will it be caught by the failure callback?

Thanks!

Conflicts with react-redux-toastr

Disclaimer: I don't fully understand all the in's & out's of npm modules just FYI.

When using redux-pack in a current project I added the react-redux-toatr package and was met with the following error message on the next hot load from webpack-dev-server:

missing-index

Did a little investigation and it seems that both packages run on different versions of uuid and that is what causes the conflict. I don't know if they are peer-dependencies or dependencies not sure how that all works. Below are a few screenshots of installing both packages and what happens to the uuid directory:

pack-install
uuid-pack

I have looked into the incorrect peer-dependency and it did not make a difference.

toastr-install
uuid-toatsr

I've never run into an issue like this before. Do I open the issue with the react-redux-toastr dev-team as well?

[Question] Is redux-pack project dead?

Hey @lelandrichardson redux-pack seems pretty cool, I see that there hasn't been any commits since 2017.

Some things in PR that's stagnant, though there doesn't seem to be many backlog issues. ๐Ÿ‘

Was curious about the general health and enthusiasm of this repo? Thanks!

How to test reducers that use `handle`?

I love the idea behind this library and am tempted to adopt it, but I don't see a good way to test reducers that use handle.

With thunk or other promise middleware, I can dispatch a manually created action with a type of LOAD_FOO_STARTED or LOAD_FOO_FAILED and test against that. But here, the structure of the action is an internal implementation detail that I can't get at from my tests.

Any thoughts on how to achieve this?

One idea is to extract the functions that create the various actions and expose them for use in tests.

Something like:

function makeStartAction({ type, payload, meta }) {
  return {
    type,
    payload,
    meta: {
      ...meta,
      [KEY.LIFECYCLE]: LIFECYCLE.START,
      [KEY.TRANSACTION]: transactionId,
    },
  }
}

etc.

It looks like KEY and LIFECYCLE are public exports of the package, so I could make my own utilities that do this, but that seems hard to maintain.

Isn't violating this another redux pattern of how to use the reducer?

I thought that one of the big things that redux points out is that you are not doing any logic in your reducer. With adding now handle into your reducer you all the sudden have "complex" calculation logic. E.g. how to handle errors, how to convert error messages, how to map responses, etc.

Another big open question to me would be how to handle data normalisation as this seems to be the way to go for bigger and more complex applications.

Looking forward to hear your thoughts on this.

Recommended way to debounce

I am looking for an recommendation on how to implement a debounce with redux-pack.

I have tried working with another middleware called redux-debounced, but I don't think it's working quite as expected.

It implements a debounce by adding the following meta to the action object:

meta: {
  debounce: {
    time: 300
  }
}

But, since the Promise is created at Action object creation time, the async call is started despite the debounce.

Any thoughts?

[Question] Refactoring AuthReducer & actions with redux-pack

Hello, very impressed with this library just trying to wrap my head around how to refactor some code I have for client-side auth.

BEFORE

actions.js

import axios from 'axios';
import { browserHistory } from 'react-router';
import { AUTH_ERROR, AUTH_USER, FETCH_MESSAGE, UNAUTH_USER } from './types';

const API_URL = 'http://localhost:3000';

export function signinUser({ email, password }) {
  return function(dispatch) { // redux-thunk allows for the return of a function.
    // Submit email/password to the server
    axios.post(`${API_URL}/signin`, { email, password })
         .then(response => {
           // If request is good...
           // - Update state to indicate user is authenticated
           dispatch({ type: AUTH_USER });
           // - Save the JWT token
           localStorage.setItem('token', response.data.token);
           // - redirect to the route '/feature'
           browserHistory.push('/feature');
         })
         .catch(() => {
           // If request is bad...
           // - Show an error to the user
           dispatch(authError('Bad Login Info'));
         });
  }
}

export function signupUser({ email, password }) {
  return function(dispatch) {
    axios.post(`${API_URL}/signup`, { email, password })
         .then(response => {
           dispatch({ type: AUTH_USER });
           localStorage.setItem('token', response.data.token);
           browserHistory.push('/feature');
         })
         .catch(response => dispatch(authError(response.data.error)));
  }
}

export function authError(error) {
  return {
    type: AUTH_ERROR,
    payload: error
  };
}

export function signoutUser() {
  localStorage.removeItem('token');
  return { type: UNAUTH_USER };
}

export function fetchMessage() {
  return function(dispatch) {
    axios.get(API_URL, {
      headers: {
        authorization: localStorage.getItem('token')
      }
    }).then( response => {
      dispatch({
        type: FETCH_MESSAGE,
        payload: response.data.message
      })
    });
  }
}

authReducer.js

import { AUTH_ERROR, AUTH_USER, FETCH_MESSAGE, UNAUTH_USER } from '../actions/types';


export default function(state = {}, action) {
  switch (action.type) {
    case AUTH_USER:
      return { ...state, error: '', authenticated: true };
    case UNAUTH_USER:
      return { ...state, authenticated: false };
    case AUTH_ERROR:
      return { ...state, error: action.payload };
    case FETCH_MESSAGE:
      return { ...state, message: action.payload };
  }
  return state;
}

I believe I understand the new way of writing my reducer and that I eliminate the need for the majority of my action types.

NEW

authReducer.js

import { handle } from 'redux-pack';
import { AUTH_USER } from './types';

const initialState = {
  isLoading: false,
  authError: null,
  authenticated: false,
  message: ''
};

export function fooReducer(state = initialState, action) {
  const { type, payload } = action;
  switch (type) {
    case AUTH_USER:
      return handle(state, action, {
        start: s => ({
          ...s,
          isLoading: true,
          authError: null
        }),
        finish: s => ({ ...s, isLoading: false }),
        // If auth fails mark the user as not authenticated.
        // Yield an error.
        // Yield a message of failure to the user.
        failure: s => ({ ...s, authenticated: false, authError: payload, message: payload }),
        // If auth passes mark user as authenticated (push to authRoutes).
        // Display messge of success to user.
        success: s => ({ ...s, authenticated: true, message: payload }),
      });
    default:
      return state;
  }
}

Where I am really getting tripped up is refactoring the actions. Seems like from the example I should gut the original signupUser and put that code in a function of it's own, something like createUser({ email, password }). So whatever is returned from the promise in createUser() will be passed as the payload/promise in my actionCreator signupUser({ email, password });.

actions.js

const createUser = ({ email, password }) => {
   axios.post(`${API_URL}/signin`, { email, password })
         .then(response => {
           // If request is good...
           // - Update state to indicate user is authenticated
           dispatch({ type: AUTH_USER });
           // - Save the JWT token
           localStorage.setItem('token', response.data.token);
           // - redirect to the route '/feature'
           browserHistory.push('/feature');
         })
         .catch(() => {
           // If request is bad...
           // - Show an error to the user
           dispatch(authError('Bad Login Info'));
         });
} 

export const signupUser = ({ email, password }) => {
   return {
      type: AUTH_USER,
      promise: createUser({ email, password })
   }
}

Is that the correct understanding or am I over/under simplifying this? Sorry for asking this I'm still pretty new to coding and trying to understand and keep up with the technology. Overwhelming at times!

Promise seems to disappear in dispatch

I'm new to using redux-pack and it looks like a great way to simplify my code and make it more maintainable. However I have hit an issue where the handler throws and error and I cannot figure out why this is, I wanted to check if there is a bug in the handler/dispatch set up somewhere?

My code looks like the following and is instrumented with debug to figure out what is going on:

action_types.js

// Auth Action Types
export const LOGIN = 'LOGIN';

auth_actions.js

import * as types from './action_types';
import * as services from '../services';
import Debug from 'debug';

let debug = Debug('auth_actions');

export const login = (email, password) => {
	debug('login, called.');
	let promise = services.login(email, password);
	debug('login promise = ' + promise);
	return {type: types.LOGIN, promise: promise};
};

auth_services.js

import 'whatwg-fetch';
import Debug from 'debug';

let debug = Debug('auth_servcies');

const checkResponse = (response) => {
	if (!response.ok) {
		let error = new Error(response.statusText);
		error.response = response;
		return Promise.reject(error);
	}
	return response;
};

export const login = (email, password) => {

	debug('login, called.');
	debug('login, email: ' + email);

	let formData = new FormData();
	formData.append('email', email);
	formData.append('password', password);

	let init = {
		method:			'POST',
		credentials:	'same-origin',
		headers:		{'content-type': 'application/json'},
		body:			formData
	};

	let request = new Request('/login', init);

	debug('login, fetch request = ' + request.url + ', '+ request.method + ', ' + request.credentials + ', ' + request.body);

	return fetch(request)
		.then(checkResponse)
		.then(response => response.json())
		.catch(error => error);
};

auth_reducer.js

import handle from 'redux-pack';
import * as actions from '../actions';
import Debug from 'debug';

let debug = Debug('auth_reducer');

const INITIAL_STATE = {
	loggedin: false,
	isworking: false,
	err: null,
};

export default function reducer(state = INITIAL_STATE, action) {
	debug('reducer called');
	const { type, payload } = action;

	debug('state is: ' + JSON.stringify(state));
	debug('action : ' + JSON.stringify(action));

	switch (type) {

	case actions.LOGIN: {
		debug('login called');
		handle(state, action, {
			start: (prevState) => {debug('login start'); return {...prevState, isworking: true, err: null};},
			finish: prevState => {debug('login success'); return { ...prevState, isworking: false };},
			failure: prevState => {debug('login fail'); return { ...prevState, err: payload };},
			success: prevState => {debug('login success'); return { ...prevState, loggedin: payload.ok };}
		});
	}
	default:
		return state;
	}
}

When I hit the submit button on my Login.jsx component I see the following on the console:

Console Output

[Log] Login login, called +3s (client.js, line 7247)
[Log] Login props = {"match":{"path":"/login","url":"/login","isExact":true,"params":{}},"location":{"pathname":"/login","state":{"from":"/authRoute"},"search":"","hash":"","key":"94243a"},"history":{"length":58,"action":"REPLACE","location":{"pathname":"/login","state":{"from":"/authRoute"},"search":"","hash":"","key":"94243a"}},"isworking":false,"loggedin":false,"err":null} +0ms (client.js, line 7247)
[Log] Login mapDispatchToProps, login dispatched. +0ms (client.js, line 7247)
[Log] auth_actions login, called. +0ms (client.js, line 7247)
[Log] auth_servcies login, called. +0ms (client.js, line 7247)
[Log] auth_servcies login, email: [email protected] +0ms (client.js, line 7247)
[Log] auth_servcies login, fetch request = https://localhost:3000/login, POST, same-origin, [object ReadableStream] +1ms (client.js, line 7247)
[Log] auth_actions login promise = [object Promise] +1ms (client.js, line 7247)
[Log] auth_reducer reducer called +4s (client.js, line 7247)
[Log] auth_reducer state is: {"loggedin":false,"isworking":false,"err":null} +0ms (client.js, line 7247)
[Log] auth_reducer action : {"type":"LOGIN","meta":{"redux-pack/LIFECYCLE":"start","redux-pack/TRANSACTION":"60655b16-6233-480f-a5c1-c9506f4e9493"}} +0ms (client.js, line 7247)
[Log] auth_reducer login called +0ms (client.js, line 7247)
[Error] TypeError: Object is not a function (near '...(0, _reduxPack2.default)...')
	reducer (client.js:48752)
	combination (client.js:47039)
	dispatch (client.js:46817)
	(anonymous function) (client.js:46581)
	handlePromise (client.js:46515)
	(anonymous function) (client.js:46577)
	login (client.js:1891)
	login (client.js:1369)
	login
	callCallback (client.js:25590)
	dispatchEvent
	invokeGuardedCallbackDev (client.js:25628)
	invokeGuardedCallback (client.js:25677)
	invokeGuardedCallbackAndCatchFirstError (client.js:25691)
	executeDispatch (client.js:25956)
	executeDispatchesInOrder (client.js:25978)
	executeDispatchesAndRelease (client.js:26076)
	forEachAccumulated (client.js:26057)
	runEventsInBatch (client.js:26218)
	runExtractedEventsInBatch (client.js:26227)
	handleTopLevel (client.js:29691)
	batchedUpdates (client.js:38027)
	batchedUpdates (client.js:27429)
	dispatchEvent (client.js:29772)
	interactiveUpdates (client.js:38082)
	dispatchInteractiveEvent (client.js:29749)
	dispatchInteractiveEvent

So I see the Login method gets called, the state has the expected values, the dispatch is called, then the action calls the service and build the request with the expected data. The action then returns a Promise Object. The reducer is then called with the expected state but I note the action appears to have the correct type (LOGIN) but no apparent promise??? The type maps to the correct switch case then the handler throws the error above. In fact it throws two of the same type and then I get a 404 response from the server which has some how been called with a GET (not a POST) to /login?.

So I guess my question is what's causing this? Is the dispatch stripping the promise or is the action formed correctly and I'm hitting an issue in the handler?

Let me know if more information is needed to help diagnose or if you need me to try out anything.

Additional action keys being removed

I have an action that sends an addional key pageId along with promise and type. I have a TOGGLE_FAVORITE_PAGE action and reducer to handle the action.

// actions.js
export function toggleFavoritePage(page) {
  // either favorite or unfavorite a page.
  const action = page.favorited ? 'unfavorite' : 'favorite';
  const favoriteUrl = `${PAGES_URL}${page.id}/${action}/`;
  const request = axios.get(favoriteUrl);

  return {
    type: TOGGLE_FAVORITE_PAGE,
    promise: request,
    pageId: page.id
  };
}

// reducers.js
export default (state = initialState, action) => {
  console.log(action)
  // rest of reducer not important, action has no pageId key
  ...
}

I need access to the id of the page in the reducer and it's not available in the returned payload when removing a favorite, which is the reason why I'm passing it along in the action object. I'm currently migrating from redux-promise, where this was working.

Is this intended behavior in redux-pack to strip out extra data in the action? If it is, is it possible to get the page id in some other way if it's not available in the returned payload?

Pass the whole action into the handlers

One problem I'm having is that I can't pass any extra context into the handlers because the rest of the action is stripped away. For example, let's say I have a search:

export const search = keyword => ({
  type: "SEARCH",
  keyword,
  promise: makeSomeAPICall(keyword)
});

I'm putting keyword in the actual action because I want access to it at the start phase of my handler (e.g. to show some UI about it). But as far as I can tell, I don't have access to properties like that in my reducer, because pack isn't doing either of these: a) passing the original action's fields into dispatch or b) passing the original action's fields into the event hooks.

[Question]: Error handling

Interesting library! Very Nice!
I have been using redux-thunk so far along with the fetch library. works well.
redux-pack will be a big addidtion for me.

But really my main concern is errors handling. Maybe it is not the right place to ask it, but how would you globally handle fetch errors. The catch section of the fetch promise get called mainly where there is a network outage. How would generally handle 500, 403, 401 or event 400 errors?

react-native preset

Hi and thank you for this project.

I have a question, what is the reason of using the react-native preset?

I am trying to use this package in a react web app, and I am getting

Error: Couldn't find preset "react-native" relative to directory "/Users/amavridis/document/app/node_modules/redux-pack"

Any idea?

Making a case for .catch

Noticed this in the the middleware src:

// NOTE(lmr):
// it's debatable whether or not we want `.then(success, failure)`
// versus `.then(success).catch(failure)`

Well, I'd like to spark that debate. Right now this is working completely contrary to the Promise spec, which is making the promise returned from the action creator not play nicely with other libraries, and making chaining off of it pretty unintuitive.

I understand that chaining off the action creator is not something that should be done heavily, but sometimes (and especially in the case of an error) it is ideal to be able to handle the error case at the point of action creator invocation.

I have found that storing error states in the store can lead to issues where error messages persist for the entire life of the app, even though they should be ephemeral.

My preference for the ephemeral case is to handle that catch in the component layer, rather than in at the store layer.

Thoughts?

Dispatch still called multiple times

@lelandrichardson The README states that redux-pack overcomes the problem that "each dispatch causes a separate re-render of the entire component tree".

As I read the code and issue #34, dispatch is called the same number of times with or without redux-pack. Am I missing something?

question: side-effects and dispatch

in this section of the doc on side-effects, it shows something like:

import { sendAnalytics } from '../analytics';
import { doFoo } from '../api/foo';

export function userDoesFoo() {
  return {
    type: DO_FOO,
    promise: doFoo(),
    meta: {
      onSuccess: (result, getState) => {
        const userId = getState().currentUser.id;
        const fooId = result.id;
        sendAnalytics('USER_DID_FOO', {
          userId,
          fooId,
        });
      }
    }
  }
}

in the above sample, sendAnalytics looks a lot like an action creator (with type and payload as args), but submitting actions to redux requires a reference to dispatch somewhere.

is the implication that sendAnalytics submits a redux action, and if so, what is the implied strategy for obtaining a reference to dispatch?

or maybe, i'm asking the wrong question, but my general question is:

what would be the idiomatic way to manage a side-effect with redux-pack that wants to submit another redux action?

or is that an anti-pattern that redux-pack is specifically attempting to avoid?

apologies for being dense, i admittedly struggle with extracting "best practices" for side-effects from related discussions...

my immediate use-case is: on the failure of a data-fetch action i want to submit a subsequent ui-centric action concerned with managing errors (e.g. notify the user visually of an error)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.