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.
Each option leads to a different modal window, where you can either continue or go back to the previous point (select an option).
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.
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.
There's another freebie that comes with applying the above state machine to a React application. It's the use of the Context API.
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. 🥳
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.
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!
Written by Phil Lee, Frontend Engineer