Reactive UI’s with VanillaJS – Part 2: Class Based Components

Avatar of Brandon Smith
Brandon Smith on

📣 Freelancers, Developers, and Part-Time Agency Owners: Kickstart Your Own Digital Agency with UACADEMY Launch by UGURUS 📣

In Part 1, I went over various functional-style techniques for cleanly rendering HTML given some JavaScript data. We broke our UI up into component functions, each of which returned a chunk of markup as a function of some data. We then composed these into views that could be reconstructed from new data by making a single function call.

This is the bonus round. In this post, the aim will be to get as close as possible to full-blown, class-based React Component syntax, with VanillaJS (i.e. using native JavaScript with no libraries/frameworks). I want to make a disclaimer that some of the techniques here are not super practical, but I think they’ll still make a fun and interesting exploration of how far JavaScript has come in recent years, and what exactly React does for us.

Article Series:

  1. Pure Functional Style
  2. Class Based Components (You are here!)

From functions to classes

Let’s continue using the same example we used in the first post: a blog. Our functional BlogPost component looked like this:

var blogPostData = {
  author: 'Brandon Smith',
  title: 'A CSS Trick',
  body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'
};

function BlogPost(postData) {
  return `<div class="post">
            <h1>${postData.title}</h1>
            <h3>By ${postData.author}</h3>
            <p>${postData.body}</p>
          </div>`;
}

document.querySelector('body').innerHTML = BlogPost(blogPostData);

In class-based components, we’ll still need that same rendering function, but we’ll incorporate it as a method of a class. Instances of the class will hold their own BlogPost data and know how to render themselves.

var blogPostData = {
  author: 'Brandon Smith',
  title: 'A CSS Trick',
  body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'
};

class BlogPost {

  constructor(props) {
    this.state = {
      author: props.author,
      title: props.title,
      body: props.body
    }
  }

  render() {
    return `<div class="post">
              <h1>${this.state.title}</h1>
              <h3>By ${this.state.author}</h3>
              <p>${this.state.body}</p>
            </div>`;
  }

}

var blogPostComponent = new BlogPost(blogPostData);

document.querySelector('body').innerHTML = blogPostComponent.render();

Modifying state

The advantage of a class-based (object oriented) coding style is that it allows for encapsulation of state. Let’s imagine that our blog site allows admin users to edit their blog posts right on the same page readers view them on. Instances of the BlogPost component would be able to maintain their own state, separate from the outside page and/or other instances of BlogPost. We can change the state through a method:

class BlogPost {

  constructor(props) {
    this.state = {
      author: props.author,
      title: props.title,
      body: props.body
    }
  }

  render() {
    return `<div class="post">
              <h1>${this.state.title}</h1>
              <h3>By ${this.state.author}</h3>
              <p>${this.state.body}</p>
            </div>`;
  }

  setBody(newBody) {
    this.state.body = newBody;
  }

}

However, in any real-world scenario, this state change would have to be triggered by either a network request or a DOM event. Let’s explore what the latter would look like since it’s the most common case.

Handling events

Normally, listening for DOM events is straightforward – just use element.addEventListener() – but the fact that our components only evaluate to strings, and not actual DOM elements, makes it trickier. We don’t have an element to bind to, and just putting a function call inside onchange isn’t enough, because it won’t be bound to our component instance. We have to somehow reference our component from the global scope, which is where the snippet will be evaluated. Here’s my solution:

document.componentRegistry = { };
document.nextId = 0;

class Component {
  constructor() {
    this._id = ++document.nextId;
    document.componentRegistry[this._id] = this;
  }
}

class BlogPost extends Component {

  constructor(props) {
    super();

    this.state = {
      author: props.author,
      title: props.title,
      body: props.body
    }
  }

  render() {
    return `<div class="post">
              <h1>${this.state.title}</h1>
              <h3>By ${this.state.author}</h3>
              <textarea onchange="document.componentRegistry[${this._id}].setBody(this.value)">
                ${this.state.body}
              </textarea>
            </div>`;
  }

  setBody(newBody) {
    this.state.body = newBody;
  }

}

Okay, there’s quite a bit going on here.

Referencing the component instance

First, we had to get a reference, from within the HTML string, to the present instance of the component. React is able to do this more easily because JSX actually converts to a series of function calls instead of an HTML string. This allows the code to pass this straight in, and the reference to the JavaScript object is preserved. We, on the other hand, have to serialize a string of JavaScript to insert within our string of HTML. Therefore, the reference to our component instance has to somehow be represented as a string. To accomplish this, we assign each component instance a unique ID at construction time. You don’t have to put this behavior in a parent class, but it’s a good use of inheritance. Essentially what happens is, whenever a BlogPost instance is constructed, it creates a new ID, stores it as a property on itself, and registers itself in document.componentRegistry under that ID. Now, any JavaScript code anywhere can retrieve our object if it has that ID. Other components we might write could also extend the Component class and automatically get unique ID’s of their own.

Calling the method

So we can retrieve the component instance from any arbitrary JavaScript string. Next we need to call the method on it when our event fires (onchange). Let’s isolate the following snippet and step through what’s happening:

<textarea onchange="document.componentRegistry[${this._id}].setBody(this.value)">
  ${this.state.body}
</textarea>

You’re probably familiar with hooking up event listeners by putting code inside on_______ HTML attributes. The code inside will get evaluated and run when the event triggers.

document.componentRegistry[${this._id}] looks in the component registry and gets the component instance by its ID. Remember, all of this is inside a template string, so ${this._id} evaluates to the current component’s ID. The resulting HTML will look like this:

<textarea onchange="document.componentRegistry[0].setBody(this.value)">
  Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
</textarea>

We call the method on that object, passing this.value (where this is the element the event is happening on; in our case, <textarea>) as newBody.

Updating in response to state changes

Our JavaScript variable’s value gets changed, but we need to actually perform a re-render to see its value reflected across the page. In our previous article, we re-rendered like this:

function update() {
  document.querySelector('body').innerHTML = BlogPost(blogPostData);
}

This is another place where we’ll have to make some adjustments for class-style components. We don’t want to throw away and rebuild our component instances every time we re-render; we only want to rebuild the HTML string. The internal state needs to be preserved. So, our objects will exist separately, and we’ll just call render() again:

var blogPost = new BlogPost(blogPostData);

function update() {
  document.querySelector('body').innerHTML = blogPost.render();
}

We then have to call update() whenever we modify state. This is one more thing React does transparently for us; its setState() function modifies the state, and also triggers a re-render for that component. We have to do that manually:

// ...
setBody(newBody) {
  this.state.body = newBody;
  update();
}
// ...

Note that even when we have a complex nested structure of components, there will only ever be one update() function, and it will always apply to the root component.

Child components

React (along with virtually all other JavaScript frameworks) distinguishes between elements and components that comprise a component and those that are its children. Children can be passed in from the outside, allowing us to write custom components that are containers of other arbitrary content. We can do this too.

class BlogPost extends Component {

  constructor(props, children) {
    super();

    this.children = children;
    this.state = {
      author: props.author,
      title: props.title,
      body: props.body
    }
  }

  render() {
    return `<div class="post">
              <h1>${this.state.title}</h1>
              <h3>By ${this.state.author}</h3>
              <textarea onchange="document.componentRegistry[${this._id}].setBody(this.value)">
                ${this.state.body}
              </textarea>
              <div>
                ${this.children.map((child) => child.render()).join('')}
              </div>
            </div>`;
  }

  setBody(newBody) {
    this.state.body = newBody;
    update();
  }

}

This allows us to write usage code like the following:

var adComponents = ...;
var blogPost = new BlogPost(blogPostData, adComponents);

Which will insert the components into the designated location in the markup.

Concluding thoughts

React seems simple, but it does a lot of subtle things to make our lives much easier. The most obvious thing is performance; only rendering the components whose state updates and drastically minimizing the DOM operations that get performed. But some of the less obvious things are important too.

One of these is that by making granular DOM changes instead of rebuilding the DOM entirely, React preserves some natural DOM state that gets lost when using our technique. Things like CSS transitions, user-resized textareas, focus, and cursor position in an input all get lost when we scrap the DOM and reconstruct it. For our use case, that’s workable. But in a lot of situations, it might not be. Of course, we could make DOM modifications ourselves, but then we’re back to square one, and we lose our declarative, functional syntax.

React gives us the advantages of DOM modification while allowing us to write our code in a more maintainable, declarative style. We’ve shown that vanilla JavaScript can do either, but it can’t get the best of both worlds.

Article Series:

  1. Pure Functional Style
  2. Class Based Components (You are here!)