Jacek Pudysz

Do you even need to re-render? The secrets of Shadow Tree and Unistyles 3.0

Transcript

Jacek

Hello everyone. So you already saw one presentation from Margello. Hanno presented you the concept of shadow tree and now someone also asked about the real world example. So here it is. Do you even need to re-render? Hi, my name is Jacek Budysz. I'm a CTO at Codemask, a company based in Poland. And here are my handles if you want to find me on GitHub or on X. You may also know me as the author of UniStyles. And today I would like to introduce you to the concept of no re-renders, at least on the C++ side. So, before we jump into the deep water, let's start with the simple stuff. Styling in React Native is easy. You have one API exposed to you from React Native, which is a stylesheet API. This is like a function where you can put your plain objects. Everyone knows that. But we have few issues here. So, it's only processed once, unless you move it to the component or you wrap it in the hook, but by default, it only processed once. Also, you can see that background color and color is fixed. There is no way to change it during the runtime. For example, when user will switch color scheme. Unless you write your own hook and you will re-render it every time. So that's another approach. Yeah, so we have non-dynamic values, but not only colors or sizes. There is no place for functions. You cannot call this container style from the component. It's fixed. Again, what about dark mode support? Like this is the obvious step when you want to build your native application. What about breakpoints? What if you want to target like tablets, they have bigger screens? What about variants? What about multiple teams? What if you have like 10 teams and you sell some teams in your app? What about functions, dynamic styling, and so on? I'm pretty sure that everyone from you, like, faced this issue before. Well, and here we are. That was me. Almost three years ago, one of our clients reached to us to build native application. But it was specific native application because we had to target Android, iOS, tablets, and web. That's not all. They also prepared hundreds of wireframes. They had their own team of graphic designers. So I was sad. I was looking as a CTO for the solution, but I couldn't find one. So in January 2023, I started to build in the code base of my client, Unistyle. So it's called Unistyle 0.0. But it was so tightly coupled with the codebase, I couldn't extract it and share with the community. After we finished the project and released it, I did it, and I released version 1.0. It was only about the JavaScript. There was no C++ code, nothing. It was based on the hooks, like I mentioned on the second slide. Then, two months later, I thought a little bit, like, how can I improve? So I decided to add additional functionalities, like exposing some native values, native sizes from the C++, like your screen width, dimensions, so it's always dynamic, it's always up to date. So I released 2.0 only after two months. Then I had to take a break because it was a lot of work. And last July, I started to prototype 3.0. This is the biggest update since version 0.1. And today, we are on beta 8. So we are not ready. We are not production ready, but it will change soon. Okay. So now I introduce you to the concept why I did it. So let's talk about unistyles. Again, I will bring back the default example. So this is like the regular stylesheet from React Native. And in order to convert it to unistyle stylesheet, you just need to change the import. That's all. You are now ready to pass your styles to the components. It's not that easy, because first you need to configure your theme, but it's like one more function. And then you can change your objects to function. And here you will get always up to date theme selected by your user. And right now with the theme, we can update the values so they are no longer static. So this is a drop in the replacement of the style sheet. You can use objects like in the regular style sheet, but you can convert them to functions. Also, you have dynamic teams. There is no limit of number of teams. Also, there is no like limit on the structure of your team. You can put there whatever you want. And it has some superpowers. You can use variants, compound variants, teaming, like dynamic values and so on. Also, if you accept the second argument, this is your native runtime. So you can access multiple metadata like screen dimensions, some kind of accessibility values, color scheme, orientation of your phone. So everything you need to use for your styles. Now, today I won't gonna talk about the Unistyle features because we have a lot of them. One of them is like custom CSS parser. So we put everything into CSS variables. You can use pseudo classes in your style sheets. So there are a lot of features and I will only focus on the one topic, which is Shadow Tree updates. Before we jump into the shadow tree, I need to present you or describe you how UniStyles works. So again, we have a simple stylesheet. I just converted the font size to the breakpoint. So for the smaller, extra small screens, you will get 20 and for the medium screens, you will get 16 pixels. Of course, it's configurable, so you can put as many breakpoints as you want. I also attached runtime insets. So we can also pull your up-to-date insets from the runtime and they are attached to padding top. So this is like the same example with two changes. First of all, we need to detect dependencies. And why we need to do that? Because we will pass this function to the C++. So we will lost all the information about your team. So we have a style sheet like here, and it has two dependencies. You accepted two arguments, so it depends on the theme and runtime. Now we also have two styles, we have container and text. Container depends on insets and theme and text depends on breakpoints and theme. So this is our dependency graph of your stylesheet. If we have our dependencies, we can now register our stylesheet and whatever you can see from this point happens on the C++. So, I expose hybrid stylesheet, this is like the React Native Nitro hybrid, and whatever you have like in this pink parenthesis, it will be passed directly to the C++. So, whatever you can see here will be executed on the C++ side. And you used like team and runtime. So we have like a dedicated type on the C++ site for such stylesheet. And we now can or should register the stylesheet with stylesheet register. This is like a uni-styles feature. So it's not about the React Native. This is like built on top of the React Native, sorry, React Native uni-styles core. And here is our style sheet. Let's call it style sheet one. But in the future, like you may have multiple style sheets. Each app has multiple style sheets. So we will actually recreate your structure of all your style sheets from the JavaScript on the C++ side. So from now on, I can reference your style sheets from the C++. Now we need to register unistyle. But what is unistyle? Like is the library? Well, unistyle is a special term in unistyle 3.0. So again, we have our registry where I hold all your stylesheets and we have our single stylesheet that contains two styles, container and text. Now, before I do anything, I need to parse your style sheet because you may have a lot of breakpoints, media queries, like weird syntax that is not available on the React Native style sheet. And actually, our parser on the C++ side has exactly 1000 lines, like 1000 lines. I checked it yesterday. So I move your container and text styles to the parser and it will produce uni-styles. Uni-styles are a C++ pointers to the parsed styles with the metadata. And we keep some kind of metadata. I will show you the example. So for example, we have one uni-style. which is a type of object or dynamic function. It depends on you. For example, we have a style key container or text and we have dependencies. So they are pulled from the Babel from the first step. Also, I keep like the raw value, which is in this case for container JSI object. And there is a special case because as you remember, I mentioned that you can use functions. So you can use something called dynamic functions. So let's say your text style is now a function. You can call it from your component and it may have multiple arguments. In this case, I will pass font size from the component to the style sheet. Now, before the last step, where I will explain how unistyle works, we need to also understand dependencies. So dependencies is not only about the Babel and saving them on the stylesheet on the C++ side. So this is the stylesheet registry and we have multiple stylesheet and our stylesheet is the first stylesheet. And each stylesheet may contain one or multiple unistyle C++ pointers. So in our example, we had two styles, container and text, and container depends on insets and theme and text depends on theme and breakpoints. But actually, unistyles can reference up to 15 dependencies. For example, keyboard height, navigation bar size, pixel ratio, or some kind of accessibility values like content size category. So whenever you will access these values in your style sheets, I will save it. And this metadata will be attached as numbers to the style sheet C++ representation and Unistyle's representation. So we have nine because, you know, insets are nine, zero, and three. Now, the final part. Why would I save your style sheets and bind them and, you know, create different representation of styles? Because it's required for selective updates. Like that's the best selling feature of Unistyle 3.0. So we have phone that can emit some kind of C++ event. This event may be produced by user, for example, when clicking on the button to switch the team or by the device, like changing device orientation. And whenever you send some kind of event, I will look for any stylesheet in the C++ registry that contains multiple, one or multiple unistyles. You should read it vertically. So for example, stylesheet one has two unistyles. And well, let's see what's happened. So let's say your user will rotate your phone and we will emit C++ event. And I will know that, for example, stylesheet two and four depends on team orientation. And I will only know that I should grab them and look for uni styles that depends on this change. so that I can build the new shadow tree and update it without the re-render. And again, here is another example. So, for example, one of your users switch your color scheme to dark, and then we can select another style sheets and find dependent styles. Okay, so let's talk about ShadowTree, like this is the real example. So let's check what we've done. So we have a stylesheet representation on the C++ side and each stylesheet contains multiple styles called unistyles. We also know each uni style and style sheet dependency so we can perform selective updates. So we can take dependent styles based on the event. Now, in order to finish our job, we need to do two more things. We need to know how to link styles to views because there is no information in the style sheet that, for example, one of your view uses a uni style. Also, we don't know yet how to perform shadow tree commit. So let's learn how to do it. So here is our uni style. Let's say it's our container. And this is exposed to you back from stylesheet.create as JSI object. And this JSI object contains like regular style values like flex1, background color. It's parsed. It has all the React Native compatible styles. there is one hidden thing inside this object. There is a JSI native state. So if we bring back this uni-style from the JavaScript to the C++, I can deduct the key or ID of this uni-style, I can convert it back to my C++ shared pointer uni-style. So that's why we need this JSI native state. And I guess we need to do the same with the shadow nodes. But where can we find shadow nodes? Our shadow tree, you can imagine this tree like that. I actually generated it with the chat GPT and it's not true because you need to invert it so the root is at the top, but let's pretend this is like the root of our tree. And our tree represents all the shadow nodes. That's what also Hannos showed you in the previous talk. But again, what are shadow nodes? Okay, we have a tree and so on. The truth is every React Native component or custom component if you build one is a shadow node. So we need to figure out how to get shadow node, for example, from React Native View. How to do that? So we need to go very deep. So handle is actually a ref, a reference to your component. And each component, maybe not each, but many of them have different paths. So that's why I need to perform a lot of checks if this is a scroll view or view or text. And somewhere there deep in the node, there is a hidden JSI native state from React Native. And this function is called findShadowNodeFromHandle. Now, okay, we have our ref, we have our uni styles, our parse styles, so we need to call another hybrid and pass the shadow node, like this is the returned value from findShadowNodeForHandle function, and one or multiple unistiles. Now on the C++ side, React Native exposed for us one helper, it's called shadowNodeFromValue. You can import it and it will look for the native state inside the node that we found in the ref and we can cast it or get from it shadow node. And actually I do the same with the uni-styles. So as you remember, I also attach JSI native state. So this is my custom function and I can grab multiple. So std vector is like an array on the C++ side. So I can get all the uni-styles and I can access all the pointers to my styles. How to get shadow tree? So in Fabric you can do that in 100% from the C++ side unless you want to use hooks, shadow tree hooks. So this is like one liner, you just need to have access to runtime and the return value from getShadowTreeRegistry will be our reference to ShadowTreeRegistry. Now, if we have refs, if we have unisites, if we have ShadowTree, then we can do two things. We can get ShadowTree based on the surface ID, or this is like the second method where you can enumerate all the surfaces. So this is like example how to handle multiple surfaces on your React Native app. And if you pass like this is weird syntax, but this is C++ function where you can put dependencies and so on. But you will get from the React Native two arguments. The first one is shadow tree and the second one is a reference to the Boolean, which you can later mutate and stop. Stop iterating over all the surfaces. And again, it was mentioned by Hanno in the previous presentation, but I would like to focus on the one function. It's called transaction. It's not there yet, but it's here. So transaction is another C++ function that you need to build and pass to ShadowTree commit, where you will get old ShadowTree node. And now you can mutate it. You need to mutate it because it can be accessed from multiple threads. So that's why there is no immutable API or a helper function to find node and mutate it. You need to traverse entire tree and mutate it. And unistyle exposes shadow tree manager, this is like a helper from unistyle to clone shadow tree. And it accepts three arguments. So the first one is old root shadow node. The second argument is a pair of your refs of your shadow nodes and unistyle attached to them. And we need to also pass affected nodes. So we will know which nodes are affected. Now, we don't have time today to dive deep into the ShadowTree commits, commit hooks. So we have a mount hook, we have platform tweaks like for Android, you need to mutate ShadowTree differently and how to traverse the ShadowTree. But if you are interested in such topics, you can browse unistyle repository. Now, we did everything. We took shadow nodes from revs, we recreated your stylesheets on the C++ side. We know which styles depends on which events. We can perform selective updates. And the result is... So this is like a quick demo created with React Native Scan. So it will highlight components that we rendered. And on the top, we have two buttons. The left one is change theme. So this is like user events that will be sent to C++ and Unistyle will perform selective updates. And the second one is to re-render the page. So we will see actually how it differs. So, if we will re-render, everything is highlighted. But if we change the theme with unistyles, there are no re-renders. Actually, if your user will switch the theme, I will find dependencies of every style sheet that depends on the theme, your uni styles, your parse styles. I will compute selective updates. I will know exactly which styles should be updated and I will convert it to the shadow tree updates. One more thing. I'm treat to announce that today after the talk, I'm going to release the first release candidate of Unisites 3.0. If you want to learn more about ShadowTree UniStyles, please get in touch with me and I'm ready for QA session. Thank you very much.

Mo

Some massive round of applause. Cool. So for the audience, get submitting with your questions on the QR code. Thank you for your talk. A quick few things. I'm going to start high level because I know a bit about Unistyles, and you didn't really touch on web today. I know one of the things that was on your roadmap was eventually having no JS plugins. based styling so that it's running on CSS and it's compiled to CSS ahead of time. Is that also part of 3.0?

Jacek

It's already done. Yeah. So actually it's possible to remove Reignitive Web dependency at the moment. The only part that we need Reignitive Web is to convert your views to divs or to text and so on, but we are working on that. So it's possible to generate like CSS media queries without Reignitive Web.

Mo

Cool. Very, very, very cool. That's awesome. I want to dig into this approach, and I've seen it with a few libraries where you're kind of escaping the React paradigm in some capacity to achieve the performance benefits, right? So FlashList has a very similar approach as a library fundamentally. It means that there's differences in behavior compared to what like the react programming model talks about do you see any risks in that as a library maintainer i know that the folks at shopify decided to take on those risks regardless but i'm curious as to like if you thought about that as a as the way that you're designing this obviously because the optimizations aren't in react anymore they're c plus plus layer on the shadow nodes but you know instead same with the web, you use CSS. So why don't you re-render your React web components if you have CSS? So I think React Native deserves something like that, like to skip re-rendering your business logic. You can update styles from the background.

Mo

That makes sense. Okay. That's very fair and I can see that from a design philosophy perspective. Very cool. Okay. Is Unistyle still incompatible with ExpoGo? Is there any workarounds?

Jacek

Yeah, this is my favorite question. Sorry. So this is a quick question for Expo. Why they don't like inject or pre-compile ExpoGo with Unistyle? Because I cannot do that. If they do that, it will be supported.

Mo

Well, there's folks from the Expo team here, or at least Caddy here. So maybe you guys should have a chat. But... You know, I think maybe just going at ExpoGo for a second, I feel like ExpoGo is being encouraged more and more for you to use less. So I think you should be on dev builds generally. Someone from Expo is going to yell at me for saying that. But yeah, like most people are on development builds these days. So it's not that big of a problem as it was when everyone was using ExpoGo back in the day. Is 3.0 compatible with the old architecture?

Jacek

No, and it won't be because we are too deep into the fabric. So it's possible to do that, but why? If like new architecture is like the new way of building up, we were waiting for it few years, so we should stick to it.

Mo

  • We should use it as much as we can.

Jacek

  • Yes.

Mo

  • And you'll see in the panel later on that that's gonna be a topic of conversation is, how long should you support the old architecture for? Is it possible to animate the value of uni style? Switch the theme without a blink, but ease in and out.

Jacek

No, you have for that reanimated. Reanimated is compatible with the uni styles. So what would I like build another animation library if you had great one like the number one there. So you can use reanimated and yeah, that's all.

Mo

Cool. One person is just congratulating you and saying it's a very impressive demo, so nice work to you and your team. We'll do one more question, just because we want to stick to time as much as possible. What was the hardest obstacle that you and your team found while working on version 3.0 of Unistyle?

Jacek

I think it was understanding how it works. So the presentation like from Hanno, like it's the best value from the community because we can understand how it works underneath. Because there is no documentation. You can actually read header files from the C++ or you need to read React Native code. I think it's possible to understand it. But, you know, it would be great to share the knowledge. So that's why I did it today. And yeah, so that was the toughest part of the new styles.

Mo

Well, some of the new ChatGPT models like O3 are quite good at understanding code when there's no documentation for it. So hopefully that process becomes smoother and smoother for everyone. Jacek, thank you so much for taking your time and doing this talk. Appreciate it.

Edit on GitHub