Frontend Masters Boost RSS Feed https://frontendmasters.com/blog Helping Your Journey to Senior Developer Thu, 15 Aug 2024 01:15:38 +0000 en-US hourly 1 https://wordpress.org/?v=6.6.1 225069128 Fine-Grained Reactivity in Svelte 5 https://frontendmasters.com/blog/fine-grained-reactivity-in-svelte-5/ https://frontendmasters.com/blog/fine-grained-reactivity-in-svelte-5/#respond Wed, 14 Aug 2024 15:05:23 +0000 https://frontendmasters.com/blog/?p=3438 We’ve been looking at the up and coming Svelte 5. We looked at basic features like state, props, and side effects. Then we looked at Snippets, which is a lightweight feature Svelte added for re-using bits of HTML within (for now) a single component.

Article Series

In this post, we’ll take a close look at Svelte’s new fine-grained reactivity.

What is fine-grained reactivity?

The best way to describe fine-grained reactivity is to show what it isn’t, and the best example of non-fine grained reactivity is React. In React, in any component, setting a single piece of state will cause the entire component, and all of the descendent components to re-render (unless they’re created with React.memo). Even if the state you’re setting is rendered in a single, simple <span> tag in the component, and not used anywhere else at all, the entire world from that component on down will be re-rendered.

This may seem absurdly wasteful, but in reality this is a consequence of the design features that made React popular when it was new: the data, values, callbacks, etc., that we pass through our component trees are all plain JavaScript. We pass plain, vanilla JavaScript objects, arrays and functions around our components and everything just works. At the time, this made an incredibly compelling case for React compared to alternatives like Angular 1 and Knockout. But since then, alternatives like Svelte have closed the gap. My first post on Svelte 5 showed just how simple, flexible, and most importantly reliable Svlete’s new state management primitives are. This post will show you the performance wins these primitives buy us.

Premature optimization is still bad

This post will walk through some Svelte templates using trickery to snoop on just how much of a component is being re-rendered when we change state. This is not something you will usually do or care about. As always, write clear, understandable code, then optimize when needed (not before). Svelte 4 is considerably less efficient than Svelte 5, but still much more performant than what React does out of the box. And React is more than fast enough for the overwhelming majority of use cases — so it’s all relative.

Being fast enough doesn’t mean we can’t still look at how much of a better performance baseline Svelte now starts you off at. With a fast-growing ecosystem, and now an incredibly compelling performance story, hopefully this post will encourage you to at least look at Svelte for your next project.

If you’d like to try out the code we’ll be looking at in this post, it’s all in this repo.

Getting started

The code we’ll be looking at is from a SvelteKit scaffolded project. If you’ve never used SvelteKit before that’s totally fine. We’re not really using any SvelteKit features until the very end of this post, and even then it’s just re-hashing what we’ll have already covered.

Throughout this post, we’re going to be inspecting if and when individual bindings in a component are re-evaluated when we change state. There’s various ways to do this, but the simplest, and frankly dumbest, is to force some global, non-reactive, always-changing state into these bindings. What do I mean by that? In the root page that hosts our site, I’m adding this:

<script>
  var __i = 0;
  var getCounter = () => __i++;
</script>

This adds a global getCounter function, as well as the __i variable. getCounter will always return the next value, and if we stick a call to it in our bindings, we’ll be able to snoop on when those bindings are being re-executed by Svelte. If you’re using TypeScript, you can avoid errors when calling this like so:

declare global {
  interface Window {
    getCounter(): number;
  }
}

export {};

This post will look at different pages binding to the same data, declared mostly like this (we’ll note differences as we go).

let tasks = [
  { id: 1, title: "Task A", assigned: "Adam", importance: "Low" },
  { id: 2, title: "Task B", assigned: "Adam", importance: "Medium" },
  { id: 3, title: "Task C", assigned: "Adam", importance: "High" },
  { id: 4, title: "Task D", assigned: "Mike", importance: "Medium" },
  { id: 5, title: "Task E", assigned: "Adam", importance: "High" },
  { id: 6, title: "Task F", assigned: "Adam", importance: "High" },
  { id: 7, title: "Task G", assigned: "Steve", importance: "Low" },
  { id: 8, title: "Task H", assigned: "Adam", importance: "High" },
  { id: 9, title: "Task I", assigned: "Adam", importance: "Low" },
  { id: 10, title: "Task J", assigned: "Mark", importance: "High" },
  { id: 11, title: "Task K", assigned: "Adam", importance: "Medium" },
  { id: 12, title: "Task L", assigned: "Adam", importance: "High" },
];

And we’ll render these tasks with this markup:

<div>
  {#each tasks as t}
    <div>
      <div>
        <span>{t.id + getCounter()}</span>
        <button onclick={() => (t.id += 10)} class="border p-2">Update id</button>
     </div>
     <div>
       <span>{t.title + getCounter()}</span>
       <button onclick={() => (t.title += 'X')} class="border p-2">Update title</button>
     </div>
     <div>
        <span>{t.assigned + getCounter()}</span>
        <button onclick={() => (t.assigned += 'X')} class="border p-2">Update assigned</button>
      </div>
      <div>
        <span>{t.importance + getCounter()}</span>
        <button onclick={() => (t.importance += 'X')} class="border p-2">Update importance</button>
      </div>
    </div>
  {/each}
</div>

The Svelte 4 code we’ll start with uses the on:click syntax for events, but everything else will be the same.

The calls to getCounter inside the bindings will let us see when those bindings are re-executed, since the call to getCounter() will always return a new value.

Let’s get started!

Svelte 4

We’ll render the content we saw above, using Svelte 4.

Plain and simple. But now let’s click any of those buttons, to modify one property, of one of those tasks—it doesn’t matter which.

Notice that the entire component (every binding in the component) re-rendered. As inefficient as this seems, it’s still much better than what React does. It’s not remotely uncommon for a single state update to trigger multiple re-renders of many components.

Let’s see how Svelte 5 improves things.

Svelte 5

For Svelte 5, the code is pretty much the same, except we declare our state like this:

let tasks = $state([
  { id: 1, title: "Task A", assigned: "Adam", importance: "Low" },
  <em>// and so on ...</em>
  { id: 12, title: "Task L", assigned: "Adam", importance: "High" },
]);

We render the page, and see the same as before. If you’re following along in the repo, be sure to refresh the page after navigating, so the page will start over with the global counter.

Now let’s change one piece of state, as before. We’ll update the title for Task C, the third one.

Just like that, only the single piece of state we modified has re-rendered. Svelte was smart enough to leave everything else alone. 99% of the time this won’t make any difference, but if you’re rendering a lot of data on a page, this can be a substantial performance win.

Why did this happen?

This is the default behavior when we pass arrays and objects (and arrays of objects) into the $state rune, like we did with:

let tasks = $state([

Svelte will read everything you pass, set up Proxy objects to track what changes, and update the absolute minimum amount of DOM nodes necessary.

False Coda

We could end the post here. Use the $state primitive to track your reactive data. Svelte will make it deeply reactive, and update whatever it needs to update when you change anything. This will be just fine the vast majority of the time.

But what if you’re writing a web application that has to manage a ton of data? Making everything deeply reactive is not without cost.

Let’s see how we can tell Svelte that only some of our data is reactive. I’ll stress again, laboring over this will almost never be needed. But it’s good to know how it works if it ever comes up.

Rediscovering a long-lost JavaScript feature

Classes in JavaScript have gotten an unfortunately bad reputation. Classes are an outstanding way to declare the structure of a set of objects, which also happen to come with a built-in factory function for creating those objects. Not only that, but TypeScript is deeply integrated with them.

You can declare:

class Person {
  firstName: string;
  lastName: string;

  constructor(firstName: string, lastName: string) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
}

Not only will this provide you a factory function for creating instances of a Person, via new Person('Adam', 'Rackis'), but Person can also be used as a type within TypeScript. You can create variables or function parameters of type Person. It’s one of the few things that exist as a runtime construct and also a TypeScript type.

That said, if you find yourself reaching for extends in order to create deep inheritance hierarchies with classes, please please re-think your decisions.

Anyway, why am I bringing up classes in this post?

Fine-grained reactivity in Svelte 5

If you have a performance-sensitive section of code where you need to mark some properties as non-reactive, you can do this by creating class instances rather than vanilla JavaScript objects. Let’s define a Task class for our tasks. For the properties we want to be reactive, we’ll set default values with the $state() rune. For properties we don’t want to be reactive, we won’t.

class Task {
  id: number = 0;
  title = $state("");
  assigned = $state("");
  importance = $state("");

  constructor(data: Task) {
    Object.assign(this, data);
  }
}

And then we just use that class

let tasks = $state([
  new Task({ id: 1, title: "Task A", assigned: "Adam", importance: "Low" }),
  <em>// and so on</em>
  new Task({ id: 12, title: "Task L", assigned: "Adam", importance: "High" }),
]);

I simplified the class a bit by taking a raw object with all the properties of the class, and assigning those properties with Object.assign. The object literal is typed in the constructor as Task, the same as the class, but that’s fine because of TypeScript’s structural typing.

When we run that, we’ll see the same exact thing as before, except clicking the button to change the id will not re-render anything at all in our Svelte component. To be clear, the id is still changing, but Svelte is not re-rendering. This demonstrates Svelte intelligently not wiring any kind of observability into that particular property.

Side note: if you wanted to encapsulate / protect the id, you could declare id as #id to make it a private property and then expose the value with a getter function.

Going deeper

What if you don’t want these tasks to be reactive at the individual property at all? What if we have a lot of these tasks coming down, and you’re not going to be editing them? So rather than have Svelte set up reactivity for each of the tasks’ properties, you just want the array itself to be reactive.

You basically want to be able to add or remove entries in your array, and have Svelte update the tasks that are rendered. But you don’t want Svelte setting up any kind of reactivity for each property on each task.

This is a common enough use case that other state management systems support this directly, for example, MobX’s observable.shallow. Unfortunately Svelte does not have any such helper, as of yet. That said, it is currently being debated, so keep your eyes open for a $state.shallow() that would do what we’re about to show. But even if it does get added, implementing it ourselves will be a great way to kick the tires of Svelte’s new reactivity system. Let’s see how.

Implementing our own $state.shallow() equivalent

We already saw how passing class instances to an array shut off fine-grained reactivity by default, leaving you to opt-in, as desired, by setting class fields to $state(). But our data are likely coming from a database, as plain (hopefully typed) JavaScript objects, unrelated to any class; more importantly we likely have zero desire to cobble together a class just for this.

So let’s simulate it. Let’s say that a database is providing our Task objects as JS objects. We (of course) have a type for this:

type Task = {
  id: number;
  title: string;
  assigned: string;
  importance: string;
};

We want to put those instances into an array that itself is reactive, but not the individual properties on the tasks. With a tiny bit of cleverness we can make it mostly painless.

class NonReactiveObjectGenerator {
  constructor(data: unknown) {
    Object.assign(this, data);
  }
}

function shallowObservable<T>(data: T[]): T[] {
  let result = $state(data.map(t => new NonReactiveObjectGenerator(t) as T));
  return result;
}

Our NonReactiveObjectGenerator class takes in any object, and then smears all that object’s properties onto itself. And our shallowObservable takes an array of whatever, and maps it onto instances of our NonReactiveObjectGenerator class. This will force each instance to be a class instance, with nothing reactive. The as T is us forcing TypeScript to treat these new instances as whatever type was passed in. This is accurate, but something TypeScript needs help understanding, since it’s not (as of now) able to read and understand our call to Object.assign in the class constructor.

If you closely read my first post on Svelte 5, you might recall that you can’t directly return reactive state from a function, since the state will be read and unwrapped right at the call-site, and won’t be reactive any longer. Normally you’d have to do this:

return {
  get value() {
    return result;
  },
  set value(newData: T[]) {
    result = newData;
  },
};

Why wasn’t that needed here? It’s true, the $state() value will be read at the function’s call site. So with…

let tasks = shallowObservable(getTasks());

…the tasks variable will not be reactive. But the array itself will still be fully reactive. We can still call push, pop, splice and so on. If you can live without needing to re-assign to the variable, this is much simpler. But even if you do need to set the tasks variable to a fresh array of values, you still don’t even need to use variable assignment. Stay tuned.

I changed the initial tasks array to help out in a minute, but the rest is what you’d expect.

const getTasks = () => [
  { id: 1, title: "Task A", assigned: "Adam", importance: "Low" },
  <em>// ...</em>
  { id: 12, title: "Task L", assigned: "Adam", importance: "High" },
];

let tasks = shallowObservable(getTasks());

And with that, rendering should now work, and none of our properties are reactive. Clicking the edit buttons do nothing.

But we can now add a button to push a new task onto our array.

<button
  onclick={() =>
    tasks.value.push(
      new NonReactiveObjectGenerator({
        id: nextId++,
        title: 'New task',
        assigned: 'Adam',
        importance: 'Low'
      }) as Task
    )}
>
  Add new task
</button>

We can even add a delete button to each row.

<button onclick={() => tasks.value.splice(idx, 1)}>
  Delete
</button>

Yes, Svelte’s reactive array is smart enough to understand push and splice.

Editing tasks this way

You might be wondering if we can still actually edit the individual tasks. We assumed the tasks would be read-only, but what if that changes? We’ve been modifying the array and watching Svelte re-render correctly. Can’t we edit an individual task by just cloning the task, updating it, and then re-assigning to that index? The answer is yes, with a tiny caveat.

Overriding an array index (with a new object instance) does work, and makes Svelte update. But we can’t just do this:

tasks[idx] = { ...t, importance: "X" + t };

Since that would make the new object, which is an object literal, deeply reactive. We have to keep using our class. This time, to keep the typings simple, and to keep the code smell that is the NonReactiveObjectGenerator class hidden as much as possible, I wrote up a helper function.

function cloneNonReactive<T>(data: T): T {
  return new NonReactiveObjectGenerator(data) as T;
}

As before, the type assertion is unfortunately needed. This same function could also be used for the add function we saw above, if you prefer.

To prove editing works, we’ll leave the entire template alone, except for the importance field, which we’ll modify like so

  <div>
    <span>{t.importance + getCounter()}</span>
    <button
      onclick={() => {
        const taskClone = cloneNonReactive(t);
        taskClone.importance += 'X';
        tasks[idx] = cloneNonReactive(taskClone);
      }}
    >
      Update importance
    </button>
</div>

Now running shows everything as it’s always been.

If we click the button to change the id, title or assigned value, nothing changes, because we’re still mutating those properties directly (since I didn’t change anything) in order to demonstrate that they’re not reactive. But clicking the button to update the importance field runs the code above, and updates the entire row, showing any other changes we’ve made.

Here I clicked the button to update the title, twice, and then clicked the button to update the importance. The former did nothing, but the latter updated the component to show all changes.

Re-assigning to the tasks array

We saved a bit of convenience by returning our state value directly from our shallowObservable helper, but at the expense of not being able to assign directly to our array. Or did we?

If you know a bit of JavaScript, you might know…

tasks.length = 0;

…is the old school way to clear an array. That works with Svelte; the Proxy object Svelte sets up to make our array observable works with that. Similarly, we can set the array to a fully new array of values (after clearing it like we just saw) like this:

tasks.push(...newArray);

It’s up to you which approach you take, but hopefully Svelte ships a $state.shallow to provide the best of both worlds: the array would be reactive, and so would the binding, since we don’t have to pass it across a function boundary; it would be built directly into $state.

SvelteKit

Let’s wrap up by briefly talking about how data from SvelteKit loaders is treated in terms of reactivity. In short, it’s exactly how you’d expect. First and foremost, if you return a raw array of objects from your loader like this:

export const load = () => {
  return {
    tasks: [
      { id: 1, title: "Task A", assigned: "Adam", importance: "Low" },
      <em>// ...</em>
      { id: 12, title: "Task L", assigned: "Adam", importance: "High" },
    ],
  };
};

Then none of that data will be reactive in your component. This is to be expected. To make data reactive, you need to wrap it in $state(). As of now, you can’t call $state in a loader, only in a universal Svelte file (something that ends in .svelte.ts). Hopefully in the future Svelte will allow us to have loaders named +page.svelte.ts but for now we can throw something like this in a reactive-utils.svelte.ts file.

export const makeReactive = <T>(arg: T[]): T[] => {
  let result = $state(arg);
  return result;
};

Then import it and use it in our loader.

import { makeReactive } from "./reactive-utils.svelte";

export const load = () => {
  return {
    tasks: makeReactive([
      { id: 1, title: "Task A", assigned: "Adam", importance: "Low" },
      <em>// ...</em>
      { id: 12, title: "Task L", assigned: "Adam", importance: "High" },
    ]),
  };
};

Now those objects will support the same fine-grained reactivity we saw before. To customize which properties are reactive, you’d swap in class instances, instead of vanilla object literals, again just like we saw. All the same rules apply.

If you’re wondering why we did this…

export const makeReactive = <T>(arg: T[]): T[] => {
  let result = $state(arg);
  return result;
};

…rather than this…

export const makeReactive = <T>(arg: T[]): T[] => {
  return $state(arg);
};

… the answer is that the latter is simply disallowed. Svelte forces you to only put $state() calls into assignments. It cannot appear as a return value like this. The reason is that while returning $state variables directly across a function boundary works fine for objects and arrays, doing this for primitive values (strings or numbers) would produce a senseless result. The variable could not be re-assigned (same as we saw with the array), but as a primitive, there’d be no other way to edit it. It would just be a non-reactive constant.

Svelte forcing you to take that extra step, and assign $state to a variable before returning, is intended to help prevent you from making that mistake.

Wrapping up

One of the most exciting features of Svelte 5 is the fine-grained reactivity it adds. Svelte was already lightweight, and faster than most, if not all of the alternatives. These additions in version 5 only improve on that. When added to the state management improvements we’ve already covered in prior posts, Svelte 5 really becomes a serious framework option.

Consider it for your next project.

Article Series

]]>
https://frontendmasters.com/blog/fine-grained-reactivity-in-svelte-5/feed/ 0 3438
Patterns for Reactivity with Modern Vanilla JavaScript https://frontendmasters.com/blog/vanilla-javascript-reactivity/ https://frontendmasters.com/blog/vanilla-javascript-reactivity/#comments Mon, 21 Aug 2023 03:41:00 +0000 http://fem.flywheelsites.com/?p=40 “Reactivity” is how systems react to changes in data. There are many types of reactivity, but for this article, reactivity is when data changes, you do things.

Article Series

Reactivity Patterns are Core to Web Development

We handle a lot with JavaScript in websites and web apps since the browser is an entirely asynchronous environment. We must respond to user inputs, communicate with servers, log, perform, etc. All these tasks involve updates to the UI, Ajax requests, browser URLs, and navigation changes, making cascading data changes a core aspect of web development.

As an industry, we associate reactivity with frameworks, but you can learn a lot by implementing reactivity in pure JavaScript. We can mix and match these patterns to wire behavior to data changes.

Learning core patterns with pure JavaScript will lead to less code and better performance in your web apps, no matter what tool or framework you use.

I love learning patterns because they apply to any language and system. Patterns can be combined to solve your app’s exact requirements, often leading to more performant and maintainable code.

Hopefully, you’ll learn new patterns to add to your toolbox, no matter what frameworks and libraries you use!

PubSub Pattern (Publish Subscriber)

PubSub is one of the most foundational patterns for reactivity. Firing an event out with publish() allows anyone to listen to that event subscribe() and do whatever they want in a decoupled from whatever fires that event.

const pubSub = {
  events: {},
  subscribe(event, callback) {
    if (!this.events[event]) this.events[event] = [];
    this.events[event].push(callback);
  },
  publish(event, data) {
    if (this.events[event]) this.events[event].forEach(callback => callback(data));
  }
};

pubSub.subscribe('update', data => console.log(data));
pubSub.publish('update', 'Some update'); // Some update

Note the publisher has no idea of what is listening to it, so there is no way to unsubscribe or clean up after itself with this simple implementation.

Custom Events: Native Browser API for PubSub

The browser has a JavaScript API for firing and subscribing to custom events. It allows you to send data along with the custom events using dispatchEvent.

const pizzaEvent = new CustomEvent("pizzaDelivery", {
  detail: {
    name: "supreme",
  },
});

window.addEventListener("pizzaDelivery", (e) => console.log(e.detail.name));
window.dispatchEvent(pizzaEvent);

You can scope these custom events to any DOM node. In the code example, we use the global window object, also known as a global event bus, so anything in our app can listen and do something with the event data.

<div id="pizza-store"></div>
const pizzaEvent = new CustomEvent("pizzaDelivery", {
  detail: {
    name: "supreme",
  },
});

const pizzaStore = document.querySelector('#pizza-store');
pizzaStore.addEventListener("pizzaDelivery", (e) => console.log(e.detail.name));
pizzaStore.dispatchEvent(pizzaEvent);

Class Instance Custom Events: Subclassing EventTarget

We can subclass EventTarget to send out events on a class instance for our app to bind to:

class PizzaStore extends EventTarget {
  constructor() {
    super();
  }
  addPizza(flavor) {
    // fire event directly on the class
    this.dispatchEvent(new CustomEvent("pizzaAdded", {
      detail: {
        pizza: flavor,
      },
    }));
  }
}

const Pizzas = new PizzaStore();
Pizzas.addEventListener("pizzaAdded", (e) => console.log('Added Pizza:', e.detail.pizza));
Pizzas.addPizza("supreme");

The cool thing about this is your events aren’t firing globally on the window. You can fire an event directly on a class; anything in your app can wire up event listeners directly to that class.

Observer Pattern

The observer pattern has the same basic premise as the PubSub pattern. It allows you to have behavior “subscribed” to a Subject. And when the Subject fires the notify method, it notifies everything subscribed.

class Subject {
  constructor() {
    this.observers = [];
  }
  addObserver(observer) {
    this.observers.push(observer);
  }
  removeObserver(observer) {
    const index = this.observers.indexOf(observer);
    if (index > -1) {
      this.observers.splice(index, 1);
    }
  }
  notify(data) {
    this.observers.forEach(observer => observer.update(data));
  }
}

class Observer {
  update(data) {
    console.log(data);
  }
}

const subject = new Subject();
const observer = new Observer();

subject.addObserver(observer);
subject.notify('Everyone gets pizzas!');

The main difference between this and PubSub is that the Subject knows about its observers and can remove them. They aren’t completely decoupled like in PubSub.

Reactive Object Properties with Proxies

Proxies in JavaScript can be the foundation for performing reactivity after setting or getting properties on an object.

const handler = {
  get: function(target, property) {
    console.log(`Getting property ${property}`);
    return target[property];
  },
  set: function(target, property, value) {
    console.log(`Setting property ${property} to ${value}`);
    target[property] = value;
    return true; // indicates that the setting has been done successfully
  }
};

const pizza = { name: 'Margherita', toppings: ['tomato sauce', 'mozzarella'] };
const proxiedPizza = new Proxy(pizza, handler);

console.log(proxiedPizza.name); // Outputs "Getting property name" and "Margherita"
proxiedPizza.name = 'Pepperoni'; // Outputs "Setting property name to Pepperoni"

When you access or modify a property on the proxiedPizza, it logs a message to the console. But you could imagine wiring any functionality to property access on an object.

Reactive Individual Properties: Object.defineProperty

You can do an identical thing for a specific property using Object.defineProperty. You can define getters and setters for properties and run code when a property is accessed or modified.

const pizza = {
  _name: 'Margherita', // Internal property
};

Object.defineProperty(pizza, 'name', {
  get: function() {
    console.log(`Getting property name`);
    return this._name;
  },
  set: function(value) {
    console.log(`Setting property name to ${value}`);
    this._name = value;
  }
});

// Example usage:
console.log(pizza.name); // Outputs "Getting property name" and "Margherita"
pizza.name = 'Pepperoni'; // Outputs "Setting property name to Pepperoni"

Here, we’re using Object.defineProperty to define a getter and setter for the name property of the pizza object. The actual value is stored in a private _name property, and the getter and setter provide access to that value while logging messages to the console.

Object.defineProperty is more verbose than using a Proxy, especially if you want to apply the same behavior to many properties. But it’s a powerful and flexible way to define custom behavior for individual properties.

Asynchronous Reactive Data with Promises

Let’s make using the observers asynchronous! This way we can update the data and have multiple observers run asynchronously.

class AsyncData {
  constructor(initialData) {
    this.data = initialData;
    this.subscribers = [];
  }

  // Subscribe to changes in the data
  subscribe(callback) {
    if (typeof callback !== 'function') {
      throw new Error('Callback must be a function');
    }
    this.subscribers.push(callback);
  }

  // Update the data and wait for all updates to complete
  async set(key, value) {
    this.data[key] = value;

    // Call the subscribed function and wait for it to resolve
    const updates = this.subscribers.map(async (callback) => {
      await callback(key, value);
    });

    await Promise.allSettled(updates);
  }
}

Here’s a class that wraps a data object and triggers an update when the data changes.

Awaiting Our Async Observers

Let’s say we want to wait until all subscriptions to our asynchronous reactive data are processed:

const data = new AsyncData({ pizza: 'Pepperoni' });

data.subscribe(async (key, value) => {
  await new Promise(resolve => setTimeout(resolve, 500));
  console.log(`Updated UI for ${key}: ${value}`);
});

data.subscribe(async (key, value) => {
  await new Promise(resolve => setTimeout(resolve, 1000));
  console.log(`Logged change for ${key}: ${value}`);
});

// function to update data and wait for all updates to complete
async function updateData() {
  await data.set('pizza', 'Supreme'); // This will call the subscribed functions and wait for their promises to resolve
  console.log('All updates complete.');
}

updateData();

Our updateData function is now async, so we can await all the subscribed functions to resolve before continuing our program. This pattern allows juggling asynchronous reactivity a bit simpler.

Reactive Systems

Many more complex reactive systems are at the foundations of popular libraries and frameworks: hooks in React, Signals in Solid, Observables in Rx.js, and more. They usually have the same basic premise of when data changes, re-render the components or associated DOM fragments.

Observables (Pattern of Rx.js)

Observables and Observer Pattern are not the same despite being nearly the same word, lol.

Observables allow you to define a way to produce a sequence of values over time. Here is a simple Observable primitive that provides a way to emit a sequence of values to subscribers, allowing them to react as those values are produced.

class Observable {
  constructor(producer) {
    this.producer = producer;
  }

  // Method to allow a subscriber to subscribe to the observable
  subscribe(observer) {
    // Ensure the observer has the necessary functions
    if (typeof observer !== 'object' || observer === null) {
      throw new Error('Observer must be an object with next, error, and complete methods');
    }

    if (typeof observer.next !== 'function') {
      throw new Error('Observer must have a next method');
    }

    if (typeof observer.error !== 'function') {
      throw new Error('Observer must have an error method');
    }

    if (typeof observer.complete !== 'function') {
      throw new Error('Observer must have a complete method');
    }

    const unsubscribe = this.producer(observer);

    // Return an object with an unsubscribe method
    return {
      unsubscribe: () => {
        if (unsubscribe && typeof unsubscribe === 'function') {
          unsubscribe();
        }
      },
    };
  }
}

Here’s how you would use them:

// Create a new observable that emits three values and then completes
const observable = new Observable(observer => {
  observer.next(1);
  observer.next(2);
  observer.next(3);
  observer.complete();

  // Optional: Return a function to handle any cleanup if the observer unsubscribes
  return () => {
    console.log('Observer unsubscribed');
  };
});

// Define an observer with next, error, and complete methods
const observer = {
  next: value => console.log('Received value:', value),
  error: err => console.log('Error:', err),
  complete: () => console.log('Completed'),
};

// Subscribe to the observable
const subscription = observable.subscribe(observer);

// Optionally, you can later unsubscribe to stop receiving values
subscription.unsubscribe();

The critical component of an Observable is the next() method, which sends data to the observers. A complete() method for when the Observable stream closes. And an error() method when something goes wrong. Also, there has to be a way to subscribe() to listen for changes and unsubscribe() to stop receiving data from the stream.

The most popular libraries that use this pattern are Rx.js and MobX.

“Signals” (Pattern of SolidJS)

Hat tip Ryan Carniato’s Reactivity with SolidJS course.

const context = [];

export function createSignal(value) {
    const subscriptions = new Set();

    const read = () => {
        const observer = context[context.length - 1]
        if (observer) subscriptions.add(observer);
        return value;
    }
    const write = (newValue) => {
        value = newValue;
        for (const observer of subscriptions) {
            observer.execute()
        }
    }

    return [read, write];
}

export function createEffect(fn) {
    const effect = {
        execute() {
            context.push(effect);
            fn();
            context.pop();
        }
    }

    effect.execute();
}

Using the reactive system:

import { createSignal, createEffect } from "./reactive";

const [count, setCount] = createSignal(0);

createEffect(() => {
  console.log(count());
}); // 0

setCount(10); // 10

Here’s the complete code for his vanilla reactivity system with a code sample that Ryan writes in his course.

“Observable-ish” Values (Frontend Masters)

Our Frontend Masters video player has many configurations that could change anytime to modify video playback. Kai on our team created “Observable-ish” Values (many years ago now, but we just published it for this article’s sake), which is another take on a reactive system in vanilla JavaScript.

It’s less than 100 lines of code and has stood the test of time! For 7+ years, this tiny bit of code has underpinned delivering millions of hours of video. It’s a mix of PubSub with the ability to have computed values by adding the results of multiple publishers together.

Here’s how you use the “Observable-ish” values. Publish changes to subscriber functions when values change:

const fn = function(current, previous) {}

const obsValue = ov('initial');
obsValue.subscribe(fn);      // subscribe to changes
obsValue();                  // 'initial'
obsValue('initial');         // identical value, no change
obsValue('new');             // fn('new', 'initial')
obsValue.value = 'silent';   // silent update

Modifying arrays and objects will not publish, but replacing them will.

const obsArray = ov([1, 2, 3]);
obsArray.subscribe(fn);
obsArray().push(4);          // silent update
obsArray.publish();          // fn([1, 2, 3, 4]);
obsArray([4, 5]);            // fn([4, 5], [1, 2, 3]);

Passing a function caches the result as the value. Any extra arguments will be passed to the function. Any observables called within the function will be subscribed to, and updates to those observables will recompute the value.

Child observables must be called; mere references are ignored. If the function returns a Promise, the value is assigned async after resolution.

const a = ov(1);
const b = ov(2);
const computed = ov(arg => { a() + b() + arg }, 3);
computed.subscribe(fn);
computed();                  // fn(6)
a(2);                        // fn(7, 6)

Reactive Rendering of UI

Here are some patterns for writing and reading from the DOM and CSS.

Render Data to HTML String Literals

Here’s a simple example of rendering some pizza UI based on data.

function PizzaRecipe(pizza) {
  return `<div class="pizza-recipe">
    <h1>${pizza.name}</h1>
    <h3>Toppings: ${pizza.toppings.join(', ')}</h3>
    <p>${pizza.description}</p>
  </div>`;
}

function PizzaRecipeList(pizzas) {
  return `<div class="pizza-recipe-list">
    ${pizzas.map(PizzaRecipe).join('')}
  </div>`;
}

var allPizzas = [
  {
    name: 'Margherita',
    toppings: ['tomato sauce', 'mozzarella'],
    description: 'A classic pizza with fresh ingredients.'
  },
  {
    name: 'Pepperoni',
    toppings: ['tomato sauce', 'mozzarella', 'pepperoni'],
    description: 'A favorite among many, topped with delicious pepperoni.'
  },
  {
    name: 'Veggie Supreme',
    toppings: ['tomato sauce', 'mozzarella', 'bell peppers', 'onions', 'mushrooms'],
    description: 'A delightful vegetable-packed pizza.'
  }
];

// Render the list of pizzas
function renderPizzas() {
  document.querySelector('body').innerHTML = PizzaRecipeList(allPizzas);
}

renderPizzas(); // Initial render

// Example of changing data and re-rendering
function addPizza() {
  allPizzas.push({
    name: 'Hawaiian',
    toppings: ['tomato sauce', 'mozzarella', 'ham', 'pineapple'],
    description: 'A tropical twist with ham and pineapple.'
  });

  renderPizzas(); // Re-render the updated list
}

// Call this function to add a new pizza and re-render the list
addPizza();

addPizza demonstrates how to change the data by adding a new pizza recipe to the list and then re-rendering the list to reflect the changes.

The main drawback of this approach is you blow away the entire DOM on every render. You can more intelligently update only the bits of DOM that change using a library like lit-html (lit-html usage guide). We do this with several highly dynamic components on Frontend Masters, like our data grid component.

See examples of other approaches in the Vanilla TodoMVC repo and associated Vanilla TodoMVC article.

Reactive DOM Attributes: MutationObserver

One way to make DOM reactive is to add and remove attributes. We can listen to changes in attributes using the MutationObserver API.

const mutationCallback = (mutationsList) => {
  for (const mutation of mutationsList) {
    if (
      mutation.type !== "attributes" ||
      mutation.attributeName !== "pizza-type"
    ) return;

    console.log('old:', mutation.oldValue)
    console.log('new:', mutation.target.getAttribute("pizza-type"))
  }
}
const observer = new MutationObserver(mutationCallback);
observer.observe(document.getElementById('pizza-store'), { attributes: true });

Now we can update the pizza-type attribute from anywhere in our program, and the element itself can have behavior attached to updating that attribute!

Reactive Attributes in Web Components

With Web Components, there is a native way to listen and react to attribute updates.

class PizzaStoreComponent extends HTMLElement {
  static get observedAttributes() {
    return ['pizza-type'];
  }

  constructor() {
    super();
    const shadowRoot = this.attachShadow({ mode: 'open' });
    shadowRoot.innerHTML = `<p>${this.getAttribute('pizza-type') || 'Default Content'}</p>`;
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'my-attribute') {
      this.shadowRoot.querySelector('div').textContent = newValue;
      console.log(`Attribute ${name} changed from ${oldValue} to ${newValue}`);
    }
  }
}

customElements.define('pizza-store', PizzaStoreComponent);
<pizza-store pizza-type="Supreme"></pizza-store>
document.querySelector('pizza-store').setAttribute('pizza-type', 'BBQ Chicken!');

This is a bit simpler, but we have to use Web Components to use this API.

Reactive Scrolling: IntersectionObserver

We can wire reactivity to DOM elements scrolling into view. I’ve used this for slick animations on our marketing pages.

var pizzaStoreElement = document.getElementById('pizza-store');

var observer = new IntersectionObserver(function(entries, observer) {
  entries.forEach(function(entry) {
    if (entry.isIntersecting) {
      entry.target.classList.add('animate-in');
    } else {
      entry.target.classList.remove('animate-in');
    }
  });
});

observer.observe(pizzaStoreElement);

Here’s an example scrolling animation CodePen in very few lines of code using IntersectionObserver.

Animation & Game Loop: requestAnimationFrame

When working with game development, Canvas, WebGL, or those wild marketing sites, animations often require writing to a buffer and then writing the results on a given loop when the rendering thread becomes available. We do this with requestAnimationFrame.

function drawStuff() {
  // This is where you'd do game or animation rendering logic
}

// function to handle the animation
function animate() {
  drawStuff();
  requestAnimationFrame(animate); // Continually calls animate when the next render frame is available
}

// Start the animation
animate();

This is the method games and anything that involves real-time rendering use to render the scene when frames become available.

Reactive Animations: Web Animations API

You can also create reactive animations with the Web Animations API. Here we will animate an element’s scale, position, and color using the animation API.

const el = document.getElementById('animatedElement');

// Define the animation properties
const animation = el.animate([
  // Keyframes
  { transform: 'scale(1)', backgroundColor: 'blue', left: '50px', top: '50px' },
  { transform: 'scale(1.5)', backgroundColor: 'red', left: '200px', top: '200px' }
], {
  // Timing options
  duration: 1000,
  fill: 'forwards'
});

// Set the animation's playback rate to 0 to pause it
animation.playbackRate = 0;

// Add a click event listener to the element
el.addEventListener('click', () => {
  // If the animation is paused, play it
  if (animation.playbackRate === 0) {
    animation.playbackRate = 1;
  } else {
    // If the animation is playing, reverse it
    animation.reverse();
  }
});

What’s reactive about this is that the animation can play relative to where it is located when an interaction occurs (in this case, reversing its direction). Standard CSS animations and transitions aren’t relative to their current position.

Reactive CSS: Custom Properties and calc

Lastly, we can write CSS that’s reactive by combining custom properties and calc.

barElement.style.setProperty('--percentage', newPercentage);

In JavaScript, you can set a custom property value.

.bar {
  width: calc(100% / 4 - 10px);
  height: calc(var(--percentage) * 1%);
  background-color: blue;
  margin-right: 10px;
  position: relative;
}

And in the CSS, we can now do calculations based on that percentage. It’s pretty cool that we can add calculations right into the CSS and let CSS do its job of styling without having to keep all that rendering logic in JavaScript.

FYI: You can also read these properties if you want to create changes relative to the current value.

getComputedStyle(barElement).getPropertyValue('--percentage');

The Many Ways to Achieve Reactivity

It’s incredible how many ways we can achieve reactivity using very little code in modern vanilla JavaScript. We can combine these patterns in any way we see fit for our apps to reactively render, log, animate, handle user events, and all the things that can happen in the browser.

Next, check out the JavaScript Learning Path and learn JavaScript deeply from awesome instructors like Anjana Vakil, Will Sentance and Kyle Simpson! Or dive right into the most loved course on the platform, JavaScript: The Hard Parts!

~ Frontend Masters Team

Article Series

]]>
https://frontendmasters.com/blog/vanilla-javascript-reactivity/feed/ 3 40