Redux Notification Enhancer
by Vytenis Urbonavičius
Redux Notification Enhancer (RNE) drastically improves performance of some Redux-based applications by providing control over subscriber notifications after state updates.
Main features:
- Notification throttling - if enabled, it makes sure that subscribers are allowed to finish handling state changes before a new notification is triggered. By default Redux is notifying all subscribers on every state change. Most of the time it does not require a significant effort to introduce throttling with RNE into an existent project.
- Manual notification control - when used, it allows to mark certain actions as passive - they would update state but avoid notifying subscribers. When introducing manual notification control into existent project, it can be done gradually.
Above optimizations become even more impactful when used together with @reduxjs/toolkit. Its thunk operations always trigger multiple dispatches whenever actions are pending, fulfilled or rejected. If RNE is not used, these auto-generated actions are causing notifications even if they are not necessary (i.e. thunk action is only dispatching other actions).
WARNING:
RNE by design breaks default lifecycle of Redux and increases complexity of code. Only use in projects where performance is critical and cannot be achieved by other optimization techniques. Please see "Troubleshooting" section for more details.
Example use case
When using Redux with React, React performs render on every Redux notification for every single state change. If a very complex system is developed and render becomes heavy despite being well written, optimized using memoization and other techniques - it becomes important to reduce amount of renders to achieve much higher browser frame rate.
This can be achieved by:
- Enabling throttling and making sure that browser draws frames between renders.
- Identifying actions which should not cause render all by themselves and marking them as passive.
Integration
Simplest way to include RNE into Redux is by passing it as a second argument to createStore.
const {createNotificationEnhancer} = require('redux-notification-enhancer')
// See "Options" to see what "options" argument may contain
const {enhancer} = createNotificationEnhancer(options)
const store = createStore(reducers, enhancer)
If you need other enhancers such as applyMiddleware, you can provide them using compose()
function provided by Redux.
const {enhancer} = createNotificationEnhancer(options)
const multipleEnhancers = compose(applyMiddleware(thunk), enhancer)
const store = createStore(reducers, enhancers)
Note that these days it is often advised to use @reduxjs/toolkit instead of pure Redux. In case of @reduxjs/toolkit, integration is performed using configureStore
:
const {enhancer} = createNotificationEnhancer(options)
const store = configureStore({
reducer,
// ...
enhancers: [enhancer],
})
Usage
RNE provides function for marking actions as "passive" or "immediate". These functions modify action "type" string by adding special prefixes.
Passive actions are actions which should modify state but should not notify subscribers.
Immediate actions are actions which should be executed at once even if throttling is enabled.
If no marker is used - action is either throttled (if throttling is enabled) or executed normally as in standard Redux without RNE.
const {enhancer, passive, immediate} = createNotificationEnhancer(options)
// Alternatively use configureStore if used with @reduxjs/toolkit
const store = createStore(reducers, enhancer)
// Throttled if throttling is enabled
store.dispatch({type: 'NORMAL_ACTION'})
// Changes state, but does not notify
store.dispatch({type: passive('STATE_CHANGE_ONLY')})
// Executed at once even if throttling is enabled
store.dispatch({type: immediate('THROTTLE_BYPASS')})
Options
Options can be provided as an optional object argument for createNotificationEnhancer
. Object keys and values are explained in the table below:
Option | Default | Meaning |
---|---|---|
throttle | false | Makes sure that promises of subscribers are resolved before notifying them again. |
requestAnimation | true | When throttling is enabled it requests animation frame after subscribers are notified. Should be disabled if used not on browser. |
prefixes | {passive:'S%', immediate:'I%'} |
Allows changing one or all marker prefixes to custom ones. |
Response of createNotificationEnhancer
Key | Meaning |
---|---|
enhancer | Enhancer itself to be passed to createStore of Redux |
passive | Function for modifying action type and marking that action as "passive" - only impacting state but not notifying subscribers. |
immediate | Function for modifying action type and marking that action as "immeditate" - to be executed at once even if throttling is enabled. There is no need to use this marker if throttling is disabled. |
getNotificationPromise | Returns promise of current notifications. This promise is resolved once all currently notified subscribers complete their work. There is no need to use this promise if throttling is disabled. |
Example use case for getNotificationPromise:
If React is used together with Redux and RNE with throttling enabled, React renders may happen slightly later than expected. To ensure that last render has completed before further operations, getNotificationPromise can be used:
// Perform some code which causes render here
// ...
await getNotificationPromise()
// ...
// Perform code which needs to read DOM or depend on render in some other way here
Troubleshooting
Problem:
State changes are not reflected in another part of the application (React-based UI for example).
Solution:
- Make sure your actions are not marked as passive.
- If using on browser, make sure that requestAnimation option is enabled.
Problem:
My actions trigger notifications too rarely (causing lagging animations for example).
Solution:
When throttling is used, notifications are delayed until all subscribers complete their work. In many cases this improves performance drastically. However, in some cases actions do not need to wait for all subscribers - you can try adding exceptions for certain actions by marking them as "immediate".
Problem:
Subscribers are notified often causing lag and excessive calculations.
Solution:
RNE throttling is designed to tackles this issue. However, it is disabled by default. Make sure you enable throttling feature by passing a "throttle" flag as "true".
Problem:
I am using React and my code depends on DOM which should change after dispatching an action. However, DOM does not change immediately after dispatching that action when using RNE.
Solution:
Request last render promise using getNotificationPromise
and wait for it to resolve before performing operations on DOM.
Happy Hacking!!!