SolidJS: Reactivity Unchained

Rate this content
Bookmark

Join Ryan Carniato, creator of SolidJS, as he shows off the fundamentals of this unique take on a JavaScript Framework. This session will reflect on the challenges of modern web development that motivated Solid's creation. By retracing his steps from simple beginnings to a full-fledged framework, Ryan will showcase how reactive primitives are the only building blocks you need.

20 min
20 Jun, 2022

Comments

Sign in or register to post your comment.

AI Generated Video Summary

Solid.js is a declarative JavaScript library for building user interfaces that addresses performance optimization. It introduces fine-grained reactivity and avoids using a virtual DOM. The Talk explores rethinking performance and reactivity in web applications, understanding reactivity and primitives, and creating DOM elements and using JSX in Solid.js. It also covers rendering components, sharing state, and the advantages of fine-grained rendering and the reactive approach in Solid.js.

1. Introduction to Solid.js

Short description:

Solid.js is a declarative JavaScript library for building user interfaces. It is similar to React in some ways, but it has a different implementation. Solid is built entirely on fine grain reactivity and does not use a virtual DOM. The update model and life cycles in JavaScript frameworks have runtime implications. Solid addresses the question of when and what to memoize for performance optimization.

Hi, I'm Ryan Carneato, author of Solid.js, and today I'm going to present an introduction to it. What is Solid? It's a declarative JavaScript library for building user interfaces like so many that have come before it.

Solid on the surface at least is a very React-like framework. It values the same things. Unidirectional flow, read write segregation, and immutable interfaces. It's built on the same building blocks. Hook-like primitives, function components, and JSX. And it has a lot of similar features. things like portals and fragments, suspension transitions, streaming SSR, and some others too. That aren't in React, like custom directives and data fetching.

But there is a but. A rather large one. Solid is sort of nothing like React. Solid is built entirely on fine grain reactivity, similar to MobX and Vue. Its components only render once. And there's no virtual DOM. The whole mental model is completely different. Same values, entirely different implementation. But what does that actually mean? Well, modern front-end web development for years has been about components. Class components, function components, option components, web components. And for good reason. Components are essential building blocks that allow our programs to be modular and composable. However, in almost every JavaScript framework, they have runtime implications. The update model and the life cycles are tied to them. And this has led to basically two views of the world. Either you use a top-down diff like a virtual DOM or a tag template literal. Or alternatively, you rely heavily on compilation to separate creation from update. But both of these still have components that basically run top-down. And this kind of begs the question, when and what to memoize? This is important for performance, because if you're going to call something over and over again, you don't want to repeat the work. There's great talk from Sean at React Comp 2021 that addresses this exact thing.

2. Building a To-Do App in React

Short description:

Your first inkling might be to build your to-do app in a framework like React. But as your program grows, you apply optimizations like memo and use callback. Let's compile it. This is compiled output, similar to what a framework like Svelte would do.

Your first inkling might be to build your to-do app in a framework like React. Kind of like this. Declare some state, and wire it up. But on any change, even unrelated to the to-do list, you'd be re-rendering the whole list. But maybe that's okay. That's why there's a Virtual DOM, to make sure this isn't as expensive. But maybe you still want to optimize. As your program grows, you apply your optimizations, and things may start looking like this. Maybe you add a filter, and some theming.

And there's nothing innately wrong with this. But now we are annotating things with memo, and use memo, and use callback. And adding our dependency arrays, and ensuring that everything runs exactly how we want it to. But it's a bit of a departure from where we started.

So... Let's compile it. And this might be what you'd end up with. To be fair, this is not the code you'd write, this is compiled output, kind of pseudo-code, from an experimental compiler React team is working on. But it isn't actually all that different from what a framework like Svelte would be doing. It's a bunch of shallow referential equality checks, that at every decision point, rerun when your component is marked as needing an update. Common ground is a component update state, then runs an update function, and checks against these memorized values, as shown here in this memcache.

3. Rethinking Performance and Reactivity

Short description:

What if we only ran what was needed to be run, and didn't rely heavily on compilation? What if the boundaries of our components didn't dictate the performance of our web applications? To do that, we need to kind of go back to the beginning.

But what if we didn't? What if we only ran what was needed to be run, and didn't rely heavily on compilation? What if the boundaries of our components didn't dictate the performance of our web applications? To do that, we need to kind of go back to the beginning. And I'm going to go all the way back to the beginning. Remember, the first program you wrote. It was a Hello, World. You were able to set some data in a variable and log it on the console. And it didn't take too long before you realized, well, you could set a new value to that variable and log it again. You know, this is the beginning of programming.

4. Understanding Reactivity and Primitives

Short description:

Then you realized, well, that's a lot of repetition, so let's extract that out into a function. Reactivity is based on primitives. The first primitive is a signal, which is a wrapper over a value. We replace the assignment with set name and call it as a function. Another primitive is create effect, which allows us to interact with our world whenever a signal changes. This is achieved by using the runtime stack. Fine-grained reactivity is the key to understanding how this works. A fine-grained reactive library can be created in about 50 lines of code.

Then you realized, well, that's a lot of repetition, so let's extract that out into a function. Maybe a greet function here, and we can have a seter name and then call greet, and then seter name and call greet again. And this is all great, but at a certain point, maybe you're like, every time I set the name, I want it to greet without having to call it. And that's where reactivity comes in.

Reactivity is based on primitives. It's kind of like a promise in JavaScript. In our case, we're going to introduce the first primitive here, and it's called a signal. Signals are relatively simple. They are just a wrapper over a value. They have a get function name here and a seter, set name. And in this case, we have to replace this assignment we have here now with set name so we can update it. And we need to replace our name. We need to call it now, because it's a function. And once we've done that, it basically acts the same. We can still see Hello World and Hello Solid JS.

That in itself, to be fair, is not terribly interesting. So we have another primitive we're going to choose here called create effect. And what effects are is they allow us to interact with our world whenever a signal changes. In this case, we're going to take our console log and we are going to put in the effect and have it listen to name. This lets us clean up the rest of this code, essentially. So now we just create our signal for the name and then create the effect that runs once. And then when we set the name again, it runs again. And again, it does this because we call names a function that lets us intercept reading that value. And this can actually extend outside of the effect itself because this is not a compiler trick. This is completely runtime. So we can make a like uppercase and now we can basically call our name in here and change it to uppercase, stick it in our effect. And because it's just using the runtime stack, we can see hello world and hello solid js now in capital. So you might be wondering how this works. And it all comes down to this idea of fine-grained reactivity. Creating a fine-grained reactive library can be done in about 50 lines of code.

5. Understanding Signals and Effects

Short description:

At its core, a signal is a getter and setter function pair that close over a value. We update our signals to check for current observers and notify them when something changes. The implementation for get current observer uses a stack to track the currently running context. Effects are executed immediately, pushing themselves onto the stack, adding themselves to subscriptions, and then popping off the stack. With this foundation, we can build other primitives like createMemo, createStore, and createResource.

I'm going to simplify it even further here for demonstration purposes, but it all starts with signals like we saw. And at its core, you just view a signal as a getter and setter function pair that close over a value. Of course, there's a little bit more to it than that. And for that, we're going to need subscription.

So let's update our signals, do a bit more. Now on read what we want to do is check if there's a current observer. And if there is, we're going to add it to a new subscriber set that we create when we create our signal. On right, we update our value still, but now we actually iterate over those subscribers and call them to basically notify them that something has changed. And that something that needs to be notified are our effects, which are the other side of our equation.

Here you can see the implementation for get current observer. What we have is a stack. This context is just an array, global context. And we just grab whatever's atop the array to see what's currently running. For our effect itself, when it's created, it's executed immediately. And then it goes through the cycle of clean up dependencies or subscriptions, push itself onto that stack, so that when we execute the provided function, it's there and can be added to the subscriptions. And finally, it pops itself off the stack. I'm gonna put the code side by side, so you can kind of see this better as we go over our example. Essentially, we create our signal, like our name signal, and it returns our read and write functions. Then we create our effect. It executes, pushing itself onto that stack. Then it runs the function and it reads from our name signal, at which point it sees the current observer, which is that effect, and adds it to its subscribers. Then it logs it to the console and the effect finishes running, popping itself off the stack. Sometime later, our signal is updated, which sets the new value and then executes our list of subscribers. In this case, it's that effect, which executes it again, cleaning up the dependencies and we just start the whole cycle all over again. And that's really it. From there, we can build a foundation for other primitives. A lot of them are not essential. They can be used as needed, but an example of a few important ones that ship with Solid are createMemo, which can be used to cache expensive computations, createStore, which is a proxy which enables deep-nested reactivity, and createResource, which is our first-party primitive for data fetching and suspense. But enough on reactivity for now. Let's get back to our example.

6. Creating DOM Elements and Using JSX in Solid.js

Short description:

When we teach reactivity, we create a DOM element, set text on it, and attach it to the DOM. We put the text setting in an effect, so it updates the DOM when the name changes. We create a button with a click handler using vanilla DOM APIs. Solid allows writing JSX, which is just DOM elements. We can abstract the code into a function and return the elements as an array. We can inline the JSX, remove unnecessary code, and format it with prettier. We can replace the array with a fragment, making it cleaner.

When we teach reactivity, we always use console logs, but let's do something a little more substantial here. Let's actually create a dom element this time and see what it does. So what we're going to do here is we're going to create a header and we're going to set some text on it and then we're going to attach it to the dom.

Okay and then we're actually going to take setting that text on it and put it in the effect. So now whenever the name changes, it will update the dom. In this case, we set the solidjs right away. So you don't even see it. We just see hello solidjs because it updated right away. So deal with that.

Let's create a button and we'll put the setter inside a click handler. So we're just using vanilla dom APIs here so that we can make it work. And there we go. Now our Hello World has a greet button and when we click greet, it changes it to hello solidjs. But I mean, this is a lot of code. Who wants to write all that vanilla js? Wouldn't it be great if we could just write something like jsx? And in solid, you can because jsx and solid is just DOM elements. So this is a real HTML button element we're creating here and we're just going to put a click handler on it, clean up that code, and our example still works. And we can give the header the same treatment. The one difference here though is that the header has a reactive statement and Sol's compiler can actually see that we're calling a function in the expression so it knows to wrap it automatically in effect. And when we do that, sure enough, it still works. And it's a little bit cleaner here but we don't really write our apps this way. So let's abstract this into a function or a component so to speak. But this is just a function. We can clean this up a little bit and maybe return the elements as an array. And then we can just call our function and just attach it. And as you can see, it clearly still works. But let's inline the JSX a little bit more, clean up a bit more, remove all of that, and then format it with prettier. And here we go. Okay. We can actually replace the array with a fragment because that's all fragments are in Solid. And this is starting to look pretty good.

7. Rendering Components and Sharing State

Short description:

Solid's components only run once. Each component can wrap over its own state. We can share state by pulling signals out of the component. Composition in Solid allows for sharing state and passing props. Only the header text is updated.

Last thing we really have to deal with now is just how we're attaching to the DOMs. So Solid provides a render function and we can just render our greet component and there we are. This looks like any modern framework.

But there is one big difference here. Solid's components only run once because as you saw, we were just calling this function. So if I put a console log in here, like greet, let's say greet. And you'll see a log when I press it, it's not logging again. It only logs once. Okay. So let's do that one more time. Okay. Here we are. Hello world. See greet, press it. Only once. And yeah.

Let's extend this a bit further. Let's actually use two greet components now. We're going to put this inside an app component that we use. And by doing this, you can see that each component can wrap over its own state. So now we have two of them, we can see that it logs the console greet twice now, but when we update each one, it doesn't log again and they update independently, each maintaining their own state because they have their own signal. But we can actually take this signal and pull it right out of the component above. And now it's shared because both instances of the component are reading from the same variable, like it works in JavaScript. And sure enough, it doesn't matter which button you press, they update the same state and the console logs only log on creation on an update. But most of the time you're gonna put this in props. So let's put this into app and pass it down as props. So we can also move the click handler up and pass it through as well. And this is kind of how composition works in solid. The important thing to understand here though, is only where that header text is, that is what actually gets updated. We are sharing the state past here, but the components all only run once.

8. Solid Rendering and Component Props

Short description:

In Solid, the handling of component props and JSX is done lazily, allowing for efficient evaluation. Only the necessary effects are executed, minimizing re-execution. Solid provides helper functions in the form of components, which are composable and can be used for special handling of rendering, such as pagination and virtualized lists.

So this app console log will show that even when we update it, it's not gonna log it again. And actually to make that more clear, I'm gonna take this set name and I'm going to actually have it increment differently for each component, even though it's using the shared state. Even though we're kind of passing everything through, when we actually do an action, there's only two effects on this whole page and those are for both headers to update that text.

Okay. So what's going on here? Well, it's because of the way we handle component props. In solid, the only thing we actually transform is the JSX. And for components, when we see an expression that contains JSX or something that could be reactive, like a function call, we wrap it in a getter, like this get name. And what this does is allow it to be evaluated lazily. It's very similar to what we do with DOM elements, where if we see something that could be reactive, we wrap it in an effect. Basically, everything is lazily evaluated all the way to the final effect, which actually does the work. And as I said, there's only two effects in that demo. The only work being done is updating that single text node inside each header. Nothing else re-executes.

The last thing you probably need to know about solid rendering is that we're dealing with real DOM nodes. And creation can be wasteful. So we needed to do some special handling for things like lists. A simple array.map would never run over every item and map new results. And we don't want to create all new DOM nodes every time we add something to a list or sort it. So we need to do something a little special here. For solid, we have helper functions. But we chose to ship them in the form of components as they fit well with our patterns and they are very composable like the rest of the primitives in our system. What I mean by composable? Well, pretend now we need to update this. We only want to show 10 items on each page. We need to paginate. Luckily, someone has created this component for that. So what do we do? Well, maybe we import it and now we just change our for component. And as you can see, it's exactly the same pattern. Lists become paginated or virtualized lists. Conditionals become layouts or suspense or error boundaries. It's all the same thing.

9. Fine-grained Rendering and Reactive Advantage

Short description:

Solid allows for fine-grained rendering and updates without the need for additional optimizations like memos or dependency arrays. Components run once, templates compile to real DOM nodes, and state is independent of components. SOLID offers a low abstraction over the DOM, providing the freedom to interact directly when needed. The performance is consistently good, and it offers a reactive advantage. Give SOLID a try to experience this freedom in organizing your code.

You know, if you don't like solid's for component, it's just a component. It's runtime, you can write your own. This is really powerful stuff. And armed with this, we can kind of return all the way back to the beginning with our to do example.

So I've recreated that example in solid that we saw at the beginning, and we have a to do list and we have a handler that can, we have set up our state of our to do's and we have a handler that will update our to do's to done. And what we've done here is we're actually passing in a visibility filter and a color picker. And this gets passed all the way through to our add to do's to kind of set the theming. So this adds a little bit more complexity or you'd think, but in solids case, without the need for adding any memos or use callback or basically, or dependency rays or basically anything additional, we can just take our list, filter it based on that prop and only have the things that need to update, update. For example, when we click this three, now we're gonna update it to to-do or when we unclick it, we update it. When we change the filter, it gets called. But as you can see, updating the to-do did not cause the filter to get called. And if we go back, well, we're gonna have to recreate those three elements. So yes, we called the filter again and we updated three times, but this prop getting passed through here for the theme color, us changing this, doesn't cause the to-do updates or the filter to update. It just works. And we did this all without any kind of memo. Actually, the hardest part about making this example was trying to show what updated because so little does. This is what happens when you have fine-grained rendering and updates.

For me, I call this the reactive advantage. Basically, components run once. There's no hook rules or stale closures or dependency arrays for that matter. Templates compile to real DOM notes. This is super low abstraction over the DOM. It means that you have that escape hatch. You need to do something with the DOM, it's right there. But most importantly state is independent of components. Component boundaries are for your sake how you want to organize your code. It's not about performance. The performance is good regardless. So if you want to experience the freedom yourself, maybe give SOLID a try.

Check out more articles and videos

We constantly think of articles and videos that might spark Git people interest / skill us up or help building a stellar career

React Summit Remote Edition 2021React Summit Remote Edition 2021
33 min
Building Better Websites with Remix
Remix is a new web framework from the creators of React Router that helps you build better, faster websites through a solid understanding of web fundamentals. Remix takes care of the heavy lifting like server rendering, code splitting, prefetching, and navigation and leaves you with the fun part: building something awesome!
React Summit 2023React Summit 2023
32 min
Speeding Up Your React App With Less JavaScript
Too much JavaScript is getting you down? New frameworks promising no JavaScript look interesting, but you have an existing React application to maintain. What if Qwik React is your answer for faster applications startup and better user experience? Qwik React allows you to easily turn your React application into a collection of islands, which can be SSRed and delayed hydrated, and in some instances, hydration skipped altogether. And all of this in an incremental way without a rewrite.
JSNation 2022JSNation 2022
28 min
Full Stack Documentation
Interactive web-based tutorials have become a staple of front end frameworks, and it's easy to see why — developers love being able to try out new tools without the hassle of installing packages or cloning repos.But in the age of full stack meta-frameworks like Next, Remix and SvelteKit, these tutorials only go so far. In this talk, we'll look at how we on the Svelte team are using cutting edge web technology to rethink how we teach each other the tools of our trade.
GraphQL Galaxy 2021GraphQL Galaxy 2021
32 min
From GraphQL Zero to GraphQL Hero with RedwoodJS
We all love GraphQL, but it can be daunting to get a server up and running and keep your code organized, maintainable, and testable over the long term. No more! Come watch as I go from an empty directory to a fully fledged GraphQL API in minutes flat. Plus, see how easy it is to use and create directives to clean up your code even more. You're gonna love GraphQL even more once you make things Redwood Easy!
JSNation 2023JSNation 2023
28 min
SolidJS: Why All the Suspense?
Solid caught the eye of the frontend community by re-popularizing reactive programming with its compelling use of Signals to render without re-renders. We've seen them adopted in the past year in everything from Preact to Angular. Signals offer a powerful set of primitives that ensure that your UI is in sync with your state independent of components. A universal language for the frontend user interface.
But what about Async? How do we manage to orchestrate data loading and mutation, server rendering, and streaming? Ryan Carniato, creator of SolidJS, takes a look at a different primitive. One that is often misunderstood but is as powerful in its use. Join him as he shows what all the Suspense is about.

Workshops on related topic

JSNation 2023JSNation 2023
170 min
Building WebApps That Light Up the Internet with QwikCity
Featured WorkshopFree
Building instant-on web applications at scale have been elusive. Real-world sites need tracking, analytics, and complex user interfaces and interactions. We always start with the best intentions but end up with a less-than-ideal site.
QwikCity is a new meta-framework that allows you to build large-scale applications with constant startup-up performance. We will look at how to build a QwikCity application and what makes it unique. The workshop will show you how to set up a QwikCitp project. How routing works with layout. The demo application will fetch data and present it to the user in an editable form. And finally, how one can use authentication. All of the basic parts for any large-scale applications.
Along the way, we will also look at what makes Qwik unique, and how resumability enables constant startup performance no matter the application complexity.
React Summit 2023React Summit 2023
106 min
Back to the Roots With Remix
Featured Workshop
The modern web would be different without rich client-side applications supported by powerful frameworks: React, Angular, Vue, Lit, and many others. These frameworks rely on client-side JavaScript, which is their core. However, there are other approaches to rendering. One of them (quite old, by the way) is server-side rendering entirely without JavaScript. Let's find out if this is a good idea and how Remix can help us with it?
Prerequisites- Good understanding of JavaScript or TypeScript- It would help to have experience with React, Redux, Node.js and writing FrontEnd and BackEnd applications- Preinstall Node.js, npm- We prefer to use VSCode, but also cloud IDEs such as codesandbox (other IDEs are also ok)
Node Congress 2021Node Congress 2021
128 min
Learn Fastify One Plugin at a Time
Workshop
Fastify is an HTTP framework for Node.js that focuses on providing a good developer experience without compromising on performance metrics. What makes Fastify special are not its technical details, but its community which is wide open for contributions of any kind. Part of the secret sauce is Fastify plugin architecture that enabled developers to write more than a hundred plugins.This hands-on workshop is structured around a series of exercises that covers from basics "hello world", to how to structure a project, perform database access and authentication.

https://github.com/nearform/the-fastify-workshop
JSNation 2023JSNation 2023
66 min
Build a Universal Reactive Data Library with Starbeam
WorkshopFree
This session will focus on Starbeam's universal building blocks. We'll use Starbeam to build a data library that works in multiple frameworks.We'll write a library that caches and updates data, and supports relationships, sorting and filtering.Rather than fetching data directly, it will work with asynchronously fetched data, including data fetched after initial render. Data fetched and updated through web sockets will also work well.All of these features will be reactive, of course.Imagine you filter your data by its title, and then you update the title of a record to match the filter: any output relying on the filtered data will update to reflect the updated filter.In 90 minutes, you'll build an awesome reactive data library and learn a powerful new tool for building reactive systems. The best part: the library works in any framework, even though you don't think about (or depend on) any framework when you built it.
Table of contents- Storing a Fetched Record in a Cell- Storing multiple records in a reactive Map- Reactive iteration is normal iteration- Reactive filtering is normal filtering- Fetching more records and updating the Map- Reactive sorting is normal sorting (is this getting a bit repetitive?)- Modelling cache invalidation as data- Bonus: reactive relationships
React Advanced Conference 2022React Advanced Conference 2022
81 min
Build a Product Page with Shopify’s Hydrogen Framework
WorkshopFree
Get hands on with Hydrogen, a React-based framework for building headless storefronts. Hydrogen is built for Shopify commerce with all the features you need for a production-ready storefront. It provides a quick start, build-fast environment so you can focus on the fun stuff - building unique commerce experiences. In this workshop we’ll scaffold a new storefront and rapidly build a product page. We’ll cover how to get started, file-based routing, fetching data from the Storefront API, Hydrogen’s built-in components and how to apply styling with Tailwind.You will know:- Get started with the hello-world template on StackBlitz- File-based routing to create a /products/example route- Dynamic routing /products/:handle- Hit the Storefront API with GraphQL- Move the query into the Hydrogen app- Update the query to fetch a product by handle- Display title, price, image & description.- Tailwind styling- Variant picker and buy now button- Bonus if there’s time: Collections page
Prerequisites: - A Chromium-based browser (StackBlitz)- Ideally experience with React. A general web development background would be fine.