Reputation: 6798
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:
Upvotes: 3
Views: 1273
Reputation: 39
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
.
Upvotes: 2