Vuex: Fake "takeLatest"

Created on 26 Jul 2017  路  14Comments  路  Source: vuejs/vuex

What problem does this feature solve?

All takeLatest-like situations. A good example would be having an action that get a list from a server by sending a bunch of filters. If we debounce it by 500ms there still is a potential scenario when you change filters while request is in progress and there is a tiny chance that second action's request will come before first one so mutation with the second list (that we want) will happen before the first list (that we don't want anymore). A "fake" takeLatest would be an ability to mark an action as cancelled so that it can not call mutations (calls no-op instead). It's not perfect but it would cover a lot of possible scenarios.

What does the proposed API look like?

I have no idea

proposal

Most helpful comment

What I was suggesting is something like:

// actions.js
import { latest } from 'vuex'

export default {
  someAction: latest(async ({commit, dispatch}) => {
    try {
      await someAsyncStuff()
      commit('something') //will throw AbortError
      dispatch('something') //will throw AbortError
    } catch (e) {
      if (e.name === 'AbortError') {
        return
      }
    }
  })
}

a simple POC for latest work would be:

const latest = (fn) => {
  let lastExecutionId = 0
  return (store, ...args) => {
    const executionId = ++lastExecutionId
    const wrappedCommit = (...args) => {
      if (executionId === lastExecutionId) {
        return store.commit.apply(store, args)
      } else {
        throw new AbortError()
      }
    }
    const wrappedDispatch = (...args) => {
      if (executionId === lastExecutionId) {
        return store.dispatch.apply(store, args)
      } else {
        throw new AbortError()
      }
    }
    const patchedArgs = [{...store, commit: wrappedCommit, dispatch: wrappedDispatch}, ...args]
    return fn.apply(null, patchedArgs)
  }
}

Dunno how much sense it makes :smile:

All 14 comments

I use async actions most of the time and it's a shame all cancellation proposals got denied :(

Another way to handle this would be to only send one request at a time (i.e. kind of like debounce but once at the beginning and then the latest and use your network latency as the debouncing mechanism). You do this by managing a local, saving, and saved copy of your resource and tracking when a give request is in progress. If you send out a request set the local version to the saving version and track that your request is in progress. When it gets back validate that the saving copy is still the same as your local copy if not send another request to persist the updates.

In that scenario, I think it's impossible to cancel the request and ongoing commits from Vuex because they are handled in the users' code. In addition, as the Promise won't have cancellation feature, there are possibly various implementations to cancel async processing and it would make hard to implement cancellation feature in Vuex.

I'd say this should be implemented in user land to adapt various cancellation strategies. And in that case, I think we can use the action enhancer approach (See https://github.com/vuejs/vuex/pull/571#issuecomment-291899754).

For example, if we implement it with AbortController strategy, it would look like:

actions: {
  // Enhance `sendRequest` action to allow cancellation
  sendRequest: abortEnhancer(signal => { // `signal` is AbortSignal
    return ({ commit }, payload) => {
      return fetch(payload, { signal })
        .then(res => res.json())
        .then(data => commit('setData', data)) // If the request is aborted, it won't commit the data.
        .catch(err => {
          if (err.name === 'AbortError') return
          commit('setError', err) 
        })
    }
  })
}

What I was suggesting is something like:

// actions.js
import { latest } from 'vuex'

export default {
  someAction: latest(async ({commit, dispatch}) => {
    try {
      await someAsyncStuff()
      commit('something') //will throw AbortError
      dispatch('something') //will throw AbortError
    } catch (e) {
      if (e.name === 'AbortError') {
        return
      }
    }
  })
}

a simple POC for latest work would be:

const latest = (fn) => {
  let lastExecutionId = 0
  return (store, ...args) => {
    const executionId = ++lastExecutionId
    const wrappedCommit = (...args) => {
      if (executionId === lastExecutionId) {
        return store.commit.apply(store, args)
      } else {
        throw new AbortError()
      }
    }
    const wrappedDispatch = (...args) => {
      if (executionId === lastExecutionId) {
        return store.dispatch.apply(store, args)
      } else {
        throw new AbortError()
      }
    }
    const patchedArgs = [{...store, commit: wrappedCommit, dispatch: wrappedDispatch}, ...args]
    return fn.apply(null, patchedArgs)
  }
}

Dunno how much sense it makes :smile:

I think this is a common enough pattern to be included in vuex even tho it's really easy to implement in user land.

Oh, I see.
I think we can just add a new option if we include that feature in Vuex.

export default {
  actions: {
    someAction: {
      async handler ({commit, dispatch}) {
        await someAsyncStuff()
        commit('something')
        dispatch('something')
      },
      takeLatest: true
    }
  }
}

@ktsn, it's your territory, I just use that pattern a lot and wanted to propose you to add it to vuex.

/ping @ktsn, should I make a PR for that?

@nickmessing Yeah, feel free to go ahead.

@ktsn, there's another thing to be considered for this, what do you think about this code?

const sleept = ms => new Promise(resolve => setTimeout(resolve, ms))

export default {
  actions: {
    someAction: {
      async handler ({commit, dispatch, noop}) {
        await sleep(500)
        noop() // throws if called again during sleep
        await someAsyncStuff()
        commit('something')
        dispatch('something')
      },
      takeLatest: true
    }
  }
}

@nickmessing Do you mean that it's like adding check points for aborting?
Hmm, I don't think it's a good idea because it would make the code more procedural and complicated.

For anyone interested, I implemented an "action enhancer" approach (see https://github.com/vuejs/vuex/pull/571#issuecomment-291899754) that addresses this issue.

=> https://github.com/sebdiem/vuex-cancellable-actions

Tell me if you find this useful.

I think we could be closing this for now. Handling promises is pretty much in user land situation. Cancelling, or interrupting with Promise in general is to broad concept to cover as single Vuex feature.

Let us know if there's any further discussion for this issue. Otherwise, I'll close it in a while 馃

Closing due to inactivity. Please feel free to open another issue if there's anything 馃憤

Was this page helpful?
0 / 5 - 0 ratings