3

Are there any examples of using redux-loop to handle complex ajax workflows? The official repo is very minimalistic. From the one example in the wild I was able to find (https://hackernoon.com/why-i-wrote-a-redux-async-outerware-277d450dba74#.2ocaloc58), it seems that redux-loop is very similar to redux-thunk.

Here are some examples of complex ajax workflows:

  • Ajax Workflow1. User selects two different filters on a results table. Each filter initiates an ajax request, which then resolve out of order. The results table should show the correct filter selection. Errors should not update the results table.
  • Ajax Workflow 2
    • User starts a report generation (which is a long running process).
    • User switches to another report. It should either cancel or ignore the pending “wait for report” action.
  • More complex workflow (based on an old redux-saga example)
    • User presses log-in button, which starts an ajax request to get an auth token
    • Either
      • User immediately presses log-out button, which should cancel/ignore the pending auth action
      • OR it should store the auth token when it resolves
    • Should clear the auth token after logout or if login error occurs
U Avalos
  • 6,538
  • 7
  • 48
  • 81

1 Answers1

2

I will give a shot at the second workflow (login).

Before going into the code, it's worth noting redux-loop is a lot simpler and offers less than redux-saga in terms of async control flow. But in the spirit of Elm, the focus is on data flow - not surprisingly is typically achieved by data types. Therefore it is helpful to think from the perspective of a statically typed language. In Haskell or Elm, it's probably beneficial to model the problem by data type, which itself encodes a state machine:

data LoginStatus data err =
    LoggedOut       | 
  , LoggedIn data   |
  , LoginError err  | 
  , Pending

Where data and err are type variables represent login data type (tokens) and login errors. JavaScript being dynamically typed, does not have the benefit expressing the same idea - but there are a lot of dynamic tricks one can use to mimic tagged union types like LoginStatus. Without further ago, here's the code:

import {match} from "single-key";

export default function reducer(state, action) {
  return match(state, {
    LoggedOut : () => loggedOutReducer(state, action),
    LoggedIn  : () => loggedInReducer(state, action),
    Pending : () => pendingReducer(state, action),
    LoginError : () => loginErrorReducer(state, action)
  });
} 

Here I will use a simple and lesser known library singe-key to achieve very basic run time union types. A "single-key" object as it's name suggest, is an object with just a key and a value, such as { a: 1 } ("a" is the key, and 1 is the value). We shall model the state with single-key objects - different keys represent different variants of LoginStatus. A few example states:

{
  LoggedOut : true
}


{
  LoggedIn : {
    token : 1235,
    user : { firstName: "John" }
  }
}

{
  Pending : true
}

With that cleared up, here are the sub-reducers used in the main reducer:

// state :: { LoggedIn: {/* some data * } }
function loggedInReducer(state, action) {
  if (action.type === LOGOUT) {
    return {
      LoggedOut : true
    };
  }
  return state;
}
// state :: { Pending : true }
function pendingReducer(state, action) {
  if (action.type === LOGIN_SUCCESS) {
    return {
      LoggedIn : {
        token : action.payload.token,
        user  : action.payload.user
      }
    };
  }
  if (action.type === LOGIN_ERROR) {
    return {
      LoginError : action.payload;  
    };
  }
  if (action.type === LOGOUT) {
    return {
      LoggedOut : true  
    };
  }
  return state;
}
// state :: { LoggedOut : true }
function loggedOutReducer(state, action) {
  if (action.type === LOGIN) {
    return loop({ Pending: true }, Effects.promise(loginRequest));  
  }
  return state;
}
// state :: { LoginError : error }
function loginErrorReducer(state, action) {
  if (action.type === LOGIN) {
    return loop({ Pending: true }, Effects.promise(loginRequest));  
  }
  return { LoggedOut : true }; 
}

These are like transitions in a finite state machine, except sometimes with data attached to the state. Each individual reducers are fairly simple and handles very few action types. Only two reducers return effects:

return loop({ Pending: true }, Effects.promise(loginRequest));

This transitions the state from LoggedOut/LoginError to Pending and specify some side effects - which will be scheduled by redux-loop. You may even merge the two variants into one: { LoggedOut : error | null }, but I feel having a seperate LoginError state is beneficial in the long run.

With some notion of data types, this problem can be easier to reason about than it first appears; you can do the same thing with reducer structured roughly the same and use just redux-thunk.

  • I appreciate the response but... the problem with this approach is that it mixes responsibilities. Async workflow + "normal" state. This leads to complex reducers. In real projects, that means you just made it easier to create brittle code.. Also, you haven't addressed how to actually do the workflows above. – U Avalos Mar 19 '17 at 16:54
  • 1
    Thanks for the feedback. This is reasonable criticism. For the last point, I believe I have mostly addressed how it would work. Well, since I have some free time at hand today, just made a working example using the reducer layout explained here. It has both a `redux-loop` and `redux-sage` implementation with almost identical reducers: [https://github.com/yiransheng/redux-login-examples](https://github.com/yiransheng/redux-login-examples) – Yiran Sheng Mar 20 '17 at 05:12
  • What's good is your idea of explicitly enumerating types. Unfortunately, as they say in America, "You're beating a dead horse." In addition, to the mixing of concerns, you don't have a dedicated API for handling asynchronicity. Hence, in real-world projects, code gets very, very complex (and repetitive). For example, you don't handle the "cancel" case. Secondly, FRP/sagas win in testing. You don't need mocks. – U Avalos Mar 20 '17 at 14:52