DEV Community 👩‍💻👨‍💻

Luiz Américo
Luiz Américo

Posted on • Updated on

From React to Web Components: using mobx

Mobx is a popular JavaScript state management library. Most of its documentation and examples revolves around React but at its core is framework agnostic and can be used for any web application.

I looked if and how i could use Mobx together with web components/LitElement, my favorite client side view stack.

Since simple, "todo app" like, demos do not show the full benefits, and possible drawbacks, of such technology, i decided to convert RealWorld React/mobx demo, a reasonably complex application.

The React Escape Route

The conversion from React to LitElement is not technically difficult, but is bit annoying since many of it must be done manually. Below are the steps to get the job done:

Build setup

The original app is built with Create React App which, obviously, does not work for web component project. A custom (minimal) webpack setup was used instead. The most complex part is to configure decorator support in babel:

{
  "plugins": [
    ["@babel/plugin-proposal-decorators", { "legacy": true }],
    ["@babel/plugin-proposal-class-properties", { "loose": true }]
  ]
}

HTML Rendering

LitElement uses html tagged template instead of JSX language for HTML rendering. The concepts are similar and seasoned React developers should not have issues making the transition. This tool can automate most of the conversion.

JSX:

<ul className="error-messages">
  {
    Object.keys(errors).map(key => {
      return (
        <li key={key}>
          {key} {errors[key]}
        </li>
      );
    })
  }
</ul>

lit-html:

html`
  <ul class="error-messages">
    ${Object.keys(errors).map(key => {
      return html`
        <li key=${key}>${key} ${errors[key]}</li>
      `;
    })}
  </ul>
`;

Mobx Integration

Mobx integrates with React through mobx-react package. It provides a way to re-render components on state change (observer decorator) and a specialized dependency injection system to expose state to components (Provider component + inject decorator).

For LitElement, lit-mobx provides a way to render components on state change and wc-context is used to expose the state to components.

Routing

Personally, i prefer to define the app router / routes in a separate data structure (out of component tree), but for sake of simplicity @stencil/router is used because its API is very similar to react-router one. Update: replace by slick-router, a smaller and more powerful solution.

Testing

The original demo does not have tests but it was something i really wanted to see how would work. I choose the test setup provided by open-wc which uses karma with a configuration tailored for web components and ES modules. The open-wc testing helpers are really helpful, something to look forward. Unfortunately, the setup breaks with third party packages distributed as commonjs modules without an effective workaround. Update: by using import-map support, i managed to get tests working.

Conclusion

Mobx works well with LitElement. There was no need to change the state management code at all to get the demo working. Also the Mobx / LitElement integration through lit-mobx works as expected, no issues founds in this aspect.

The converted code can be found here alongside a live demo and the bundle size composition.

Some notes

  • mobx-devtools works with a web component app!
  • People should really analyze the bundle output. The Promise.prototype.finally shim increases bundle size by 43Kb (minified) due to a dependency from which only three functions are used.
  • @stencil/router did the job but i would not recommend for non stencil projects since it brings together the stencil stack adding 62Kb of minified code. Also it lacks advanced features.
  • Mobx implements only the "legacy" decorator spec, so any project that uses the new spec is out of luck. Not Mobx fault at all because the decorator state is a great mess at this moment.
  • Testing web components apps is a Far West like world without an out of box solution. open-wc setup works only with projects where all dependencies are ES modules, something not realistic in many cases (like that). Jest does not support web components at all (and have no signs of doing so). The devs are left in the wonderful world of configuring karma with webpack / mocha / chai etc.

Top comments (4)

Collapse
mdgbayly profile image
Martin Bayly

Thanks for putting this together! Out of interest, what were your reasons for converting the MobX version of the React RealWorld app rather than the Redux version.

Mostly this is a round about way of asking if you have a preference for using MobX vs Redux with LitElement :-).

Or did you just fancy trying your hand at MobX or did you already have experience with it?

Collapse
blikblum profile image
Luiz Américo Author

Some context: i came from a Delphi (Windows desktop) development background but, in the last years, i invested in web development. Initially in Backbone/Marionette which have worked well for personal projects and auxiliary apps.

We have a project to migrate our medium to large sized, commercial, desktop application. Since involves third party investment with roadmap, deadlines we need a not niche tech stack which contracted programmers can work on without much training.

So i needed a mainstream state management to work with web components (this choice is another history). It does not have many. Backbone has lost mind share, although i can ensure it can do The Job Done. Ember and Ember Data is all or nothing. Angular also lives in its own world. This leaves the React friendly state managements.

I looked into Redux which is by far the most used, with its merits, and easier to have devs to know how to work with it. I really tried to switch my mind to use redux but every time something made me step back from it. Sometimes the boilerplate, sometimes the need to everything (like routing) be managed by redux, sometimes the number of choices to be made...

Mobx is another alternative which sounds less intimidating. So i used this conversion to test how would work in practice. So far has met the demands of allowing to create modern (Reactive) UI with a manageable testable state.

BTW somethings in the RealWorld architecture, inherited by React version, i would do differently like decoupling the state update from the components.

Collapse
mdgbayly profile image
Martin Bayly

Thanks for the reply. We're also looking into MobX as a solution in comparison with Redux which we find from experience leads to too much boilerplate.

So far I'm intrigued by MobX and based on my initial efforts to integrate into some of our components, I find it quite straightforward to use.

I am a little bit concerned about the 'magic' it is doing compared to Redux which is much more straightforward. But maybe I just need to dig under the covers of MobX a little.

I'm interested if you could clarify what you mean by your last comment:

... I would do differently like decoupling the state update from the components

In general, my experience with building React/web components using state management solutions like Redux/MobX is that they lead you to create components/state that are unfortunately tightly coupled to a specific application scenario and hence are not as easily reusable by different applications.

We work on a large web application that can be thought of as being comprised of many smaller applications/pages and we want our components to be re-usable across many of those smaller applications. So I'm focusing on how we can use something like MobX to allow state to be managed by components without making too many assumptions about how the components might be used together. But it's not sufficient to just keep state in each component as there are scenarios where we do what state to be shared if the components are used on the same page/application part.

Thread Thread
blikblum profile image
Luiz Américo Author

I'm interested if you could clarify what you mean by your last comment:

See the code below:

github.com/blikblum/lit-element-mo...

I would write as:

  connectedCallback() {
    super.connectedCallback()
    // this could be removed
  }

  shouldUpdate(changedProperties) {

    // probably also not necessary here since route change could be handled elsewhere 

    if (changedProperties.has('username')) {
      this.trigger('username:change', this.username)
    }

    if (changedProperties.has('pathname')) {
      this.trigger('pathname:change', this.pathname)
    }
    return true
  }

The responsibility to call e.g. articlesStore.loadArticles() should be in a layer above what many call the orchestrator layer. I like having the route as this layer (not component based) but can be separated classes.

With this architecture is easier to test the component (just ensure calls the events when appropriate).

This is the core principle of data down, events up pattern

Other things to improve decoupling: move the stores to a instance property instead of a context property and listen to route changes elsewhere

The architecture of the demo is contaminated by React thinking that everything is handled in a component (routing, data loading etc) one of the things why i don't like React.

You can look what i mean by using route like the orchestrator here

🌚 Friends don't let friends browse without dark mode.

Sorry, it's true.