State Management with Redux in LWC

One thing I miss about the react world whenever I work on a salesforce lightning project is state management. As applications grow in size and complexity, multiple pieces of state get scattered as they reside across different components and the interactions between them tend to become decidedly more entangled.

Redux is the most popular Flux implementation in the React ecosystem and enables the you to manage the state of your application as follows.

The entire state of your application is stored in an object tree inside a single store. Instead of mutating the state directly, you specify the mutations you want to happen with plain objects called actions. To specify how the actions transform the state tree, you write special pure functions called reducers that decide how an action transforms the application’s state.

Redux is view-layer agnostic, therefore it can be used with any front-end frame work through bindings (react-redux for React, ng-redux for Angular, etc.). I’ve started work on such bindings for Salesforce proprietary Lightning Web Components (LWC) framework and I’m calling it lwc-redux.

LWC Redux

The library provides two key modules. The connect() function to manage the store interaction logic and the provider component to wrap the application with. If you’ve used React Redux most of this will seem familiar to you. It’s worth taking a look at the documentation for React Redux since the first two arguments to connect(), mapStateToAttributes and mapDispatchToAttributes mimics the behavior of their corresponding analogues mapStateToProps and mapDispatchToProps in React Redux.

However, there are key differences between how React and LWC works and consequently, the connect() function and provider component in this project behave very differently from those in React Redux.

Provider

In React Redux, the Provider component takes in the store as a prop and houses it there. It then allows any nested component to have access to it through the connect function.

The Provider component in this project works differently. First, it loads the redux and redux thunk static resources and then calls redux methods to generate the store and makes it available to any component by making global stores. It does this by creating a field called reduxStores on the global window object and puts the created store there keyed by the name of the store (the name of the default store is 'redux'). Keying the store by name enables you to create multiple stores (although you probably shouldn't do that unless you have very good reasons to do so).

For multiple apps to access the same store, each app needs to be wrapped in a provider component. Of these, only one can be a primary provider (this is the one that will create the store and hence will require the reducers attribute to be passed in) while the rest are secondary (marked using the secondary flag). The secondary providers do not create a store. Rather they look for a global store that the primary provider generates and once the store is found, it renders that app it houses. This is done through polling.

Because the stores are global they can be accessed from anywhere but you should only access them through the use of the connect() function(Nothing is going to stop you from doing otherwise, of course. But then, nothing stops you from making mutations to the redux state either).

This decision to house the redux store on the global window object may seem like a bad practice; however, since LWC doesn’t have an equivalent of React’s Context Api, this is the only way to make the state available to components nested within the Provider component without having to pass it down each level. In practice, passing the state down tends to become extremely cumbersome as applications grow large with lots of components and complex component hierarchies.

Also I’ve decided to place the store creation logic in the Provider component since generally you only do this once on an app and third party libraries are loaded asynchronously in Salesforce through static resources. You can, of course, change the behaviour of this as you see fit or decide to do away with the provider component completely. You can just load up the Redux library and create a global store on one of your components and then just use the connect function in this library to access it. This probably seems like a very opinionated way of creating the store but then, Salesforce is extremely opinionated. I do intend to make changes to the provider component to allow more flexibility but for now this can be thought of as an opinionated example.

Provider Attributes

store-name - Name of the redux store (defaults to 'redux')

reducers - A reducing function that returns the next state tree or a map of reducer names and their corresponding reducing functions (if such a map is used, be sure to set the use-combine-reducers flag as the reducers will need to be combined using the combineReducers method on Redux)

initial-state - The initial state that you may optionally specify to preload the state

secondary - flag to indicate if the provider is secondary. If secondary, all the provider does is wait till it finds a store that's been generated by a primary (non secondary) provider, and then renders its children.

use-combine-reducers - flag to indicate if combineReducers need to be used

use-thunk - flag to indicate if thunk middleware should be used

use-logger - flag to indicate if a logger middleware is to be used (currently the logger middleware is a simple one I've included in the library that just prints out the action dispatched and the resulting state from that action on the console)

Example Usage

<c-provider reducers={reducers} use-combine-reducers use-thunk>
<c-app></c-app>
</c-provider>

econdary provider (here, c-app-two uses the same store as c-app)

<c-provider secondary>
<c-app-two></c-app-two>
</c-provider>

Connect()

The connect() function connects a React component to a Redux store. It provides its connected component with the pieces of the data it needs from the store, and the functions it can use to dispatch actions to the store. In this way it is similar to the connect() function in React Redux. However it differs in the following way.

In React Redux, connect() does not modify the component class passed to it; instead, it returns a new, connected component class that wraps the component you passed in. It does this by using a pattern called higher-order components, which emerges from React’s compositional nature. I have not found any way to replicate this behaviour in LWC when hosted inside salesforce (there is no documentation on dynamically creating or passing attributes to components). Hence the connect() function returns a function that takes in the component that needs to be connected and modifies the component in question.

Also, the connect() assumes that by the time it is invoked, the redux library and the store have been instantiated on the window object. This means that it should be called from a components nested inside the provider component. That way the provider component would have made the global variables available for any of its children components that want to connect to the redux store. The ideal place to call it is from the connectedCallback() function of the LWC component lifecycle hooks which is called when the element is inserted into a document and this hook flows from parent to child.

Connect() Parameters

mapStateToAttributes - a function that takes in the state and returns a map of the selected state keyed by names of the attributes they will be attached to in the component

mapDispatchToAttributes - a function that takes dispatch and returns a map of actions keyed by names of the attributes they will be attached to in the component

storeName - the name of the store to connect to (defaults to redux)

Connect returns a function that takes in the component that is to be connected

Example usage

import { LightningElement, api, track } from 'lwc';
import { connect } from 'c/connect'
import { setVisibilityFilter } from 'c/actions'
const mapStateToProps = (state, ownProps) => ({
variant: ownProps.filter === state.visibilityFilter
? 'brand'
: 'neutral'
})
const mapDispatchToProps = (dispatch, ownProps) => ({
handleClick: () => dispatch(setVisibilityFilter(ownProps.filter))
})
export default class TodoFooter extends LightningElement {
@track variant
@api label;
@api filter;

connectedCallback() {
connect(mapStateToProps, mapDispatchToProps)(this);
}
}

This project is still in very early stages and is likely to go through significant changes as I plan to optimise performance, add more features, and make it less opinionated over the coming weeks. Also, I’ll be adding unit tests to the repo and examples of how you’d go about testing your own components connected to Redux. Testing components that aren’t connected to redux is the same as the examples in LWC documentations. For connected components, the reducers and action creators can be tested separately and so can the mapStateToAttributes and mapDispatchToAttributes functions.

You can check out the code or deploy it to your org from here.