Skip to content
v1.8.0
Compare
Choose a tag to compare
@markerikson markerikson released this
· 235 commits to master since this release

This release adds the new "listener" middleware, updates configureStore's types to better handle type inference from middleware that override dispatch return values, and updates our TS support matrix to drop support for TS < 4.1.

Changelog

New "Listener" Side Effects Middleware

RTK has integrated the thunk middleware since the beginning. However, thunks are imperative functions, and do not let you run code in response to dispatched actions. That use case has typically been covered with libraries like redux-saga (which handles side effects with "sagas" based on generator functions), redux-observable (which uses RxJS observables), or custom middleware.

We've added a new "listener" middleware to RTK to cover that use case. The listener middleware is created using createListenerMiddleware(), and lets you define "listener" entries that contain an "effect" callback with additional logic and a way to specify when that callback should run based on dispatched actions or state changes.

Conceptually, you can think of this as being similar to React's useEffect hook, except that it runs logic in response to Redux store updates instead of component props/state updates.

The listener middleware is intended to be a lightweight alternative to more widely used Redux async middleware like sagas and observables. While similar to thunks in level of complexity and concept, it can replicate some common saga usage patterns. We believe that the listener middleware can be used to replace most of the remaining use cases for sagas, but with a fraction of the bundle size and a much simpler API.

Listener effect callbacks have access to dispatch and getState, similar to thunks. The listener also receives a set of async workflow functions like take, condition, pause, fork, and unsubscribe, which allow writing more complex async logic.

Listeners can be defined statically by calling listenerMiddleware.startListening() during setup, or added and removed dynamically at runtime with special dispatch(addListener()) and dispatch(removeListener()) actions.

The API reference is available at:

https://redux-toolkit.js.org/api/createListenerMiddleware

Huge thanks to @FaberVitale for major contributions in refining the middleware API and implementing key functionality.

Basic usage of the listener middleware looks like:

import { configureStore, createListenerMiddleware } from '@reduxjs/toolkit'

import todosReducer, {
  todoAdded,
  todoToggled,
  todoDeleted,
} from '../features/todos/todosSlice'

// Create the middleware instance and methods
const listenerMiddleware = createListenerMiddleware()

// Add one or more listener entries that look for specific actions.
// They may contain any sync or async logic, similar to thunks.
listenerMiddleware.startListening({
  actionCreator: todoAdded,
  effect: async (action, listenerApi) => {
    // Run whatever additional side-effect-y logic you want here
    console.log('Todo added: ', action.payload.text)

    // Can cancel other running instances
    listenerApi.cancelActiveListeners()

    // Run async logic
    const data = await fetchData()

    // Pause until action dispatched or state changed
    if (await listenerApi.condition(matchSomeAction)) {
      // Use the listener API methods to dispatch, get state,
      // unsubscribe the listener, start child tasks, and more
      listenerApi.dispatch(todoAdded('Buy pet food'))
      listenerApi.unsubscribe()
    }
  },
})

const store = configureStore({
  reducer: {
    todos: todosReducer,
  },
  // Add the listener middleware to the store.
  // NOTE: Since this can receive actions with functions inside,
  // it should go before the serializability check middleware
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().prepend(listenerMiddleware.middleware),
})

You can use it to write more complex async workflows, including pausing the effect callback until a condition check resolves, and forking "child tasks" to do additional work:

// Track how many times each message was processed by the loop
const receivedMessages = {
  a: 0,
  b: 0,
  c: 0,
}

const eventPollingStarted = createAction('serverPolling/started')
const eventPollingStopped = createAction('serverPolling/stopped')

listenerMiddleware.startListening({
  actionCreator: eventPollingStarted,
  effect: async (action, listenerApi) => {
    // Only allow one instance of this listener to run at a time
    listenerApi.unsubscribe()

    // Start a child job that will infinitely loop receiving messages
    const pollingTask = listenerApi.fork(async (forkApi) => {
      try {
        while (true) {
          // Cancellation-aware pause for a new server message
          const serverEvent = await forkApi.pause(pollForEvent())
          // Process the message. In this case, just count the times we've seen this message.
          if (serverEvent.type in receivedMessages) {
            receivedMessages[
              serverEvent.type as keyof typeof receivedMessages
            ]++
          }
        }
      } catch (err) {
        if (err instanceof TaskAbortError) {
          // could do something here to track that the task was cancelled
        }
      }
    })

    // Wait for the "stop polling" action
    await listenerApi.condition(eventPollingStopped.match)
    pollingTask.cancel()
  },
})

configureStore Middleware Type Improvements

Middleware can override the default return value of dispatch. configureStore tries to extract any declared dispatch type overrides from the middleware array, and uses that to alter the type of store.dispatch.

We identified some cases where the type inference wasn't working well enough, and rewrote the type behavior to be more correct.

TypeScript Support Matrix Updates

RTK now requires TS 4.1 or greater to work correctly, and we've dropped 4.0 and earlier from our support matrix.

Other Changes

The internal logic for the serializability middleware has been reorganized to allow skipping checks against actions, while still checking values in the state.

What's Changed

Since most of the implementation work on the middleware was done over the last few months, this list only contains the most recent PRs since 1.7.2. For details on the original use case discussions and the evolution of the middleware API over time, see:

PRs since 1.7.2:

Full Changelog: v1.7.2...v1.8.0