Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Queuing Actions in Redux

I've currently got a situation whereby I need Redux Actions to be run consecutively. I've taken a look at various middlewares, such a redux-promise, which seem to be fine if you know what the successive actions are at the point of the root (for lack of a better term) action being triggered.

Essentially, I'd like to maintain a queue of actions that can be added to at any point. Each object has an instance of this queue in its state and dependent actions can be enqueued, processed and dequeued accordingly. I have an implementation, but in doing so I'm accessing state in my action creators, which feels like an anti-pattern.

I'll try and give some context on use case and implementation.

Use Case

Suppose you want to create some lists and persist them on a server. On list creation, the server responds with an id for that list, which is used in subsequent API end points pertaining to the list:

http://my.api.com/v1.0/lists/           // POST returns some id http://my.api.com/v1.0/lists/<id>/items // API end points include id 

Imagine that the client wants to perform optimistic updates on these API points, to enhance UX - nobody likes looking at spinners. So when you create a list, your new list instantly appears, with an option at add items:

+-------------+----------+ |  List Name  | Actions  | +-------------+----------+ | My New List | Add Item | +-------------+----------+ 

Suppose that someone attempts to add an item before the response from the initial create call has made it back. The items API is dependent on the id, so we know we can't call it until we have that data. However, we might want to optimistically show the new item and enqueue a call to the items API so that it triggers once the create call is done.

A Potential Solution

The method I'm using to get around this currently is by giving each list an action queue - that is, a list of Redux actions that will be triggered in succession.

The reducer functionality for a list creation might look something like this:

case ADD_LIST:   return {     id: undefined, // To be filled on server response     name: action.payload.name,     actionQueue: []   } 

Then, in an action creator, we'd enqueue an action instead of directly triggering it:

export const createListItem = (name) => {     return (dispatch) => {         dispatch(addList(name));  // Optimistic action         dispatch(enqueueListAction(name, backendCreateListAction(name));     } } 

For brevity, assume the backendCreateListAction function calls a fetch API, which dispatches messages to dequeue from the list on success/failure.

The Problem

What worries me here is the implementation of the enqueueListAction method. This is where I'm accessing state to govern the advancement of the queue. It looks something like this (ignore this matching on name - this actually uses a clientId in reality, but I'm trying to keep the example simple):

const enqueueListAction = (name, asyncAction) => {     return (dispatch, getState) => {         const state = getState();          dispatch(enqueue(name, asyncAction));{          const thisList = state.lists.find((l) => {             return l.name == name;         });          // If there's nothing in the queue then process immediately         if (thisList.actionQueue.length === 0) {             asyncAction(dispatch);         }      } } 

Here, assume that the enqueue method returns a plain action that inserts an async action into the lists actionQueue.

The whole thing feels a bit against the grain, but I'm not sure if there's another way to go with it. Additionally, since I need to dispatch in my asyncActions, I need to pass the dispatch method down to them.

There is similar code in the method to dequeue from the list, which triggers the next action should one exist:

const dequeueListAction = (name) => {     return (dispatch, getState) => {         dispatch(dequeue(name));          const state = getState();         const thisList = state.lists.find((l) => {             return l.name === name;         });          // Process next action if exists.         if (thisList.actionQueue.length > 0) {             thisList.actionQueue[0].asyncAction(dispatch);     } } 

Generally speaking, I can live with this, but I'm concerned that it's an anti-pattern and there might be a more concise, idiomatic way of doing this in Redux.

Any help is appreciated.

like image 607
MrHutch Avatar asked Aug 03 '16 11:08

MrHutch


People also ask

What is actions used for in Redux?

Actions are the only source of information for the store as per Redux official documentation. It carries a payload of information from your application to store. const ITEMS_REQUEST = 'ITEMS_REQUEST'; Apart from this type attribute, the structure of an action object is totally up to the developer.

What is async actions in Redux?

Redux Async Data Flow​ Then, we call dispatch() , and pass in something, whether it be a plain action object, a function, or some other value that a middleware can look for. Once that dispatched value reaches a middleware, it can make an async call, and then dispatch a real action object when the async call completes.


2 Answers

I have the perfect tool for what you are looking for. When you need a lot of control over redux, (especially anything asynchronous) and you need redux actions to happen sequentially there is no better tool than Redux Sagas. It is built on top of es6 generators giving you a lot of control since you can, in a sense, pause your code at certain points.

The action queue you describe is what is called a saga. Now since it is created to work with redux these sagas can be triggered to run by dispatching in your components.

Since Sagas use generators you can also ensure with certainty that your dispatches occur in a specific order and only happen under certain conditions. Here is an example from their documentation and I will walk you through it to illustrate what I mean:

function* loginFlow() {   while (true) {     const {user, password} = yield take('LOGIN_REQUEST')     const token = yield call(authorize, user, password)     if (token) {       yield call(Api.storeItem, {token})       yield take('LOGOUT')       yield call(Api.clearItem, 'token')     }   } } 

Alright, it looks a little confusing at first but this saga defines the exact order a login sequence needs to happen. The infinite loop is allowed because of the nature of generators. When your code gets to a yield it will stop at that line and wait. It will not continue to the next line until you tell it to. So look where it says yield take('LOGIN_REQUEST'). The saga will yield or wait at this point until you dispatch 'LOGIN_REQUEST' after which the saga will call the authorize method, and go until the next yield. The next method is an asynchronous yield call(Api.storeItem, {token}) so it will not go to the next line until that code resolves.

Now, this is where the magic happens. The saga will stop again at yield take('LOGOUT') until you dispatch LOGOUT in your application. This is crucial since if you were to dispatch LOGIN_REQUEST again before LOGOUT, the login process would not be invoked. Now, if you dispatch LOGOUT it will loop back to the first yield and wait for the application to dispatch LOGIN_REQUEST again.

Redux Sagas are, by far, one of my favorite tools to use with Redux. It gives you so much control over your application and anyone reading your code will thank you since everything now reads one line at a time.

like image 149
EJ Mason Avatar answered Oct 09 '22 05:10

EJ Mason


Have a look at this: https://github.com/gaearon/redux-thunk

The id alone shouldn't go through the reducer. In your action creator (thunk), fetch the list id first, and then() perform a second call to add the item to the list. After this, you can dispatch different actions based on whether or not the addition was successful.

You can dispatch multiple actions while doing this, to report when the server interaction has started and finished. This will allow you to show a message or a spinner, in case the operation is heavy and might take a while.

A more in-depth analysis can be found here: http://redux.js.org/docs/advanced/AsyncActions.html

All credit to Dan Abramov

like image 27
Anthony De Smet Avatar answered Oct 09 '22 06:10

Anthony De Smet