Reactivity with SolidJS

Fine-Grained Reactivity Demo

Ryan Carniato

Ryan Carniato

SolidJS Creator
Reactivity with SolidJS

Check out a free preview of the full Reactivity with SolidJS course

The "Fine-Grained Reactivity Demo" Lesson is part of the full, Reactivity with SolidJS course featured in this preview video. Here's what you'd learn in this lesson:

Ryan demonstrates the flexibility of the reactive code in the application. Local state can easily be moved to a global state and shared across components.


Transcript from the "Fine-Grained Reactivity Demo" Lesson

>> We can kinda take this a bit further, right? Like, this isn't a real app, it's a counter. So let's make an app component, let's say, and our app component is going to return a fragment again. And it's going to have some counters in it. And this is kind of an introduction to components here, because we're using Capital case syntax to run a component.

So this is why we named them Pascal case here, because any thing in JSX that is pascal case does a tag, will be a component rather than a native element. So for the lowercase ones, you can only use what's available in the environment like button or h1, these are HTML elements.

But by using the Pascal case you can run any arbitrary function and have it act as our component here. So, what we're gonna do is, instead of rendering counter, we are gonna render app. And app has two counters that are counting up, but I'm gonna get rid of the setInterval, cuz this is just noise.

And as you can see, we have counters, two console logs, and hopefully if you're following me to this point, if you can click each one, they each have their own state that they manage. And when you update them, they are literally just changing these two effects, nothing else reruns.

There's only the original two counter console logs. And this is pretty powerful because it's very simple. You saw how we got here, because we literally just without a component system had our own update model. But it has a lot of implications, because if you think that this is just a function, nothing fancy, and this has nothing to do with that function, you can do stuff like this, like pull it out And if you know JavaScript well, if you have a variable that's referenced by functioning and multiple instances, they just share that state.

So now we have global state management, it's the same thing. All that happens is when we update the count, it updates these two text nodes, nothing else, no rerunning, nothing else. And global state management is pretty sweet. We often actually have a kind of more scoped. So what we can do, is we can bring our signal now, and let's put it in app.

That's fine, counters complaining that count is not defined. And that's because it's going to get something called props, and props is the object that gets passed in with all the properties that you pass it on your component. And for our sake, what are we gonna do? You can pass props a few different ways, but I'm going to, sorry, counter.

We're going to pass the count actually, as the children props, but I'm gonna go like this, and pass it between like this. And if we do that, we also need a way to handle updating that count. So, what I'm going to do here, is we can call this really whatever we want, our convention is generally to put an "on" in front of it.

This suggests like it's an event, but, I mean, it can really be anything let's say, Yeah, I'm just gonna use onClick, cuz I'm kinda being lazy about this, but We can just do onClick and what I'm going to do here, is I'm going to take the setCount that we have from above here, and I'm going to stick one in each.

And to differentiate it a bit, I'm gonna say the top counter increments by 1, and the bottom counter increments by 2. And actually, to differentiate these, I'm gonna make the second counter actually be 2 times the count as well. So one updates by 2 and 2 times the count the other one's kind of like the original counter.

And what we need to do here, is now, because we don't have count anymore, we need to reference it, and the way we do that, if you do props.children, for ones that are passed in between, and then for named things, we can just reference them directly, so props.onClick.

And for emphasis, unmute this count, I'm going to go console log app. But at this point, I'm thinking it might be unsurprising what happens here. So we have app, and we have our two counters. Let's try clicking the first button. Well, they're sharing state still, because the state is being passed in from the app component that's above.

But what you notice is the first counter shows 1, and the second one shows 2 because it's 2 times the count like we set up here. Actually, let's format this so we can see it a bit better. Yeah. But nothing console logs, app doesn't console log, neither counter console logs.

In fact, When we click the second button, and it adds by 2, we see the same thing as well. Basically, nothing needs to rerun, except for those same two effects in the same two text nodes that we've had right from the beginning. So this is important to kind of show, because it's important to understand that no matter how many components you have, the performance scales, it doesn't actually matter.

It doesn't scale on code size, or number components, it scales on interactivity. And this is very powerful for performance, but you might still be looking at this and go, okay, I'm missing something. How is it possible that I can have expressions like count times 2, and stuff from the Parent Talk to things in the child without things re rendering.

And there is a little bit of a trick with that, but it's not too different than what we've been seeing so far actually. And that is most of the work, the component itself is just a function. It's technically a little bit more than a function actually. It's a function that's called with untrack, because, we wanna make sure that if people access reactivity top level in the component it's not gonna cause some parent retriggering.

So that this is a use case for untrack. But for the most part, the actual power in the compilation is how we manage props. We actually look at the props the same way we looked at the other JSX and decide how to handle it. So in the case of an example like this, if we see that there's a function call or a property access, we wrap it in a getter, which we saw earlier on when I was comparing to Vue.

So this is just lazy. It doesn't run this function until you call for example, or props.children. And then if it's a static value, like a string or a number or something, It just inlines it right away, it's just part of the object. Like in this case this change handler it's not a function call, but a function declaration.

We can pass it straight through. And understanding this, it means that inside our components, we can treat everything as props.blank, whether it's a signal or a wrapped expression, or whatever static value. The interface is the same from the inside and the outside, as long as you treat it like you're passing a value, you call those functions.

The compiler can see that and create the correct getters, and in so, what we've done is effectively flattened the whole tree. The whole component tree doesn't matter anymore, because the only thing that happens is, when we get to that text node, way down, where we're updating that text, it calls prop.children, which is this getter, which calls the count internally.

So we just call all the functions inside the effects and nothing else needs to rerun. So that's basically the power of this approach. We kinda forget about the components, and just focus on updating only what changes. And I'm gonna take this a little bit further, hopefully everyone can see this.

The idea here is, what if all rendering was an effect? And some frameworks they like to tell you that all rendering is pure, that it's like something that doesn't have side effects. And the challenge there is when you do have side effects, you suddenly have to kinda break through that model, you're kind of like, there's different rules here, you have to follow.

But with Solid's approach, you can kind of view the whole tree as a bunch of nested effects. So, if you have an effect, and under that affect, you create signal a, but then you only read it under the second effect, and that second effect also creates its own signals, that only are read under effect three, four and five.

You're actually isolating it, cuz as I showed earlier when we create our own system, the effects execution is just a stack that we push on to and pop off. So it's only the nearest effect that matters. So if you update b, for example, or d, let's say, then it only needs to rerun that little bit.

If you update a, well, yeah, you have to do a little bit more work. But you have to understand that in real app, code like this might look something like this. So our d or b might be in effect, that say update the document title, and what's your updates?

Some text in a paragraph element, and a in our example, is inside a conditional, like a ternary. So you're not gonna update a very often unless you're actually changing a condition, like deciding whether to show, that whole subsection. So, what ends up happening, ultimately, is our branching points, the places that change are based on the logic of your code, not based on components.

So it doesn't matter how many components we put in the middle, between these points, it scales on, like, decisions, using conditions.

Learn Straight from the Experts Who Shape the Modern Web

  • In-depth Courses
  • Industry Leading Experts
  • Learning Paths
  • Live Interactive Workshops
Get Unlimited Access Now