Home Blog Why and How You Should Write Progressively Enhanced Idempotent (Stateless) JavaScript

DEV

Why and How You Should Write Progressively Enhanced Idempotent (Stateless) JavaScript

Posted by antoine on July 12, 2015, 6 p.m.

480px-JavaScript-logo.png

 

As a user, there's nothing that annoys me more than a non-working website; as a developer, I noticed that most of these bugs happen in JavaScript.

Writing JavaScript is the trickiest part when programming a website. Your back-end is always going to run in the same environment, in a reproducible manner. However, with client side code, this could be run by many different platforms (browsers) which all have a different behavior. In addition to this, browsers come in different versions which implement different sets of features.

If you want to support the 2 latest versions of each browser, you end up having to test and write code for no less than 12 environments! (2 versions of Firefox, Chrome, Internet Explorer, Safari, Opera, Android's Browser.)

Add to this people who are still using very, very old Android phones, and you get a deadly cocktail for front-end engineers.

As a preamble, this blog post talks about simple JavaScript interactions which improve the user experience. We're not talking about single pages application. In these cases, you should use React.js if you want to go mainstream, Riot.js if you're worried about size, or Mercury if you like modularity.

In this blog post, we'll be talking about simple JavaScript interactions, very often written only using jQuery.

Also, this blog post assumes that you have basic knowledge of html, web accessibility and javascript.

Progressive Enhancement

Progressive enhancement is a feature that is very easy to implement, but never done. There are many myths about it though, like " I have to implement my logic twice", or "we don't have users without javascript.”

Some of these myths are partially true, some of them are totally irrelevant. The worst one is "we don't have to users without javascript." Some blind people actually use full-text browsers, without any javascript. Another thing, if your javascript breaks and crashes early, you end up with a page behaving the same way as if there were no javascript.

The thing is that progressive enhancement is very easy to do, but many front-end engineers just don't care about it.

In order to understand that, let me give you an example. This how many people write tabs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<code>
  <!-- Tab headers -->
  <nav class="tabs">
    <a href="javascript:show_tab('default')">Default</a>
    <a href="javascript:show_tab('other')">Other tab</a>
  </nav>

  <!-- Tabs content -->
  <div class="tabs-content">
    <section id="default">
      <p>This is the content of the default tab</p>
    </section>

    <section id="other" style="display:none;">
      <p>This is the content of the other tab!</p>
    </section>
  </div>

  <script>
    jQuery(function ($) {
      var current_tab = 'default';

      window.show_tab = function (name) {
        if (current_tab != name) {
          $('#' + current_tab).hide();
          $('#' + name).show();
          current_tab = name;
        }
      }
    });
  </script>
</code>

In this previous example, if you load the web page without JavaScript, the other tab is going to be hidden, and there is no way to display it.

One of the rules of progressive enhancement is to do anything related to the javascript interaction in JavaScript. For example, the section other is hidden because the user is expected to be able to display it thanks to javascript.

So the way to fix that is to remove the hardcoded style="display:none;", and hide the other tab in javascript like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<code>
  <!-- Tab headers -->
  <nav class="tabs">
    <a href="javascript:show_tab('default')">Default</a>
    <a href="javascript:show_tab('other')">Other tab</a>
  </nav>

  <!-- Tabs content -->
  <div class="tabs-content">
    <section id="default">
      <p>This is the content of the default tab</p>
    </section>

    <section id="other">
      <p>This is the content of the other tab!</p>
    </section>
  </div>

  <script>
    jQuery(function ($) {
      var current_tab = 'default';

      $("#other").hide();

      window.show_tab = function (name) {
        if (current_tab != name) {
          $('#' + current_tab).hide();
          $('#' + name).show();
          current_tab = name;
        }
      }
    });
  </script>
</code>

That way, if the javascript is broken on this browser, or if a user on the page doesn't have javascript, they will be able to see the entire content of the page.

Idempotent JavaScript

This is a very well-known technique for system programmers, but it seems that the closer we get to the front-end, the more forgotten it gets.

People use different names for it : "idempotent" or "stateless.” This technique consists in detecting the state of your current system, and finding which steps need to be taken in order to switch the state the system should be in.

The point of this is to avoid storing a state in your program's memory. The said state could get out of sync with the real environment. This paradigm is very common when writing cron jobs or configuration management scripts.

Here's how it works:

  • First of all, you try to discover which state you system is

  • Then you compute the difference between the state your system is and the state you want your system to be in.

  • You execute your computed difference

With jQuery, it's easier because many functions are like .hide() and .show(), meaning they are idempotent. Calling .hide() on a hidden object will be a "no-op."

This is what it would look like with our example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<code>
  <!-- Tab headers -->
  <nav class="tabs">
    <a href="#default">Default</a>
    <a href="#other">Other tab</a>
    <a href="#last">Last tab</a>
  </nav>

  <!-- Tabs content -->
  <div class="tabs-content">
    <section id="default">
      <p>This is the content of the default tab</p>
    </section>

    <section id="other">
      <p>This is the content of the other tab!</p>
    </section>

    <section id="last">
      <p>This is the content of the last tab!</p>
    </section>
  </div>

  <script>
    jQuery(function ($) {
      var switch_to_tab = function (tabid) {
        // This is the idem-potent part
        // This computes what to show and what to hide.
        var $tabs_to_hide = $(".tabs-content > section").not(tabid),
            $tab_to_show = $(".tabs-content > section").filter(tabid);

        $tabs_to_hide.hide();
        $tab_to_show.show();
      };

      var default_tabid = "#default";

      var valid_hash = function (hash) {
        return hash != '' && $(".tabs-content > section").filter(hash).length > 0;
      }

      var switch_to_hash = function (hash) {
        var valid = valid_hash(hash);
        // We don't want to switch to a non valid hash.
        // This allow to have multiple sets of tabs on the same page
        if (valid)
          switch_to_tab(hash);
        return valid;
      }

      // This is the progressive enhancement part
      if (!switch_to_hash(window.location.hash))
        switch_to_hash(default_tabid);

      window.onhashchange = function (data) {
        return switch_to_hash(window.location.hash);
      };

    });
  </script>
</code>

Change your way of thinking and working

In order to write "progressively enhanced idempotent javascript" here's what you should do:

  • Start by writing your application without any javascript.

  • Add your javascript on top of your application.

  • Modify the back-end code as little as possible in order to make your javascript work.

  • When you write your javascript for every variable you use in javascript, you should ask yourself:

    • Am I storing a state here?

    • Is this state computed from the environment, or am I managing a state?