JavaScript—still a Ghost

an Illustration of a ghost with the letters J and S for eyes

When I first started learning the JavaScript framework React I experienced quite a bit of cognitive dissonance—it was difficult for me to reconcile everything I had learned about making websites with how React works. This caused me to think the following while writing my first React application:

  • re-inventing HTML is a bad idea
  • this is not JavaScript’s job
  • this ignores some of the fundamental ideas about how the web works

I am mostly concerned with the last: how can I buy into a framework that seemingly discards the strengths of the web?

In this post I am going to expand on the ideas of progressive enhancement I wrote about in JavaScript is a Ghost and hopefully convince you that JavaScript frameworks and web pages that work without JavaScript not only are not at odds, but can actually compliment each other well.

Let’s start by looking at how the different parts of the front end stack work.

Principle and Capability Jump link for Principle and Capability

The W3C’s rule of least power says that we should use the least powerful tool to get the job done:

When designing computer systems, one is often faced with a choice between using a more or less powerful language for publishing information, for expressing constraints, or for solving some problem. This finding explores tradeoffs relating the choice of language to reusability of information. The “Rule of Least Power” suggests choosing the least powerful language suitable for a given purpose.

The Rule of Least Power

So let’s apply our front end stack that way, First let’s rank front end browser technologies by capability.

  1. HTML: humble and helpful, it can easily and reliably display and send information
  2. CSS: beautiful and simple, it can add style, visual structure and basic interactions
  3. JavaScript: your new demiurge, it can do all the things that HTML and CSS can do—in addition, it can mine bitcoins and collect all your personal information for use by large tech companies and foreign governments

Reality Check Jump link for Reality Check

Given these rankings one might think that we most often reach for HTML to solve most of our problems. Well, when using a JavaScript framework we are using JavaScript as our primary tool. So everything should be dependent on JavaScript—right? Regardless of what framework you are using your code needs to contend with some harsh realities if it is going to live on the World Wide Web. Some of these include:

  • varying degrees of network connectivity
  • different support for web API’s depending on what client your website or application is rendered in
  • user preference

Considering these factors, we can imagine a whole lot can go wrong with executing JavaScript in the browser.

I would like to stress that I am not going to suggest we do not use JavaScript, only that we adequately prepare for failure.

React is a State Machine Jump link for React is a State Machine

React is a state machine. State changes dictate how applications work. Need to update your UI? Update state. Building off this idea we can incorporate different states of browser rendering in our application. Need to know if the DOM has loaded and JavaScript is ready to work its magic? Put it in state. The client supports the Geolocation API? Put it in state.

The examples I will present in the next section will show how we can make our applications more reliable and prepared for failure. Having our application respond to the state of the client or browser it is rendering in adds integrity. Integrity and peace of mind knowing that if the HTML was successfully sent—our users can accomplish what they need to.

Listening to the Client Jump link for Listening to the Client

Note ⚠️ : These examples assume that components are rendered as static markup with ReactDOMServer.

The first thing we need to do is populate state with the browser features we would like to qualify. Let’s do this in a top level <App/> component.

class App extends Component {
  constructor() {
    super();
    
    this.state = {
      domReady: false,
      prefersReducedMotion: true,
    };
  }
  
  componentDidMount() {
    this.setState({
      domReady: true,
      prefersReducedMotion: checkPrefersReducedMotion(),
    });
  }

  render() {
    return (
      <Component {...this.state} />
    );
  }
}

Here is a recap of what we are doing here. First, we create two state properties, domReady and prefersReducedMotion. Next, when the component mounts we know that JavaScript is ready to execute. Therefore we use setState to inform our application that features in our application that use JavaScript are available. In addition, we run a helper method that checks whether our user has prefers reduced motion set in their OS. Lastly, we pass down these properties to the rest of our application as properties.

Now that our application is aware of what features our client supports we can conditionally render our components based on these features. Neat!

In Practice Jump link for In Practice

We will first look at an accordion component. It can hide and show content by toggling a button.

class Accordion extends PureComponent {
  constructor() {
    super();

    this.state = {
      expanded: false,
    };
  }

  toggleAccordion() {
    this.setState(prevState => ({
      expanded: !prevState.expanded,
    }));
  }

  render() {
    const { domReady, heading } = this.props;
    const { expanded } = this.state;

    const accordionClasses = classNames({
      accordion: true,
      ['accordion-js']: domReady,
      expanded: expanded,
    });

    return (
      <section
        className={accordionClasses}
        aria-labelledby="accordion-heading"
      >
        <h2 id="accordion-heading">
          {domReady ? (
            <button onClick={() => this.toggleAccordion()}>
              <span>{heading}</span>
            </button>
          ) : (
            <Fragment>{heading}</Fragment>
          )}
        </h2>
        <div>{children}</div>
      </section>
    );
  }
}

Breaking down what’s going on: First we create an expanded property and an accompanying method to handle the hiding and showing of our content. Then we add classNames to our component based on our internal state and the domReady property. If domReady is true we add all our styling that is JavaScript dependent to the component. And finally we only render our button if domReady. Take a look at how the styling works for our different states of rendering.

.accordion {
  //base styling
}
.accordion-js > div {
  overflow: hidden;
  visibility: hidden;
}
.accordion-js.expanded > div {
  visibility: visible;
}

If JavaScript fails to handle our accordion for whatever reason we can rest assured that our users can still read its contents.

Some Audio Jump link for Some Audio

Sometimes we may want to render a completely different component based on our domReady prop. That is the case with this Player component.

function Player(props) {
  return (
    <Fragment>
      {props.domReady ? (
        <ComplexAudioPlayer src={props.src} />
      ) : (
        <audio src={props.src} controls />
      )}
    </Fragment>
  );
}

I like to think of the domReady property we created kind of like a React version of documentReady.

A page can’t be manipulated safely until the document is “ready.”

JQuery Documenation

We should wait until our application knows that JavaScript is ready to render UI that is dependent on JavaScript.

User Preference Jump link for User Preference

Remember our prefersReducedMotion property we added to state earlier. Using this property we can easily adapt to user preference in a declarative straightforward manner.

function Loading(props) {
  return (
    <div className="loading">
      <span>
        <em>loading...</em>
        {!props.prefersReducedMotion && (
          <svg width="50" height="50" viewBox="0 0 50 50">
            <path d="M25.251,6.461c-10.318,0-18.683,8.365-18.683,18.683h4.068c0-8.071,6.543-14.615,14.615-14.615V6.461z">
              <animateTransform
                attributeName="transform"
                type="rotate"
                from="0 25 25"
                to="360 25 25"
                dur="0.6s"
                repeatCount="indefinite"
              />
            </path>
          </svg>
        )}
      </span>
    </div>
  )
}

In this component we only return an animated scalable vector graphic if our user does not have prefers reduced motion set.

Be Prepared Jump link for Be Prepared

All this is about being prepared—being prepared and adding integrity to our code. How do we respond to user preference? How do we respond to varying browser features? How do we respond to unstable network connections? Consider this quote:

The Web is the most hostile software engineering environment imaginable.

Douglas Crockford

Focus on optimizing the most reliable part of the stack—HTML.

One thought on “JavaScript—still a Ghost

Comments are closed.