redux-logic icon indicating copy to clipboard operation
redux-logic copied to clipboard

Docs: note that debounce/throttle apply to the action, not `process`?

Open abrenneke opened this issue 8 years ago • 3 comments

So I had a logic like this:

createLogic({
  type: 'DO_SOMETHING',
  debounce: 250,
  process: etc
});

From the docs I assumed that debounce would debounce the process function, not the action itself and I was confused why not all of my DO_SOMETHING actions were going through (other reducers are changed by DO_SOMETHING also). Was a misunderstanding on my part.

I think it would be helpful to note somewhere that it's the actual redux dispatch that gets debounced/throttled, rather than just the process function!

Another way to think of how I got confused: I was trying to "attach async logic to existing actions", when what redux-logic does is sort-of "turns a dispatch into an async dispatch with optional additional async processing"

abrenneke avatar Sep 22 '17 22:09 abrenneke

@SneakyMax Thanks for writing! Yes, the debounce is in indeed happening on the front of the chain on the action before it goes to the reducers. This allows you to send all onchange actions from an input and only use the last one that fires after a pause rather than every key stroke.

I will definitely update the docs to indicate more specifically how this works. Sorry for the confusion.

jeffbski avatar Sep 26 '17 13:09 jeffbski

had the same confusion when tried to listen redux-logic, debounce was actually causing whole application to freeze

import * as R from 'ramda'
import { createLogic } from 'redux-logic'
import { actionTypes } from 'redux-form'

import * as previewActions from '~/app/actions/preview'
import { createStoryFormId } from '~/app/containers/CreateStoryForm'
import { isFormAction } from '~/app/utils'
import createPage from '~/app/services/createPage'

const logic = createLogic({
  type:     actionTypes.CHANGE,
  debounce: 1500,
  latest:   true,

  validate({ action }, allow, reject) {
    const valid = isFormAction(createStoryFormId, action)
    if (valid) {
      allow(action)
    } else {
      reject()
    }
  },

  async process({ action }, dispatch, done) {
    const { payload: link } = action

    if (R.isEmpty(link)) {
      dispatch(previewActions.setPage({}))
    } else {
      try {
        dispatch(previewActions.setLoading(true))

        const { data } = await createPage(link)

        dispatch(previewActions.setPage(data))
        dispatch(previewActions.setLoading(false))
      } catch (e) {
        dispatch(previewActions.setLoading(false))
      }
    }

    done()
  },
})

export default logic

then I've tried

import * as R from 'ramda'
import debounce from 'lodash.debounce'
import { createLogic } from 'redux-logic'
import { actionTypes } from 'redux-form'

import * as previewActions from '~/app/actions/preview'
import { createStoryFormId } from '~/app/containers/CreateStoryForm'
import { isFormAction } from '~/app/utils'
import createPage from '~/app/services/createPage'

async function process({ action }, dispatch, done) {
  const { payload: link } = action

  if (R.isEmpty(link)) {
    dispatch(previewActions.setPage({}))
  } else {
    try {
      dispatch(previewActions.setLoading(true))

      const { data } = await createPage(link)

      dispatch(previewActions.setPage(data))
      dispatch(previewActions.setLoading(false))
    } catch (e) {
      dispatch(previewActions.setLoading(false))
    }
  }

  done()
}

const debouncedProcess = debounce(process, 500)

const logic = createLogic({
  type: actionTypes.CHANGE,

  validate({ action }, allow, reject) {
    const valid = isFormAction(createStoryFormId, action)
    if (valid) {
      allow(action)
    } else {
      reject()
    }
  },

  process: debouncedProcess,
})

export default logic

but only first part of process was dispatching actions (part before await), it was because createLogic was scanning my process function to find how many arguments it is expecting

finally made it working with

  process: (context, dispatch, done) => {
    debouncedProcess(context, dispatch, done)
  },

At the end it was looking like this :expressionless:

import * as R from 'ramda'
import debounce from 'lodash.debounce'
import { createLogic } from 'redux-logic'
import { actionTypes, getFormSyncErrors } from 'redux-form'
import { CancelToken } from 'axios'

import { isPropEmpty } from '~/app/utils'
import * as previewActions from '~/app/actions/preview'
import { createStoryFormId } from '~/app/containers/CreateStoryForm'
import createPage from '~/app/services/createPage'

const isFormChanged = R.pathEq(['meta', 'form'])
const isFieldChanged = R.pathEq(['meta', 'field'])

const isFieldValid = (formId, fieldName, state) => {
  const getErrors = getFormSyncErrors(createStoryFormId)
  const errors = getErrors(state)
  return isPropEmpty(fieldName, errors)
}

let cancel = null

async function process({ getState, action }, dispatch, done) {
  const { payload: link } = action

  if (R.isEmpty(link)) {
    dispatch(previewActions.setPage({}))
  } else {
    // we have to wait for validation to pass
    const state = getState()
    const isLinkValid = isFieldValid(createStoryFormId, 'link', state)
    if (!isLinkValid) return

    if (cancel) {
      cancel()
      cancel = null
    }

    try {
      dispatch(previewActions.setLoading(true))

      const cancelToken = new CancelToken(c => {
        cancel = c
      })
      const { data } = await createPage(link, cancelToken)

      dispatch(previewActions.setPage(data))
      dispatch(previewActions.setLoading(false))
    } catch (e) {
      dispatch(previewActions.setLoading(false))
    }

    cancel = null
  }

  done()
}

const debouncedProcess = debounce(process, 500)

const logic = createLogic({
  type: actionTypes.CHANGE,

  validate({ action, getState }, allow, reject) {
    const isCreateStoryFormChanged = isFormChanged(createStoryFormId, action)
    if (!isCreateStoryFormChanged) return reject()

    const isLinkChanged = isFieldChanged('link', action)
    if (!isLinkChanged) return reject()

    return allow(action)
  },

  process: (context, dispatch, done) => {
    debouncedProcess(context, dispatch, done)
  },
})

export default logic

srghma avatar Dec 11 '17 17:12 srghma

Thanks for sharing @BjornMelgaard

jeffbski avatar Dec 12 '17 01:12 jeffbski