Check out a free preview of the full Reactivity with SolidJS course:
The "Creating a Signal from Scratch" 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 creates a reactive signal from scratch. A global context array is used to store a list of observers that are subscribed to the signal. When the signal is updated, each observer's execute function is called.

Get Unlimited Access Now

Transcript from the "Creating a Signal from Scratch" Lesson

[00:00:00]
>> That is kind of an overview of what it looks like to play with a reactive system, but I think it'd be a lot more fun if we just built one. This is one of my favorite things about it, because with magic, people kind of call it magic, it isn't necessarily that magical.

[00:00:19] You can build a reactive system in less than 50 lines of code. I think it's actually easier to implement a whole reactive system than implement a promise. Like people ask that on interview questions, how to implement a promise. We can kind of just go in and build something very simple ourselves to kind of appreciate how it works and kind of demystify the magic, so to speak, so that's what we're gonna go to next.

[00:00:44] We're gonna build a basic reactive library here. Now, if you want you can just follow along with me or you can code it yourself as we go. But I've just opened up Solid's playground at playground.solidjs.com and cleared it, but you can do this in code sandbox or any kind of code location.

[00:01:01] We're just gonna build it quickly, we have two files that I'm gonna use for this. I'm gonna have a main file which is essentially where we're going to test runner basically our reactive library and then have reactive, which is where we're gonna do our implementation. So, I guess the first thing we kind of need to do is we need to create a signal, and I'm going to where is it from?

[00:01:29] Dot slash reactive. And we haven't actually created this yet, but I'm just kind of preemptively creating the interface that we want because we're going to let's say, [count, setCount] = createSignal. Let's set it to zero here, we're just kind of recreating that example I was kind of playing with earlier.

[00:01:50] Let's see if we can get it to at first just kind of console log the count, and we want this to be, I guess, zero. And then if we set count to, I don't know, counting by five for some reason, and we console log it again, then we should see five, right?

[00:02:13] That's kind of our goal here, so I'm gonna go into reactive here. And we're gonna kind of flesh out how we might create a signal, right? So we can go function createSignal and it's going to take some kind of value, okay? And we're actually gonna export this function so that it does, and we want it to, Return an array I think, sorry.

[00:02:53] Count is not a function, okay. So I'm making some progress here, and our array, we need the ability to read and to write essentially. So, We're gonna implement those functions. Actually I'm gonna do this right, don't mind my style too much, I'm being very kind of functional programming approach here.

[00:03:21] But, To start with, we're just going to say when we read it, we just read our value and when we write it, We get a new value in. What should I call this? New value. And then value equals new value. Actually I'm gonna turn type script off for a moment here.

[00:03:53] This isn't a lesson on how to do type script generics at this point. But essentially, this is probably where we might start for looking at how to create a signal cuz I'm actually leveraging a little trick in JavaScript called closures here. The fact that this is the same variable that gets returned in the function and the fact that this gets assigned to that exact same variable is why this actually works.

[00:04:17] But essentially, we have a read function that reads a value [LAUGH], we have a write function that updates the value, we return the array and everything seems to be working now. And if I look at my console down here, I have a zero and a five and just a sanity check, let's make that a ten, and it's zero and ten.

[00:04:36] So, this is where we'd probably start. But as you can imagine, this doesn't really do anything. What we really wanna do is maybe make our effect and I'm just gonna kind of stub this out again. We want an effect to do our console log now, so we want createEffect and it gets a function.

[00:05:02] I'm just gonna use an arrow function here and it's going to console log whatever the count is. And in this case, the first time it runs, we want the log 0. And then after we set at ten, or five or whatever a number is, we're hoping that

[00:05:25] Of course, createEffect doesn't exist. So, this is where we gotta do a little bit more work. So, let's go back into here. createEffect. And our effect takes a function, okay. So to do this, we actually need to do a couple things. First thing we need to do is actually upgrade our signal.

[00:05:58] Inside our signal, we're going to add a subscription. So, this I'm just gonna call it subscriptions and I'm just gonna use a set, it's probably the easiest way to do it, we just create a new set that's gonna hold our subscriptions. Under the hood, it's like an event emitter or a pub sub kind of mechanism where there's subscribers.

[00:06:23] And now, another thing that we're gonna need to do here is I'm gonna set up something, I'm gonna call it context, but it's gonna be a global. It's gonna be an array. And this is gonna be important in a minute because this is how we actually can keep track of what's currently running.

[00:06:45] As I mentioned before, this is using the runtime, and this isn't a compiler anything, it's a runtime mechanism, there is a bit of a runtime stack. The trick here is when we read our value, I'm gonna update this a little bit, still gonna return our value, right? But when we read our value, we're gonna use that context to figure out if there is something observing it, let's call it observer, okay?

[00:07:15] And we're gonna look at the context. I'm gonna go and we're gonna take context.length minus one, we're gonna look at the top of this. You can think of context as a stack, you push things onto it, pop things off the top. We're gonna look at this context and see if there's something there.

[00:07:31] And if there is an observer, then we need to go subscriptions, add that observer. Okay, and then let's see here. On the write side, we're gonna do a little bit extra work too here. We're gonna update the value first like we did before, but then what we're gonna do is we're going to kind of iterate over our observers, so observer const.

[00:08:20] Let's see here, Of our subscriptions. There. And, We haven't really talked about what the observer looks like, we'll get there in a minute, but we're gonna notify them. I'm gonna just call it execute for this sake, but we're gonna go through and essentially the process is everyone that's subscribed to the signal when it's updated tell it to rerun its function, so that's what we're doing here.

[00:09:00] And that's actually most of it for the signal side. On the effects side, we kind of have to build out this kind of new structure. So I'm gonna make a variable here, I'm gonna call it an effect, and it's gonna be an object, and it's gonna implement that execute function that we just, Created.

[00:09:36] And actually we're going to also run it immediately or sorry, effect.execute. So the first time we run our effect, it's gonna run that function immediately. And how I'm doing 29 lines of code, okay, let's see how we're doing here. Okay, so what does an execute function look like?

[00:10:02] We're gonna iterate on this probably a couple times, but the basics of it is we're going to push ourselves onto the stack. So we're gonna push our effect onto the stack essentially, and then, We are going to run the function, and then we're going to pop ourselves off of the stack.

[00:10:30] And if you've been watching the console you might notice this is actually already working at this point. So, let's look a bit of what's going on here, but essentially this is the most basic version, I think we have like 30 lines of code here. But if we took our example here, What's happening is we create our signal and it returns our two functions, right?

[00:10:59] And then the very first thing that happens is we call this effect function essentially, we create our effect. And the effect pushes itself onto the stack at that point as we saw here, it pushes itself onto the stack. And then we get to our console log and it goes to run and it goes, actually I have to call this count function first.

[00:11:24] And it calls the count function from here which is our read function and it looks at that stack and sees that effect. And because it exists, it adds the effect to its subscription list and returns to value, and then the console log finishes. Then later, we set the new value to ten.

[00:11:49] So when we write it to ten, we update it, and then we go over that list of subscriptions, which is just one, and then we recall the effect.
>> Does that mean that this signal registers itself on first read? What if we ran write before read? To word it better, whatever calls read adds itself to the subscriptions on the first call?

[00:12:11]
>> Yeah, I see the question. The question is, what if the effect doesn't exist or nothing's listening at the beginning? If I understand the question correctly, in this case, and then we can test it to find out. If we set it to two intermediately, what ends up happening through this flow is that all we did is we created the signal, go to read and then we write it immediately.

[00:12:39] And when we write it immediately, it's gonna set the new value but there's no observer. So it doesn't do this for loop and it just continues, so it's almost like where we started. And then when the effect runs the very first time, it just happens to read the latest value which is the already updated value of two.

[00:13:02] As you can see, because we're using functions to do reads here with these long living effects, there's no stale closures or any of that kind of thing because you always just get the latest value when you call the function.