question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Thunks should support sync or async workflow

See original GitHub issue

There are scenarios where it would be convenient to call an action from another action. This helps keep code DRY and avoids a complicated maze of listeners to accomplish the same thing.

For example, an action could apply branching logic to decide which of 3 different updates to do. There may be individual actions for each of these updates, with their own data-transformation logic. Therefore it’s cleaner and DRYer to call these actions rather than duplicate their code.

I propose passing appending ‘actions’ as a 3rd argument to action() & actionOn(). This usually won’t be needed, just as storeState isn’t often needed, but would be available when it is…

action: (state, payload, actions) => {...}
actionOn: (state, target, actions) => {...}

Thunks, (a type of action) receive an actions arg, so this is not-inconsistent.

Here’s a contrived example, using the proposed 3rd argument to the handler:

updateValue: action((state, payload, actions) => {
    const { data, category } = payload;
    const { rights } = state.user;
    if (category === 'top') {
        if (rights === 'admin') {
            actions.setTopValue(data)
        } else {
            actions.setCategoryIfAllowed(payload)
        }
    } else {
        actions[`set${category}Value`](data)
   }
})

It’s not a great example, but hopefully it illustrates how branching to other actions can help keep things DRY and simple.

Why not put this logic in the calling code? If this action is called from multiple places, I don’t want to repeat this logic.

Why not create a helper method? This simple branching logic is based on data already in state, so an action seems the logic place for it. An action should be able to handle such logic, without replicating code from other actions that can also be called directly in other scenarios.

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Comments:13 (13 by maintainers)

github_iconTop GitHub Comments

1reaction
ctrlplusbcommented, Jul 22, 2019

I appreciate your desire to keep things DRY, however, I think we should consider dispatching actions within an action an anti-pattern. It it essentially the equivalent of dispatching an action from within a Redux reducer, and would suffer the same pitfalls.

The primary motivation you seem to have is avoiding duplication of code. Perhaps this could solved in a similar manner to a “pure” redux reducer strategy. Instead of delegating to reducers however, we could delegate to functions.

function removeInProgressItem(state, item) {
    state.inProgress = state.inProgress.filter(x => x !== item.id);
}

export const notificationModel = {
    addDoneItem: action((state, item) => {
        state.itemsDone.push(item)
        removeInProgressItem(state, item);
    })
}

If it gets tedious, or you discover patterns in your code, then perhaps consider to write helpers. For example;

const composedAction = (...actions) => {
    return action((state, payload) => {
        actions.forEach(x => x(state, payload));
    });
}

Which could be used like so:

function removeInProgressItem(state, item) {
    state.inProgress = state.inProgress.filter(x => x !== item.id);
}

function addDoneItem(state, item) {
    state.itemsDone.push(item);
}

export const notificationModel = {
    itemDone: composedAction(
        addDoneItem,
        removeItemInProgress
    )
}
0reactions
allprocommented, Jul 23, 2019
function removeInProgressItem(state, item) {
    state.inProgress = state.inProgress.filter(x => x !== item.id);
}

export const notificationModel = {
    addDoneItem: action((state, item) => {
        state.itemsDone.push(item)
        removeInProgressItem(state, item);
    })
}

In this example, the removeInProgressItem ‘function’ is really an ‘action’ that lives outside the model. The anti-pattern/limitation has not been avoided - just worked around.

You noted this is “the equivalent of dispatching an action from within a Redux reducer”. Yes and no. Normal reducers are ‘listeners’. Every reducer is called for every action, so multiple reducers can act on the same action-name. EP maps each action to a specific reducer, which is what creates the limitation of one reducer per-action

My own Redux wrappers also mapped one-action to one-reducer. When I needed a second reducer to fire, I triggered it. In simplified form: reducers.doSomething(action). Remembering this made me realize that I can do the same thing with EP model methods!

Instead of using an external function, this example calls another model action directly. Since I only want to trigger other actions within the same model/slice, this should work for me.

const notificationModel = {
    items: [],
    itemsDone: [],

    addDoneItem: action((state, item) => {
        state.itemsDone.push(item)
        notificationModel.removeItem(item)
    }),

    removeItem: action((state, item) => {
        state.items= state.items.filter(x => x !== item.id);
    })
}
Read more comments on GitHub >

github_iconTop Results From Across the Web

Understanding Asynchronous Redux Actions with Redux Thunk
Learn how to use the Redux Thunk middleware to run asynchronous operations, talk to an API and dispatch actions to the store.
Read more >
thunk | Easy Peasy v5
Thunks cannot modify state directly, however, they can dispatch actions to do so. ... They can be asynchronous or synchronous.
Read more >
Combining Synchronous Actions Using Redux Thunk
Learn how to use Redux Thunk for handling several synchronous actions at once to modify different areas of the application state.
Read more >
Writing Logic with Thunks - Redux
Thunks are best used for complex synchronous logic, and simple to moderate async logic such as making a standard AJAX request and dispatching ......
Read more >
The Saga of Async JavaScript: Thunks | by Roman Sarder
Here is the point — we could rewrite our synchronous example with a callback and then treat both an async and sync thunk...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found