Coping with Stateful Code

Justin Meyer by Justin Meyer

Coping with Stateful Code

Justin Meyer Learn why stateful code make stability challenging and learn some strategies for coping with stateful code.

posted in CanJS ,stable-and-innovative on September 14, 2017 by Justin Meyer

In this article we will:

  • Learn why stateful packages challenge stability
  • See an example of a stateful package
  • Identify CanJS’s stateful packages
  • Provide strategies that minimize the problems with stateful packages

With the elimination of side effects, it becomes possible to use multiple versions of the same package within the same application. Ideally, you should be able to use components made with can-component@4.X along side components made with can-component@3.X. This means you wouldn’t have to rewrite working code to use a new major release!

Unfortunately, there are some packages where using multiple versions is impossible. These are stateful packages. For example, can-view-callbacks is a stateful package used to register custom elements and attributes in CanJS. Its code looks similar to the following:
 

// can-view-callbacks@3
var tags = {};

module.exports ={ 
    tag: function(tag, callback){ 
        if(tag){
            tags[tag] = callback;
        } else{
            return tags[tag];
        }
    }
});

A stateful module contains its own state (tags in can-view-callbacks case) and allows outside code to mutate that state. Let’s see an example of how multiple versions of a stateful package could be so much trouble.

Imagine wanting to use two versions of can-component in an application. old-thing.js uses can-component@3.X:

// old-thing.js 
var Component = require("can-component@3"); 
var view = require("./old-thing.stache");  
Component.extend({
     tag: "old-thing",
     ViewModel: {},
     view: view
});

new-thing.js uses can-component@4.X:

// new-thing.js 
import {register} from "can-component@4"; 
import view from "./new-thing.curly"; 
import define from "can-define";

@define
class NewThing {  } 

Component.register("new-thing", NewThing, view);

But if can-component@3.X MUST use can-view-callbacks@3.X and can-component@4.X MUST use can-view-callbacks@4.X, there will be two custom element registries and make it impossible to use both types of components in the same template. Stateful packages must be treated with care!

CanJS’s stateful packages

CanJS has the following stateful modules: 

Module Purpose
can-cid Uniquely labels objects.
can-observation Registers reading an observable value.
can-view-callbacks Registers custom elements and attributes.
can-namespace Registers `can` namespace, prevents duplicate stateful packages.
can-symbol Register integration behaviors with CanJS

Stateful solutions

There are a few ways to mitigate the problems with stateful modules:

1. Shift the statefulness to the developer.

One option, is to avoid stateful modules altogether and make the user create the state and pass it around to other functionality that need it. For example, we could eliminate can-view-callbacks as follows:

First, make all components export their constructor function:

// my-component.js
module.exports = Component.extend({ ... });

Then, every template must import their components:

<!-- app.stache -->
<can-import from="./my-component" as="MyComponent"/>
<MyComponent/>

This isn’t a viable solution for many other packages because it would create too large a burden on developers with little concrete stability gains. Fortunately, there's other things we can do to help.

2. Minimize the statefulness and harden APIs.

Stateful packages should expose the state with the most minimal and simple API possible. You can always create other packages that interface with the stateful API. For example, we could just directly export the tags data in can-view-callbacks like:

// can-view-callbacks 
module.exports = {};

Other modules could add more user friendly APIs around this shared state.

3. Let people know when they’ve loaded two versions of the same package.

We use can-namespace to prevent loading duplicate packages in a sneaky way. The can-namespace package simply exports an empty object like:

// can-namespace@1.0.0 
module.exports = {};

We should never have to release a new version of can-namespace, but every stateful package imports it and makes sure there’s only one of itself as follows:

// can-cid@1.0.0 
var namespace = require("can-namespace");

if (namespace.cid) { 
     throw new Error("You can't have two versions of can-cid!”); 
} else { 
	 module.exports = namespace.cid = cid; 
}

If we make changes to the stateful modules, we can at least ensure the user knows if they are getting multiple.

Conclusions

Stateful code stinks, but minimizing the scope of its impact has helped CanJS progress much faster in the past year than anytime before, without having to make breaking changes.  In the next section we will see how a tiny bit of well-defined statefulness with can-symbol allows CanJS to tightly integrate with other libraries.

Create better web applications. We’ll help. Let’s work together.