0

tl;dr I would like to know where to place context specific multi-step async callback logic in a redux architecture, and if I am on the right track with the example code I supply below. By "multi-step" and "context specific" I typically mean server calls initiated by some user action (onClicks, etc) where the logic might only be relevant for a given component (such as redirect to a given route when successful).

The redux docs has this to say on code with side effects:

In general, Redux suggests that code with side effects should be part of the action creation process. While that logic can be performed inside of a UI component, it generally makes sense to extract that logic into a reusable function so that the same logic can be called from multiple places—in other words, an action creator function.

While that seems fine, I am not totally sure whether it is "correct" to put calls to my routing component in there, as these action creators usually seem quite generic, and triggering routing to some other resource in the app is usually quite context dependant.

I also find it a bit weird to put these quite-different beasts, that trigger action creators asynchronously and dispatch the resulting actions, in the same files (foo-model/actions.js) as the "clean" sync action creators. Is this the right place? When reading tutorials on Redux it seems like they live side by side.

The example code is quite simple and basically describes these steps:

  1. On a user click, call a function with some param
  2. This function calls another async function (such as a network call)
  3. When the async call completes, trigger a routing action to another page

Background: I want to gradually refactoring a Meteor project by moving all Meteor specific bits out of the React components, eventually substituting Meteor in the front and back for something else. As there are about 50KLOC I cannot do this in one go, so I am gradually working my way through one route at a time, hoping to end up with a standard React+Redux+ReduxRouter package. In the current code routing, data fetching, and rendering is somewhat intertwined in each component, and I am having some trouble finding out where to put multi-step async logic, such as the example below.

Details on the stack I am trying to wrangle my way out of:

  • FlowRouter for routing
  • Meteor/MiniMongo for data mutation and retrieval
  • React Komposer for Higher Order Components

old Meteor code in MyContainerComponent

// triggered as onClick={(e) => this.saveEncounter(e.target.value)}
// in render()
const saveEncounter = (encounter) => {
    Meteor.call('createEncounter', encounter, handleSaveResult);
  }
};

const handleSaveResult = (err, encounterId) => {
  if (err) {
    this.setState({errorMessages: err});
  } else {
    // route to another page
    NavigationActions.goTo('encounter', {encounterId: this.props.encounter._id || encounterId});
  }
}

new redux code - moved into actions.js

I am trying to keep the implementation straight forward (no additional deps) at this point to understand the foundations. "Simplification" using redux-thunk, redux-actions or redux-saga need to come later. Modeled after the example code in the Redux tutorial for Async Actions

export const saveEncounter = (encounter) => {

    function handleSave(err, encounterId) {
        if (err) {
            dispatch(createEncounterFailure(err), encounter);
        } else {
            dispatch(createEncounterSuccess(encounterId));
        }
    }

    dispatch(createEncounterRequest(encounter));
    Meteor.call('createEncounter', encounter, handleSave);
}


// simple sync actions creators
export const CREATE_ENCOUNTER_REQUEST = 'CREATE_ENCOUNTER_REQUEST';
function createEncounterRequest(encounter) {
    return {
        type: CREATE_ENCOUNTER_REQUEST,
        encounter
    };
}
export const CREATE_ENCOUNTER_FAILURE = 'CREATE_ENCOUNTER_FAILURE';
function createEncounterFailure(error, encounter) {
    return {
        type: CREATE_ENCOUNTER_FAILURE,
        error,
        encounter
    };
}
export const CREATE_ENCOUNTER_SUCCESS = 'CREATE_ENCOUNTER_SUCCESS';
function createEncounterSuccess(encounterId) {
    return {
        type: CREATE_ENCOUNTER_SUCCESS,
        encounterId
    };
} 
oligofren
  • 20,744
  • 16
  • 93
  • 180
  • I ended up finding a lot of valuable info in the answer by Dan Abromov here: https://stackoverflow.com/questions/35411423/how-to-dispatch-a-redux-action-with-a-timeout/35415559#35415559 I wish this was part of the docs. – oligofren Jun 01 '17 at 14:30

3 Answers3

1

I see your point, you would like to have a way to divide and categorize your actions, is that right? Actions that will execute sync code, async code, logger, etc.

Personally, I use some naming convention. If I have to dispatch an action that has to fetch some data, I call it REQUEST_DATA. If have to store some data arrived from the server to the ReduxStore, I call it STORE_DATA.

I don't have a specific pattern. I also have to point that I divide my codebase based on the feature, so the modules where I define my actions are pretty small and neat

Ematipico
  • 1,181
  • 1
  • 9
  • 14
  • That division, does that include components, containers, actions and reducers? – oligofren Jun 01 '17 at 12:02
  • 1
    Components are in a separated folder because they are agnostic and they are supposed to work in every section. So I would have `components` folder and `containers` folder. Inside containers I will have `actions`, `reducers` and `saga`'s (if you use such library) and the container component that is connected to the store – Ematipico Jun 01 '17 at 12:45
1

As you noted in a comment, Dan Abramov discussed a lot of the ideas behind handling async work in Redux in his answer for how to dispatch an action with a timeout. He also wrote another excellent answer in why do we need middleware for async flow in Redux?.

You might want to read through some of the other articles in the Redux Side Effects category of my React/Redux links list to get a better idea of ways to handle async logic in Redux.

In general, it sounds like you may want to make use of either "sagas" or "observables" for managing some of your async logic and workflow. There's a huge variety of Redux middlewares for async behavior out there - I summarized the major categories and most popular libraries in my blog post The Tao of Redux, Part 2 - Practice and Philosophy. There's also some interesting thoughts on a very decoupled saga-based Redux architecture in a post called Redux Saga in Action.

markerikson
  • 63,178
  • 10
  • 141
  • 157
  • 1
    I learned a lot from those links, especially from the Dan Abramov answer, as that detailed exactly how I would assume things would be done in the simplest way, as well as the weaknesses of that approach, and how thunks helps us simplify it. – oligofren Jun 01 '17 at 19:58
0

In my experience with Redux, I haven't found any problems with putting async calls inside action creators. I think redux-thunk or some other middleware is very helpful, even for a simple setup.

The only thing I'd add is that I don't find your sample code very readable.

Personally I've come to like the ducks pattern, but also just keeping action types, action creators and reducers in separate files would work to improve clarity.

Hope this helps.

U r s u s
  • 6,680
  • 12
  • 50
  • 88
  • I'm not sure which parts of sample code is hard to read - the old or the new. They are almost identical, apart from the new one having a bunch of action creator function at the bottom and some dispatch calls. The new code is just modeled after the reference pattern in the Redux official docs: http://redux.js.org/docs/advanced/AsyncActions.html – oligofren Jun 01 '17 at 11:00
  • Thanks for the answer, btw, but besides my comment above, my main grievances are not really touched upon. How do you differentiate between clean action creators and the ones with side effects? Do you differentiate through some naming construct? Often the hardest part is finding different names for the subtly different things. GET_ENCOUNTER might be good for an in-app action, but what would I call the almost identical one that fetches the entity over the network? Do you have any input on the the actual routing bits I touched upon in the question? – oligofren Jun 01 '17 at 11:05
  • 1
    Sorry for not being more helpful, but I might be struggling to see what your actual problems are. As far as I can see, the best way forward is to pick a convention that suits you and use it consistently. Certainly, if a framework starts to hinder you, it might not be the right for you. – U r s u s Jun 01 '17 at 13:11