Register to get access to free programming courses with interactive exercises

Application initialization JS: Frontend architecture

The operation of any application can be seen as a life cycle divided into three stages:

  • Initialization
  • Execution
  • Completion

Depending on how these stages are implemented, the code may be easy to test and support, or complex and practically untestable. In this lesson, we'll look at the most important part – the difference between initialization and execution, we'll break down specific examples, and learn how to correctly separate responsibilities.

What is initialization? Before any application can work, all the necessary libraries and frameworks must be set up, linked together, and running. In backend development, it's a lot easier because the frameworks have full control over this process. In the frontend, a lot is left to the developer himself, so there are often situations where the initialization process is either not separated, or it's only partially separated, or otherwise unsuccessfully.

What does initialization include? Everything that needs to be done exactly once so it can be used later in the application:

  • Creating an initial state
  • Connecting WebSockets
  • Setting up i18next
  • Loading and running the framework, if there is one
  • Setting up various libraries: http-clients, working with dates and so on

This list is far from complete, every situation is different and involves different things being initialized.

In practice, the initialization process is organized as follows: a function is created, in which all the necessary setup is done. You can do this in the init.js file for convenience.

// A hypothetical example
// init.js file
import i18next from 'i18next';
import io from 'socket.io-client';

// Initial function
export default async () => {
  // creating an i18next instance
  const i18nextInstance = i18next.createInstance();
  await i18nextInstance.init({
    lng: 'en',
    resources: /* translations */
  });

  const state = {
    /* status description */
  }

  // socket creation
  const socket = new io();
  socket.on(/* setting up WebSockets */);

  const form = document.querySelector('some-form');
  form.addEventListener('submit', (e) => {
   // And here is the logic of the application. It's better to put it somewhere else.
   // state and socket are used somewhere in these handlers
  });
};

This function is run elsewhere, for example in the index.js file, which is the entry point into the application:

import runApp from './init.js';

runApp();

Why is it divided like this? In frontend development, applications are usually global. I.e., there's one application for the entire page, it's loaded exactly once and run exactly once, and it controls everything that happens as if there was nothing else around. But this isn't always the case. For example, widgets can appear on the same page more than once, which means that each widget must be its own thing. I.e., the initialization of such a widget works in its own environment with its own objects and doesn't change anything outside (global objects). Otherwise, there'll be conflicts and widgets will interfere with each other.

On the other hand, when it comes to testing, each test is constructed so that it's independent of other tests, i.e., each test works as if no other tests exist. This behavior requires initializing the application for each test from scratch. Only in this case can you ensure that changes made to the state of the application in one test will not affect the other tests. A prime example is the initialization of i18next. This library exports a global object that can only be initialized once, re-initializing the same object (e.g., by re-running the application in tests) will lead to bugs and is forbidden by the documentation. For this reason, in the example above, each time the application starts, it creates its own instance of i18next, which is then passed to the functions that use it.

// Tests are studied in detail in other courses
// Somewhere in the tests
import runApp from '../src/init.js';

// This function is performed before each test
beforeEach(() => {
  runApp(); // initializing
});

test(/* tests */)
test(/* tests */)

In the example above, it's the function that initializes the application, not the index.js file, where the function is called. If we had imported the index.js file, the initialization would have happened immediately during the import, and in terms of testing it would have been global. In this case, normal testing is almost impossible.


Recommended materials

  1. Code Complete: States Within Modules
  2. Scripts, modules, and libraries

Are there any more questions? Ask them in the Discussion section.

The Hexlet support team or other students will answer you.

For full access to the course you need a professional subscription.

A professional subscription will give you full access to all Hexlet courses, projects and lifetime access to the theory of lessons learned. You can cancel your subscription at any time.

Get access
130
courses
1000
exercises
2000+
hours of theory
3200
tests

Sign up

Programming courses for beginners and experienced developers. Start training for free

  • 130 courses, 2000+ hours of theory
  • 1000 practical tasks in a browser
  • 360 000 students
By sending this form, you agree to our Personal Policy and Service Conditions

Our graduates work in companies:

Bookmate
Health Samurai
Dualboot
ABBYY
Suggested learning programs
profession
Development of front-end components for web applications
10 months
from scratch
Start at any time

Use Hexlet to the fullest extent!

  • Ask questions about the lesson
  • Test your knowledge in quizzes
  • Practice in your browser
  • Track your progress

Sign up or sign in

By sending this form, you agree to our Personal Policy and Service Conditions
Toto Image

Ask questions if you want to discuss a theory or an exercise. Hexlet Support Team and experienced community members can help find answers and solve a problem.