How to boldly adopt new technologies: RxJS & XState

RxJS and XState are two popular libraries in the JavaScript world that are used for managing and orchestrating complex asynchronous and state-based behaviors in applications. We adopted both libraries. How could we make such a bold decision, given that adopting just one tech stack is usually a big commitment?

Product Requirements

To create a Project View that combines the power of XL8's Sync (subtitle transcription and timecode injection), Translation, and Post Editing features, we had to think about a number of things.

  • How would a user start a project? They could start with a subtitle transcription or a translation.
  • The subtitles should be displayed alongside the video/audio. We needed to make subtitles active and have them automatically scroll to the appropriate position based on the current time of the media. This will make them easier for the user to recognize.

Unlike a typical web app, these features have much more challanging requirements. In order for MediaCAT to evolve into a more professional tool, we need to offer more options to users and that requires managing more states and events in our application. It's not easy for us to do this with the existing technology stack of React and React-Query alone.

  • Conditional statements with tons of boolean flags
  • Global/local data processing tangled in get/set hooks

Such things can make it challenging to maintain readability and ensure maintainability in products with large codebases, and we decided it was time to use another technical leverage point.

Requirement 1. Project creation modal

A modal pops up to give users options and let them choose.

Project creation options modal, the entry point.

Each option leads to a different modal window, where you can either continue or go back to the previous point (select an option).

(left) Start from transcription or (right) start from translation

Requirement 2. Integrating subtitles with media

Once settings for the media and subtitles are done, the UI changes to respond to the current time of the streaming video.


Now let's tackle each of these requirements.

XState

XState is a JavaScript library for managing state and state machines in applications. It allows you to model and visualize complex state transitions and behaviors in a declarative and intuitive way. XState is based on the concept of finite state machines (FSMs) and provides a set of tools for defining, interpreting, and managing state machines in JavaScript applications. It also provides features for handling nested states, parallel states, history states, and more, making it suitable for managing complex application states with robust error handling and transitions.

For the project creation scenario where the first requirement was

  • what state it was in
  • what state we need to get back to

to which state we need to return. In Figma, it looks like this

Now the front-end developer needs to model this flow in code. What do you think, it looks pretty similar, Huh?

Stateful changes are now in the form of declarative code!

What a waste of time it would be for a new developer to have to hunt down Figma and compare it to the code to understand the code flow, or to manipulate the UI locally. What if the code was written procedurally without Xstate? You'd have to jump back and forth between the variable declarations and the UI code (JSX), trying to remember the names and states of the variables, while also paying attention to where the changes are happening. The cognitive load makes it much harder to understand the code.

Once you've learned the basics of XState, it's much easier to understand the flow of the code by simply putting the code on the right (the machine definition) into the Visualizer. It's modeled as what you see.

Bonus: Performance

There's another freebie that comes with applying the above state machine to a React application. It's the use of the Context API.

https://xstate.js.org/docs/recipes/react.html#global-state-react-context

Personally, I'm a big fan of React's Context API. It serves as a combination of Dependency Injection (DI) and Boundary setting.

  • Modularization with DI gives you the flexibility and scalability to have loose coupling, meaning that if one component changes, it can change independently without significantly impacting other components. Loose coupling has the advantage of making your system more maintainable and scalable, and minimizing the impact of changes. We can simplify tests as well.
  • Providing declarative boundaries via <Provider> is also very helpful for maintainability. This is because the boundaries of the code are clear, which narrows the area that developers need to focus on.

However, this Context API has a fatal flaw. A single state change causes a performance penalty due to the re-rendering of all subcomponents. Fortunately, the  authService in the example code is a reference, so any change in its state will not cause unnecessary re-rendering of all sub-components. 🥳

RxJS(+Observable-Hooks)

RxJS is a reactive programming library for JavaScript that provides a set of tools and operators for handling asynchronous data streams. It is based on the Observable pattern and allows you to represent and manipulate streams of data over time, such as events, HTTP requests, or user interactions, in a declarative and composable way. RxJS provides a wide range of operators for filtering, transforming, combining, and managing streams of data, making it a powerful tool for handling complex asynchronous scenarios in applications.

As a side note, Angular has been supporting RxJS in the framework itself since version 2. React, on the other hand, is only responsible for Views, and only creates new DOM tree through React-managed state. This makes it difficult to use RxJS out of the box. Fortunately, we can synchronize stream data to React via the observable-hooks library.

Coming back to the time problem, the state of the video's current time changes in real time and needs to be handled. There's even data flowing in both directions. (Video ↔️ subtitles) In this case, the typical RxJS object,  Observable, is not enough. A special Observable object,  BehaviorSubject, is very useful: it's both an Observable and an Observer (check out the code below to see what I mean).

First of all, we need to watch for changes in the media.

Now we can detect changes in currentTime as the video plays..

Conversely, the following subscription is also possible in the Video Component.

Although not visible in the example code,  useMediaObservableState() used React.useContext() internally, and like the XState example, injected dependencies and created boundaries via Provider. Observable objects are also reference objects, and a change in value doesn't trigger unnecessary re-rendering of the subtree.

Conclusion

I've covered a relatively simple case with libraries and I’m still learning it. I think the important thing is to define the problem, find a solution, and move forward.

And a word of caution. When you adopt a library and it works well, you're happy coding for a while. Everything seems possible, and you're solving problems here and there with the help of the library, and then you hit a snag.

When all you have is a Hammer, everything looks like a Nail

As your codebase grows, it's easy to end up with a code structure that doesn't easily adapt to new requirements. Or you end up spending a lot of time making simple changes. It's tempting to swear at libraries and want to overhaul your project. But as long as you have principles and use it for the right use cases, it's a great tool and can last a lifetime. Here are some lessons learned

  • Consider XState for complex UI flows
  • Consider RxJS for managing state changes over time.

Let's make a list of principles, study them, and apply them to our projects to create flexible yet robust products!

Reference

Written by Phil Lee, Frontend Engineer


Need more information?

Feel free to reach out to us
and we'll get back to you within one business day.

Contact Us