Scale By The Bay 2019: Kavita Laddad, Taming complex webapps with Scala and React
Recording: Scale By The Bay 2019: Kavita Laddad, Taming complex webapps with Scala and React
Kavitha Ladard, Co-Founder of Pyre Labs Hi everyone, I'm Kavitha Ladard. I'm co-founder of Pyre Labs. For the last couple of years, we have been working on a complex web application that uses Scala and React. Today, I will narrate my experience using these two technologies. So our product LearnRaga, it's an online learning platform for Indian classical music. It has been developed in Scala, both back-end and front-end. In my talk today, I'll mostly focus on the front-end part of it. So before we delve deeper into the technical stuff, a bit about Indian classical music
has these two pillars, rag and tal. Rag is a melodic structure that can be played on instruments like a flute or sitar. Or if you are a vocalist, you can sing. It is roughly defined as a set of notes, their relations to each other, and approach of how to transition between these notes. Tal, on the other hand, it's a rhythmic structure that can be played on percussion instruments like tabla or pakhavaj. These two concepts, they come together in the form of a composition in Indian classical music. Let me show you a composition. Here
So here on LearnRaga, we have this composition editor, where you can enter notes for a composition. You can enter lyrics for it. can be done either through a piano that we provide, or you can just type notes through keyboard. This particular composition, it's in Rag, Yemen, which has these notes. are something similar to Dore Mi Fa Solati in the Western world. In addition to that, it is in Tal, which is a rhythmic structure of a cycle of 16 beats. So you can play this composition. You can play along with this composition
I don't know if you can hear the sound. So, this is one way you can play this composition, you can play along with it. We also have a different view where we show this composition in the form of a chart. Here, we plot these notes like this. If you look at these first three notes, Ni, Da, Pa, we plot this on a chart where frequencies of these notes are plotted against the Tal, that is the 16-bit cycle. So these first three notes, Ni, Da, Pa, they are going in ascending order. And this note, Pa, it spans over two beats. So, if you, a typical user on LearnRaga, they would put on their headset and sing along with this composition
So when they sing, we plot their actual pitches on top of what is expected. So, traditionally, Indian music has been taught in a very personalized setting, where students used to go and stay with their teachers to learn this music. But this is very difficult in today's modern times. These days, people usually take lessons maybe once or twice a week from their teacher. For example, me and my co-founder, we are students of Indian classical music. We take lessons from our teachers who are in India over Skype once a week. So, when we are in our class during that one-hour session, our teachers are there to help us, they guide us, they tell us if we make any mistakes. But what happens when we are practicing on our own? Many times, we are lost
This is how my co-founder looks when he is practicing on his own. So, we are lacking that whole traditional way of teaching, where teachers would always be there to guide you. To fill in that gap, we started LearnRaga. On LearnRaga, we provide lots of tools, like the composition editor we just saw. You can play that composition. You can play along with it. You can share that composition. We provide real-time pitch feedback at every place in it
In Indian classical music, you are supposed to practice these patterns over and over again to get pitch accuracy. We provide thousands of those patterns. And in addition to that, Indian classical music, it's mostly about improvisation. So, we provide a practice room where you can go and practice your skills. We have these accompanying instruments like percussion instrument, tabla, and drone instrument, danpura, at your fingertips. So, all these functionalities that I'm talking about, they are highly interactive in nature. And we wanted this interaction to mostly happen on the client side. So, we developed a single page application
Here is the architecture of our system. We have API server at API LearnRaga.com, web server at LearnRaga.com. We have multiple instances of both of these to take care of any failures or scalability if needed. We serve most of the static assets from our web server, like the HTML, JavaScript, CSS. The JavaScript here, it's the React code. When user tries to visit our site, we return index.html. This index.html, we have reference to some JavaScript, some CSS, but no visual element in there. We return this index.html, which in turn loads the JavaScript
Once the JavaScript is loaded, then React takes control of the system. To render a component, if it needs to talk to API server to fetch some data, it talks through REST and GraphQL. The API server, in turn, it talks to services like Mailgun, Amazon's S3 service, where we have some of our audio files, like the TANPURA flute sound and Tabla sound that's stored there. And most important, the Postgres database, where we have the user data. Now API server features that data, and it returns it back to the client. And once React receives it, it renders the component. We have a single page application. To improve the initial performance, we have added some server-side rendering code, which we will look at later during the talk
This JavaScript code here is generated by Scala.js. It's the React code, which is generated by Scala.js. So for this, to tame this complex application, we need good technologies. So we are using Scala on the back end and Scala.js on the front end. With Scala.js, we are using React. is a JavaScript library. Both Scala and React, they highly promote immutability and functional programming. So these two things worked very well together for us
But React is a JavaScript library. So how do we use it? We use Slinky framework. Now this Slinky framework, it allows us to write React components in our Scala code. So if you look at the Slinky code, it is very much similar to how you would write your JavaScript components. Along with these, we use, for some of our performance-sensitive code, we use WebAssembly with Rust. And being a small team, we try to utilize all the libraries that are available out there, both in Scala and React world. So we have all these libraries here, especially the Scala tests. So having Scala on both client and back end, it helped
So we have some algorithms that we need to run on both the back end and on front end. So we could write these common Scala tests, which we could run in JVM and in JS. Now let's look at these technologies. So React. We know that React is a mapping from properties to component. Here we have a simple component. It's a play button component from our system, where we have this property play command we receive. We use the play command that kind to show a particular tooltip
It's a simple functional component in React. So properties are passed to a React component, but state is local to a component. React creates VDOM representation for it, and then the final DOM representation. Now these properties are passed on from parent component to child component, and that's how the properties can change. But the state here, it is local to a component. It can change because of some stimuli, like some user interaction, like I pressed button in my composition editor, or some event happening or timer going off. When any of these things happen, and when property or state changes, React recomputes the VDOM representation. It compares this new VDOM representation with the old VDOM representation, and reconciles the changes into final DOM
Here we have a simple audio settings model component where we use state. Let me show you this component on Landraga. So this audio settings model here, where user can come and change the audio settings for the system. So when user clicks on these tabs, we change the contents that are shown. So how do we do it? We have here, we are using a state, a hook, use state, for the tab index. We change this tab index whenever user clicks on any of the tab. We also use this tab index to determine which tab is active. We pass that active property to the tab component
One of the great benefit of React is its ecosystem. There are lots of JavaScript components that have been developed by people. These are open source libraries. And we wanted to utilize these libraries from our Scala code. For that, both ScalaJS and Slinky, they help us. We can write external components to use these JavaScript libraries from Scala. For example, here I have a component, a React tag autocomplete component, which is a JavaScript library. When we want to use any JavaScript library, we usually need to write this facade, which is a bridge between the typed system of Scala and untyped system of JavaScript
So when we need to do it, we usually look at the documentation that is provided in the JavaScript world and try to map those things into Scala world. So here, these tags, it is defined as optional property in JavaScript. It is an array which has these objects with properties ID and name. For that, we have defined here properties, which has tags. It is defined as js undef are of sequence of React tag. Now, we are defining it this way because it's optional property. So user may select to not pass this property. So what this means, js undef are
It means that this property will be either undefined or it can be a sequence of React tag. Again, this React tag, it's a type defined by us in the Scala world. So by defining all these types, it becomes easy and concise and error free when we use this component from Scala. So now some of the properties are defined as js undef are and some of the other properties like this on addition or on delete. These are defined as functions from React tag to unit or int to unit. This is because these two properties were defined as required properties in the JavaScript world. So this is how we map a component and how do we use it from our code just like any other slinky component, React component we would have written. So a JavaScript library available to us in Scala
How do and this is how we use it. React functional component, it's a pure function. We are not supposed to do any side effective things in effects in it. Side effects like maybe subscribing to some service or fetching some data or even modifying DOM if needed. So for those purposes, we can use effects in React. So whenever properties or state changes, React renders the component. After rendering a component, it looks at the component and sees if there are any effects defined in there. can be multiple effects defined in a component
If there are any effects defined, then React goes and executes those effects. These effects may have some dependencies. This can be anything state or properties. Now, if there are dependencies defined, then React looks at it, checks these dependencies. If these dependencies have changed between the renders, then only these effects will get executed. So the same play button component from earlier. We need access to audio state here so that we can display appropriate pause or play button. But this audio state is maintained in, is managed by audio service in our application
So how do we get access to the audio state in this play component? We write effect. So this effect, it subscribes to the service, audio service. So we subscribe to audio service, and audio service will let us know whenever the audio state changes. When the audio state changes, we change the local state of this component here. So in effect, you can also pass a cleanup function where we are unsubscribing. And then the dependency sequence. Here we are passing sequence empty. So if we had omitted the dependency sequence here, that means that this effect will have to be executed every time the render happens
And that will be, would be overkill for subscription purpose. So when we pass here, the sequence empty, what that means is it is constant dependency in between the renders once the component has been mounted. So this particular effect, it will execute the subscription part on the mount. It will execute the unsubscription. This cleanup function will get executed on unmount. Now, if you are familiar with the class components in React, there you would write this subscription and unsubscription logic in two different lifecycle methods. Component did mount and component will unmount. This same audio state is required by many other components in our system
So it's same copy-pasted code, but we can do better. So we have extracted, refactored it into a hook. So here we have play hook, the same code. Now, this refactoring was possible just because of the effects. If we were using class components, this refactoring wouldn't be possible. Now, this both subscription and unsubscription logic, it relies in one place. It is clean, clear, and we won't forget to unsubscribe from the service and create any memory leaks. So we have a few other things
So it doesn't need to worry about the audio state. It just can worry about its business logic. Now, this particular hook was very specific to our needs. But there are other hooks, like we have a window listener hook, where we want to know when user resizes the window. Some of these hooks, which can be applicable to any application out there, we have made them open source. They are available at this URL. The last topic I'm going to cover is about performance. We have a single page application
So we wanted to see how we can reduce the work that React needs to do every time any state or property changes. We know that whenever the properties change, React needs to recreate the VDOM representation. Why do we need it? Let me show you the example. So here in composition editor, when I'm playing the composition, we show this highlight, which moves from one cluster to another. This composition editor, it has a hierarchy of components. The top level element is the composition editor, where we maintain the state. Inside composition editor, we have these sections. There can be multiple sections in a composition
Now, we have these sections. Each section, it has this line editor, these lines. And then each line has a cluster. Now, whenever the state changes, which is maintained at the composition editor level, React needs to reevaluate the whole hierarchy to compute the VDOM representation. Now, just to change this highlight from this cluster to this cluster, it's a lot of work to recompute the whole hierarchy here. There's all these thousands of clusters here. So how can we make it better? So we use React Memo. it's a higher order function, where you can wrap your component inside React Memo and pass it a function
This function is, it takes old properties, new properties, and returns a Boolean. Now, when React comes to this function, it checks. If this function returns true, then it says, just use the memoist value. Do not need to recompute the VDOM again. Whereas, if it returns false, then only in that case it will compute the VDOM representation. So for our case here in the composition editor, when we are playing, this play highlight is moving. The play state is changing at the composition editor level. Now, it comes to composition editor
It needs to render all these components. So it checks for this. It needs to render the sections first. So it comes to the first section. We have memo at the first section. So it checks if this particular change, is it relevant to this section? Which is in this case. But for the rest of the section, it would say, no, nothing is changing. Just use the memoist value
So we had already avoided the computation, that evaluation of the rest of the sections in this composition. Now it just, this particular section. It comes and checks for each line here. The first line would say, yes, things have changed. But the rest of the lines here would say, no, nothing has changed. And just use the memoized value. Now when it comes to the first line, these clusters from first to ninth, they would say nothing has changed. So just use the memoized value
But the cluster 10th and 11th, where the highlight was there earlier and where the highlight is there now, those are the only two clusters that need to be re-evaluated. So from these multiple sections to thousands of clusters, we have reduced React's work to just these two clusters here. And that's how we improve performance in this. And how do we use React memo? Here is the section editor component. So we wrap our component inside the React memo. We pass this function here from old properties, new properties. And it's a bit involved function. So we have written it inside the props
So now about improving first-time user experience. We have a single page application. So we do have, we do cache our assets like the images, JavaScript, CSS. But this is useful only second time onwards when user visits our site. What happens first time when they visit? Being a Scala.js application, we have a bit of disadvantage where Scala.js right now doesn't allow bundle splitting. So we have this 800 kilobyte JZP JavaScript that needs to be loaded before anything is displayed on the page. So to improve that, we have added server-side rendering. Now remember the same architecture diagram from earlier
So before using server-side rendering, the index.html, it didn't have any visual elements in there. So it would just, on slower networks especially, it would just show this blank page. And to improve that, we added server-side rendering where this server-side rendering code, it's the same code, the same React code that's there in the browser. But there is no DOM to render into on the web server. So here we ask React to render the component into a string. And we put that string representation into our index.html. And now we return that index.html. So our page is displayed almost instantaneously
What is not working, what we need is the interaction. So once the JavaScript code is loaded, then the interaction comes, the page comes alive, like the buttons for login, et cetera. For some of the performance-sensitive code, such as the DSP algorithms that we use for pitch detection, we are using WebAssembly with Rust. So here in this... So here we have this component. Before using WebAssembly, so here as I'm speaking, it's just plotting my pitches. If I were to sing, I'll try singing a bit. I'm a really beginner to Indian classical music, but let me try
That way you get the real feel what happens. So here you can go to the other words. So here you can go back and see how you were doing. So you can see that I wasn't approaching the notes correctly. Or you can see that I did a little better when I was coming down, but not as well when I was going up. So now in this component, we need to grab the audio that user is producing, process that data, compute the pictures, and plot it back onto the graph. And this whole thing needs to be done in 60 frames per second to get this smooth experience. So we used to, we had earlier it was all Scala code, Scala JS code
So we used all the collection library that was available. We used good data structures. But that Scala JS code was getting converted to JavaScript. And it was the JavaScript that was running in the browser. And then JavaScript VM has its own limitation when it comes to performance. So we converted some of our DSP algorithms to WebAssembly. We used Rust Wasm to compile that Rust code into WebAssembly. When it compiles that code, it also creates a JavaScript file, which gives us access to these WebAssembly functions
And that JavaScript is the one we use from Scala JS. And we get this wonderful performance now. Here are some of the libraries that we are using in our application. And with that, thank you all for coming to my deck. Really appreciate it. Thank you. We have time for questions. Any questions? Hello
When you handled errors, did you use anything like the try blocks or either or? You know, when you had errors, how did you handle errors? in the sense where, what are you referring to? Sorry. Exceptions. You know, Scala has the try, T-R-Y, and either, you know, to capture errors. I wondered if you had embedded those somewhere. Yeah. So you're saying how do we do try, catch, and that is what it is. Yeah, catch exceptions. Yeah, yeah
We do handle those in case if something fails. And is that the question? I am not sure. I can talk to you later maybe. Anyone else? Thank you, Kavit. Thank you.