LWC Reactive State Manager



If you started to create LWC applications, you might have found that managing the state of a component is trivial, thanks to the reactive properties and the @track decorator. You do not have to deal with a specific state property, or explicitly ask the component to render when its state changed.

Now, when your application gets more complex, you often want this state to be shared with other components. This is when state managers, like Redux or MobX, come into play. As these libraries are technology agnostic, you can obviously use them with LWC components. To make it easier, you can even create a few helpers, typically wire adapters.

But can we create a store manager that is closer to LWC, leveraging the core LWC capabilities to provide global state management?

Towards a Reactive LWC Store

First, the code described bellow is available as a library published here: https://www.npmjs.com/package/@lwce/store. The source code is on Github: https://github.com/LWC-Essentials/wire-adapters/tree/master/packages/store.

The idea is simple: we can have a global object, the "store", that holds [key:value] entries. Then, we'll use a wire adapter that lets a component access any entry in the store using a key:
  @wire(useStore, {key: 'user'}) user;

That's it. The user property is "reactive" so any change to its content will be tracked by the component. Multiple components can share the same entry coming from the store, by using the same key.  Even pure JavaScript code can access an entry programmatically:
  const u = getEntry('user')

To make all the entries reactive, we need the global store object, holding all these entries, to be itself reactive. The LWC component reactivity is provided by a specific membrane, called the reactive membrane, which is private to the runtime. The solution is then to create an invisible component that holds the whole store object as a reactive property. This component is silently created when we initialize the store using:
  initStore(content?: object)

The  content parameter is optional, just in case we want to initialize the store with initial values. It can be useful, for example, when the store is pre-populated from server side rendering results.

Adding Entries to the Store

Accessing a store value using the @wire or the API is simple, but how do we add an entry to that store? Well, the answer is straightforward: lazily, when we need it. In other words, an entry is created in the store the first time it is requested by key. And, right after being created, it gets initialized. For this, the developer can register initializers that will be invoked during this step:
  registerInitializer(
    (key) => {
        if(key==='user') {
            return {name: 'Doe', first: 'John'};
        }
        return undefined; // not for this entry
    }
  );


If the initialization data is not immediately available, for example because the data comes from a network request, an initializer can return a Promise. The store will consume the value when the promise will be resolved. By the meantime, the consumer can check some boolean values to understand the status of the entry, see bellow.

Rich Entry Content

Entries in the store are actually not just exposing the raw data, but an object with the following properties:
  {
    data: any
    error: string
    loading: boolean
    initialized: boolean
  }

So in our example above, the user content have to be accessed through the data property:
   const u = getEntry('user').data

Similarly in a component template:
  <p>{user.data.firstName} {user.data.lastName}</p> 

Because an entry contains {error, loading, initialized}, a consumer can also check if:
  • An error happened during initialization. error will then contain the error message.
  • The data is still being loaded
  • The data has already be initialized once
The difference between loading and initialized is subtle: as a store entry can be reloaded, loading can happen while data has already been loaded once. In that case, the developer can choose to provide a slightly different UI (ex: the previous data is still displayed with a loading icon while the entry is being reloaded).

Putting the Store in Action

The sample-app accompanying the library on Github shows a commerce cart built using this library: https://github.com/LWC-Essentials/wire-adapters/tree/master/packages/sample-app

All the components (cart, checkout, basket icon) are sharing the same 'cart' store entry. They all update when cart changes:


As all the store entries are reactive, simply changing some values within the cart makes all the components re-render.


Comments