erikras / react-redux-universal-hot-example

A starter boilerplate for a universal webapp using express, react, redux, webpack, and react-transform
MIT License
11.99k stars 2.5k forks source link

examples of complex actions #880

Open krukid opened 8 years ago

krukid commented 8 years ago

Hi, I'm new to react/redux and I'm somewhat struggling with the concept of complex actions at the moment. Specifically, sequenced async actions with potential redirects and API requests.

It would be awesome if someone could review some of my approaches and give pointers on the issues I've been having.

So, let me provide some code for a generalized use case for actions I come across in my app:


// make an API request via clientMiddleware, fire store actions
export function apiRequestA(data) {
  return {
    types: [A_START, A_SUCCESS, A_FAIL],
    promise: (client) => client.post('/api/request-a', {data})
  };
}

// make several API requests in sequence, with inter-dependencies etc conditionals,
// firing transitional actions and success/failure actions at the end
export function doSomethingAsync({payloadA, payloadB} = {}) {
  return (dispatch, getState) => {
    let sequence = Promise.resolve();

    sequence = sequence.then(() => {
      return dispatch(apiRequestA(payloadA)).then(
        action => action.type === A_SUCCESS
          ? Promise.resolve(action.result)
          : Promise.reject('error requesting A');
      );
    });

    if (payloadB) {
      sequence = sequence.then(result => {
        if (result.something) {
          return Promise.reject('cannot request B because A returned "something"');
        } else {
          return dispatch(apiRequestB(payloadB)).then(
            action => action.type === B_SUCCESS
              ? Promise.resolve(action.result)
              : Promise.reject('error requesting B')
          );
        }
      });
    }

    return sequence
      .then(result => dispatch({type: DO_SOMETHING_SUCCESS, result}))
      .catch(error => dispatch({type: DO_SOMETHING_FAILURE, error}));
  };
}

Now, this seems to work fine and do the job, but my first question is - maybe there's a canonical way of chaining these API requests, maybe without using the clientMiddleware? Or could that be extended for simpler chainability?

My second question is regarding redirects within actions - again, there don't seem to be any examples of this within the project. Consider the following code:

// actions.js
import { pushStore } from 'redux-router';

export function myRedirectAction(url) {
  return (dispatch, getStore) => {
    return dispatch(pushState(null, url));
  };
}

// container.js
import { pushStore } from 'redux-router';
import { myRedirectAction } from './actions';

@connect(
  (state) => ({}),
  {pushStore, myRedirectAction}
)
class MyComponent extends Component {
  render() {
    return (
      <div>
        <a href="#" onClick={() => this.props.pushStore(null, '/foo')}>This works</a>
        <a href="#" onClick={() => this.props.myRedirectAction('/foo')}>This doesn't work</a>
      </div>
    );
  }
}

Is there any simple way to redirect from within the action?

Nedomas commented 8 years ago

I'm also struggling with sequential actions and using similar approach. It works, but is suboptimal. Have you figured out a better way to do it?

krukid commented 8 years ago

Hey @Nedomas, at this point there isn't too much actual logic in my app (focusing on data flow, styling and component structure currently), so I didn't dwell on this issue too much yet. It does seem that both of the points mentioned in the OP have been affected by the recent merge of "simple-router" branch. Namely, the redirects now work from both custom actions and components as they're supposed to and clientMiddleware was changed to allow simpler chaining of API requests/responses with other logic.

Example:

// actions.js
function simple() {
  return {
    type: SIMPLE,
    data: 'data'
  };
}

function remote() {
  return {
    types: [REMOTE, SUCCESS, FAIL],
    promise: (client) => client.get('/url')
  };
}

export function complex(flag1, flag2) {
  return (dispatch, getStore) => {
    if (flag1) {
      return dispatch(simple());
    } else {
      let sequence = dispatch(remote());
      if (flag2) {
        sequence = sequence.then(result => {
          if (result.flag3) {
            Promise.resolve({...result, modify: true});
          } else {
            Promise.reject('some error');
          }
        });
      }
      return sequence.then(
        result => dispatch({type: COMPLEX_COMPLETE, result}),
        error => dispatch({type: COMPLEX_FAIL, error})
      );
    }
  };
}

Following this approach, however, you have to be super-careful with results, because it's easy to lose track of what is returned and modified. IMO there needs to be some kind of convention here, maybe some new abstractions with specific interfaces, e.g. "action models". I also think RRUHE is lacking more robust abstractions here to keep things relatively unopinionated, so that it would be easier to adopt new trends, should they develop elsewhere. Anyway, I'll keep experimenting, once I start wiring more of my UI with the API :) Plz share if you find something meanwhile.