Skip to content

Middleware is broken #3019

@benoneal

Description

@benoneal

Bug

An important middleware pattern has been broken. IMO the majority of middleware solutions encapsulate their own concerns and are independent of the existence of other middleware. A change was made recently to break a pattern that only caused a minor issue for a specific subset of middleware: those that exhibit different behaviour when interacting with other middleware during the applyMiddleware initialisation path.

Here are some examples:

// Make state aware of browser media queries
const mediaQueries = (mediaQueries = defaultMediaQueries): Middleware => store => {
  if (typeof window !== 'undefined') {
    const reverseMap = createReverseMap(mediaQueries)

    const handleMediaChange = ({media, matches}: MediaChangedActionPayload) =>
      store.dispatch(mediaChanged(reverseMap[media], matches))
  
    const addMatchListener = (media: string) => {
      const match = window.matchMedia(media)
      match.addListener(handleMediaChange)
      return match.matches
    }
  
    values(mediaQueries).forEach(media => 
      handleMediaChange({media, matches: addMatchListener(media)}))
  }

  return next => (action: any) => next(action)
}

Another example:

// Make state aware of user adblockers
const adBlockDetection: Middleware = store => {
  if (typeof document !== 'undefined' && !document.getElementById('XMpADSwgbPUC')) {
    store.dispatch({type: AD_BLOCKER_DETECTED, payload: true})
  }
  return next => (action: any) => next(action)
}

Another example:

// Make state aware of socket connectivity and allow synchronisation of actions
const socketMiddleware = ({actionCreator, url, dataSync}) => store => {
  const socket = new WebSocket(url.replace(/(http|https)/, 'ws'))

  socket.addEventListener('message', ({data}) => {
    store.dispatch(JSON.parse(data))
  })

  socket.addEventListener('open', () => {
    socket.send(syncCreator(CONNECT_TO_DOMAIN, store.getState(), null, socket))
    store.dispatch(actionCreator({connected: true}))
  })

  socket.addEventListener('close', reason => {
    store.dispatch(actionCreator({connected: false}))
  })

  return next => action => {
    if (dataSync.hasOwnProperty(action.type)) {
      socket.send(syncCreator(action.type, store.getState(), action.payload, socket))
    }
    return next(action)
  }
}

What is the current behaviour?

The following error is thrown during applyMiddleware, essentially breaking the application:

Uncaught Error: Dispatching while constructing your middleware is not allowed. Other middleware would not be applied to this dispatch

Refactoring the above middleware to be closed over by the next => function (like below) makes no difference (but is obviously not a viable solution either).

// Still broken:
const mediaQueries = (mediaQueries = defaultMediaQueries): Middleware => store => next => {
  // ...middlewareCodeGoesHere...
  return (action: any) => next(action)
}

What is the expected behavior?

Do not break the app. Log a warning if you want, so I can ignore it, because in 100% of my use cases I do not care if other middleware can handle that dispatch. I strongly suspect that there are (or should be) very few legitimate reasons why any dispatch from within a middleware context should ever interact with any other middleware in the stack, and if it did, I would consider that a code smell (it would place implicit order dependencies on middleware at the very least).

Which versions of Redux, and which browser and OS are affected by this issue? Did this work in previous versions of Redux?

Worked until this PR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions