DEV Community

Tomek Buszewski
Tomek Buszewski

Posted on

Module pattern in JavaScript

A module is a construct somewhat similar to a singleton class. It has only one instance and exposes its members, but it doesn’t have any kind of internal state.

Defining a module

Module is created as an IIFE (immediately invoked function expression) with a function inside:

const SomeModule = (function() {})();

Everything within the body of said function is bound to that module and can be seen by each other. Modules emulates „public” and „private” methods by creating mentioned earlier scope and exposing only those things that are declared.

Private methods or functions are members of given entity than can be seen only within said entity. Public ones can be accessed from the outside of given entity.

Let us try and create a module with a private function inside.

const Formatter = (function() {
  const log = (message) => console.log(`[${Date.now()}] Logger: ${message}`);
})();

As you can see, there is a simple log function that will log received message. How to execute it? Formatter.log?

Formatter.log("Hello");

Can you guess what it produces? Uncaught TypeError: Cannot read property 'log' of undefined. Why is that? Because our module doesn’t return anything, so it is actually undefined, even though the code inside will execute.

const Formatter = (function() {
  console.log("Start");
  const log = (message) => console.log(`[${Date.now()}] Logger: ${message}`);
})();

This will log Start, because this function has been fired, and as you know, functions doesn’t have to always return something.

So, now we know that accessing a module is actually accessing whatever it returns.

The log function can be treated as a private one. It can be accessed from within the module and other functions inside can execute it. Let’s try!

const Formatter = (function() {
  const log = (message) => console.log(`[${Date.now()}] Logger: ${message}`);

  const makeUppercase = (text) => {
    log("Making uppercase");
    return text.toUpperCase();
  };
})();

Hey, wait a minute, pal! That’s another function within the module that I can’t access!

Exposing a module

Yes, this is another function that isn’t accessible to us. But, knowing what we’ve learned earlier about accessing the module, we can easily solve this! You already know what to do? Exactly, return this function! But, do not return a single function (although it is possible), return an object with it!

const Formatter = (function() {
  const log = (message) => console.log(`[${Date.now()}] Logger: ${message}`);

  const makeUppercase = (text) => {
    log("Making uppercase");
    return text.toUpperCase();
  };  

  return {
    makeUppercase,
  }
})();

Now, we can use the makeUppercase function as we normally would:

console.log(Formatter.makeUppercase("tomek"));

What’s the result?

> Start
> [1551191285526] Logger: Making uppercase
> TOMEK

Modules can house not only functions, but arrays, objects and primitives as well.

const Formatter = (function() {
  let timesRun = 0;

  const log = (message) => console.log(`[${Date.now()}] Logger: ${message}`);
  const setTimesRun = () => { 
    log("Setting times run");
    ++timesRun;
  }

  const makeUppercase = (text) => {
    log("Making uppercase");
    setTimesRun();
    return text.toUpperCase();
  };

  return {
    makeUppercase,
    timesRun,
  }
})();

Let’s execute it:

console.log(Formatter.makeUppercase("tomek"));
console.log(Formatter.timesRun);

As expected, 0 is shown. But note that this can be overwritten from outside.

Formatter.timesRun = 10;
console.log(Formatter.timesRun);

Now console logs 10. This shows that everything publicly exposed can be changed from the outside. This is one of the biggest module pattern drawbacks.

Reference types works differently. Here, you can define it and it will be populated as you go.

const Formatter = (function() {
  const log = (message) => console.log(`[${Date.now()}] Logger: ${message}`);
  const timesRun = [];

  const makeUppercase = (text) => {
    log("Making uppercase");
    timesRun.push(null);
    return text.toUpperCase();
  };

  return {
    makeUppercase,
    timesRun,
  }
})();

console.log(Formatter.makeUppercase("tomek"));
console.log(Formatter.makeUppercase("tomek"));
console.log(Formatter.makeUppercase("tomek"));
console.log(Formatter.timesRun.length);

It will log 3, after saying my name three times in uppercase.

Declaring module dependencies

I like to treat modules as closed entities. Meaning, they reside within themselves and nothing more is needed for them to exist. But sometimes you may want to work with, for example, DOM or window global object.

To achieve that, module may have dependencies. Let’s try to write a function that will write a message to our requested HTML element.

const Formatter = (function() {
  const log = (message) => console.log(`[${Date.now()}] Logger: ${message}`);

  const makeUppercase = (text) => {
    log("Making uppercase");
    return text.toUpperCase();
  };

  const writeToDOM = (selector, message) => {
    document.querySelector(selector).innerHTML = message;
  }

  return {
    makeUppercase,
    writeToDOM,
  }
})();

Formatter.writeToDOM("#target", "Hi there");

It works out of the box (assuming that we have an element with id target in our DOM). Sounds great, but document is available only when the DOM is accessible. Running the code on a server would produce an error. So, how to make sure that we’re good to go?

One of the options is to check whether document exists.

const writeToDOM = (selector, message) => {
  if (!!document && "querySelector" in document) {
    document.querySelector(selector).innerHTML = message;
  }
}

And this pretty much takes care of everything, but I don’t like it. Now the module really depends on something from the outside. It’s „I will go only if my friend will go too” scenario. It has to be like this?

No, of course not.

We can declare our module’s dependencies and inject them as we go.

const Formatter = (function(doc) {
  const log = (message) => console.log(`[${Date.now()}] Logger: ${message}`);

  const makeUppercase = (text) => {
    log("Making uppercase");
    return text.toUpperCase();
  };

  const writeToDOM = (selector, message) => {
    if (!!doc && "querySelector" in doc) {
      doc.querySelector(selector).innerHTML = message;
    }
  }

  return {
    makeUppercase,
    writeToDOM,
  }
})(document);

Let’s follow it step by step. At the top, there is an argument to our function. Then, it is used in writeToDOM method, instead our document. In the end, right in the last line, we are adding document. Why? Those are the arguments our module will be invoked with. Why I changed the argument name in the module? I don’t like to shadow variables.

This is a great opportunity for testing, of course. Now, rather than relying on whether our testing tools have DOM simulator or something similar, we can insert a mock. But we need to insert it during our definition, not later. This is fairly simple, you just need to write a mock and place is as a „spare”:

const documentMock = (() => ({
  querySelector: (selector) => ({
    innerHTML: null,
  }),
}))();

const Formatter = (function(doc) {
  const log = (message) => console.log(`[${Date.now()}] Logger: ${message}`);

  const makeUppercase = (text) => {
    log("Making uppercase");
    return text.toUpperCase();
  };

  const writeToDOM = (selector, message) => {
    doc.querySelector(selector).innerHTML = message;
  }

  return {
    makeUppercase,
    writeToDOM,
  }
})(document || documentMock);

I even removed the check inside makeUppercase, because it’s not needed anymore.

Module pattern is a very common one, and – as you can see – very good at that. I often try to write modules first, then – if needed – classes.

Top comments (19)

Collapse
 
bayazz profile image
Bayazz

You said with Formatter.timesRun = 10; We access timesRun variable and change it. But actually when we call makeUpperCase method we see that timesRun is still 0 and it doesn't change. Could you please explain this strange behavior?

Collapse
 
tomekbuszewski profile image
Tomek Buszewski

Hi Bayazz!

Can you reproduce the problem or show the code? I did run the one from example on codesandbox and it seems fine.

Collapse
 
bayazz profile image
Bayazz • Edited

Hi Tomek,
thanks for the reply.

If we add console.log(timesRun);in the setTimesRun method right after ++timesRun; we can see in the console that timesRun is not 10. But with Formatter.timesRun it's 10.

codesandbox.io/s/lingering-sound-g...

Collapse
 
bayazz profile image
Bayazz

Any ideas? Im still struggling to understand it.
Thanks

Thread Thread
 
tomekbuszewski profile image
Tomek Buszewski • Edited

Hi, sorry for replying late, I've seen this before (I guess I didn't try it), but I'll try to come up with something in the upcoming days ;)

-- edit

In the meantime (and actually all the time) you should have explicit functions modifying such values. Take a look here: codesandbox.io/s/busy-field-2yf01

Thread Thread
 
bayazz profile image
Bayazz

I actually wanted to understand why it's like this.
Anyway thanks for the reply:)

Thread Thread
 
hilver profile image
Paweł Pęczkowski • Edited

When you do Formatter.timesRun = 10; you don't assign this value to the variable, but to the property of Formatter. You don't have directly access to variables in module pattern. Look here.

I hope this is more clear now :)

Thread Thread
 
tomekbuszewski profile image
Tomek Buszewski

Hey, I've totally forgot about this. Well, this is what you are getting when you are old :D

Thread Thread
 
bayazz profile image
Bayazz

Yeah it happens:)
Thanks, it's clear now:)

Thread Thread
 
hrexandro profile image
Hrexandro

I'm still confused, why is the property different from the variable? Where is it stored if the variable is not used and why does incrementing the property does not increment the variable?

Thread Thread
 
muewwy profile image
Muewwy

I'm only learning, so take this all with a grain of salt, but I was confused here to and this is what helped increment my understand of this error forward: The variable itself isn't returned because "return { timesRun }" is actually object property: value shorthand for "return { timesRun: timesRun }", so it is only creating the property to be stored in Formatter and setting the initial value, which is only set the first time Formatter is created. If you want to get a counter accessible from outside the function you can either:
a) create a function that gets and returns the current private value, because a function declared within the same scope of the private value will retain access to it when called from outside
b) make the property Formatter.timesRun a getter for timesRun, also declared within the same scope, or
c) use the property itself as the counter within the function.

Collapse
 
chengmo profile image
Cheng_Mo_

I think this behaviour because it's called Immediately Invoked Function Expression ... As a result it returned the property Immediately when it was equal to 0 before any calling for the public methods and any changes wouldn't affect the returned property unless you change it directly by Formatter.timesRun = ## .... That's my thought about this matter correct me if I'm wrong, I'm just a fellow learner just like you not an expert or something... Hope it helps!

Collapse
 
m0xai profile image
m0xai

A little thing:
Before running the code in Exposing a Module, you deleted the line " console.log("Start");" i mean, we can't see this console.log just before invoking the function. But in output you added it in first line.

Collapse
 
taf profile image
taff

great explanation. I don't know if this is obsolete anymore but I'm learning one at a time. thank you.

Collapse
 
hareom284 profile image
Harry • Edited

Thank man this article

Collapse
 
lizsafina profile image
LizSafina

Hey, great article. Thank you!
Just one thing:
I even removed the check inside makeUppercase, because it’s not needed anymore
Did you mean "you removed the check inside 'writeToDom'?"

Collapse
 
guico33 profile image
guico33

Now we have actual modules with esm or even commonjs, what's the point of creating modules using an IIFE?
To me it sounds like a pattern which should disappear in favor of more modern practices.

Collapse
 
tomekbuszewski profile image
Tomek Buszewski

Hi Guico,
You are right about the new modular approaches, I just might be a little late to the party with this article ;)

Nevertheless, I still find IIFE often in codebases I work with while doing consulting or audit stuff. And I don't think those are for refactor (even though it would be quite painless), as such modules are valid and fully functional parts of an application.

Collapse
 
axterix profile image
Axterix

Thanks for your post, clear a nice explained.... was very usefull and informative.