Part Two, Actions on Mount

Asynchronous actions are essential in any modern web application. The ability to fetch data from some service and display it to users is an important part of building a framework to support these applications. In order for our components to do this, we need to refactor. Our component calls mount when we want to render it on the page, but this function only serves to attach styles and event handlers to the component, not fetch data asynchronously. To accommodate actions taken after our component has been defined, but before it is actually rendered, we need a new method: componentDidMount.

The componentDidMount method will be called just before the component's initial render. When called during this stage of the component's life cycle, the componentDidMount method will not have access to any reference to the component's children. As we implement more complex use cases for our components, the life cycle stage of componentDidMount will need to change in order to support features such as child refs.

Moving into our component code, we can do a bit of refactoring:

export class Component {
      constructor(element, args = {}) {
        this.$$typeof = OUR_COMPONENT;
        this.el = document.createElement(element);
        this.args = args;
      }
    
     + componentDidMount() {}
    
      - mount() {
      + setup() {
      + if (this.args.style) this.applyStyles();
      +  if (this.args.on) {
      +    this.args.on.forEach(handler => {
      +      const [event, func] = Object.entries(handler)[0];
      +      this.el.addEventListener(event, func);
      +      Object.entries(handler).forEach(([event, func]) => {
      +        this.el.addEventListener(event, func);
      +      });
      +    });
      +  }
      }
    
      render(children = []) {
       + this.setup();
       + this.componentDidMount();

By default, we want the componentDidMount method to be a no-op since adding life cycle behavior is not always necessary. Logic for applying styles and event handlers is moved to a separate function so that it won't be overridden by or need to be implemented in the componentDidMount method.

We can now put these changes to use by creating a component which fetches an image from a server and applies it as the background of the element created in the component's render method.

import { Component } from './Component';
    
    // fetch the image
    async function getImage(width, height) {
      const response = await fetch(`https://picsum.photos/${width}/${height}`);
      return response.url;
    }
    
    class PageWrapper extends Component {
      async componentDidMount() {
        const width = window.innerWidth;
        const height = window.innerHeight;
        this.el.style.width = width + 'px';
        this.el.style.height = height + 'px';
        const url = await getImage(width, height);
        this.el.style.background = `url(${url})`;
      }
    }
    
    const Container = new PageWrapper("div", {
      style: { background: "papayawhip"}
    })

Our newly created PageWrapper component will now fetch and display a full screen image when it is rendered. You may notice that there is no way to set an intermediary state for our component, such as generating some text to inform the user that the image is loading. This concept of local state will be covered by the next post.