Learning path · Level 6 · Tutorial 4

Split Browser Code into JavaScript Modules

Refactor a small browser interface into connected JavaScript modules and explain how Vite makes local modules and installed packages usable by the browser.

  • Level: Intermediate
  • Duration: 22 min
  • 8 steps
Start tutorial Back to the path
Split Browser Code into JavaScript Modules

What you will go through

  1. Choose the Module Boundaries Identify the calculator’s responsibilities, establish its working single-file baseline, and plan the file boundaries for the refactor. 3 min
  2. Extract Logic with Named Exports Move reusable calculator pricing logic into a separate JavaScript module and connect it to the browser entry module with matching named exports and imports. 4 min
  3. Use Aliases and Default Exports Use an import alias and a default export while preserving the Notebook calculator’s behavior. 3 min
  4. Keep Dependencies Pointing One Way Read the calculator's one-way module graph and keep reusable modules independent from entry-module state. 3 min
  5. Trace Module Evaluation Trace dependency-first evaluation in a small module graph with a reused shared dependency. 3 min
  6. Separate Import Syntax from Vite Resolution Distinguish local file imports from package imports and identify Vite’s development-time role versus browser execution. 3 min
  7. Repair Broken Module Connections Use visible module-loading clues to repair a wrong relative path and an import/export mismatch, then confirm that the calculator still works. 3 min
  8. Verify the Finished Module Refactor Assemble the final Notebook quantity calculator modules, verify its browser behavior, and explain how imports, Vite resolution, and browser execution connect. 3 min

What you will learn

  • Move reusable logic out of a browser entry file and connect the files through explicit imports and exports.
  • Use named exports, import aliases, and default exports with their matching import syntax.
  • Trace a small acyclic module graph and explain module scope and dependency-first evaluation.
  • Distinguish relative file imports from bare package imports and describe Vite's role in resolving them.
  • Correct an import-path or exported-name mismatch and manually verify that the refactored interface still behaves as expected.

Before you start

  • Run a Local Development Server with Vite
  • Manage npm Dependencies and Lockfiles
  • Write Functions with Parameters and Lexical Scope
  • Keep Interface State and Rendering Consistent

Step 1 of 8

Choose the Module Boundaries

Identify the calculator’s responsibilities, establish its working single-file baseline, and plan the file boundaries for the refactor.

Protect behavior and separate responsibilities

Refactor without changing behavior

A refactor reorganizes code without changing what a person can observe in the interface. Before splitting files, protect the calculator’s current behavior:

  • It starts with Notebook, quantity 2, and subtotal $25.00.
  • Increase raises the quantity and subtotal.
  • Decrease lowers them.
  • At quantity 0, Decrease is unavailable.

The goal is not to add features. It is to move each responsibility to a clearer home while preserving these results.

Three responsibilities

Diagram separating product data, reusable price logic, and browser interface coordination into three areas.

The calculator has data, reusable logic, and interface coordination. The refactor gives each area a focused file.

Tip

Choose focused file responsibilities

The target organization is deliberately small:

  • src/product.js will hold the Notebook product record.
  • src/pricing.js will hold price calculation and formatting functions.
  • src/main.js will continue to hold interface state, event listeners, DOM updates, and render().

This separation is about responsibility, not about putting every declaration in its own file. The connection syntax comes next; for now, identify the boundaries.

Start with the HTML entry

Set up the entry document

Create or replace index.html with this complete entry document. It keeps the Vite-compatible module entry script and provides every element that the calculator code needs.

index.html

index.html

html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Notebook quantity calculator</title>
  </head>
  <body>
    <main>
      <h1 id="product-name"></h1>
      <p>Quantity: <span id="quantity"></span></p>
      <p>Subtotal: <span id="subtotal"></span></p>
      <button id="decrease" type="button">Decrease</button>
      <button id="increase" type="button">Increase</button>
    </main>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

Establish the single-file baseline

Run the baseline locally

Place this complete single-file version in src/main.js. It is the behavior baseline to preserve. It intentionally keeps product data, price logic, and DOM coordination together for now.

Use the Vite server you already set up, save both files, and open its reported local URL. Verify the initial values, select Decrease twice to reach zero, confirm that Decrease becomes disabled, then select Increase twice to return to 2.

Single-file calculator baseline

src/main.js

javascript
const product = {
  name: 'Notebook',
  unitPriceCents: 1250
};

const state = {
  quantity: 2
};

const productName = document.querySelector('#product-name');
const quantity = document.querySelector('#quantity');
const subtotal = document.querySelector('#subtotal');
const decrease = document.querySelector('#decrease');
const increase = document.querySelector('#increase');

function calculateSubtotal(unitPriceCents, quantity) {
  return unitPriceCents * quantity;
}

function formatPrice(cents) {
  return '$' + (cents / 100).toFixed(2);
}

function render() {
  productName.textContent = product.name;
  quantity.textContent = state.quantity;
  subtotal.textContent = formatPrice(
    calculateSubtotal(product.unitPriceCents, state.quantity)
  );
  decrease.disabled = state.quantity === 0;
}

decrease.addEventListener('click', function () {
  if (state.quantity > 0) {
    state.quantity -= 1;
    render();
  }
});

increase.addEventListener('click', function () {
  state.quantity += 1;
  render();
});

render();
Expected output
Initial page:
Notebook
Quantity: 2
Subtotal: $25.00
Decrease is enabled.

After selecting Decrease twice:
Notebook
Quantity: 0
Subtotal: $0.00
Decrease is disabled.

After selecting Increase twice:
Notebook
Quantity: 2
Subtotal: $25.00
Decrease is enabled.

Expected browser result after loading the page and during the described button sequence.

Tip

Keep reusable logic independent

Keep prices as integer cents: 1250 represents $12.50. This avoids fractional-money calculations and lets formatting happen only when the value is displayed.

During the refactor, the functions should keep receiving values through parameters. They should not need DOM elements or the interface state object.

Apply the target structure

Plan the file boundaries

Match each calculator responsibility to the file where it belongs after the refactor.

Tap an item and then its matching pair.

Step 2 of 8

Extract Logic with Named Exports

Move reusable calculator pricing logic into a separate JavaScript module and connect it to the browser entry module with matching named exports and imports.

Expose reusable pricing functions

Module boundaries and scope

The calculator keeps the behavior you established, but its reusable price logic moves out of the entry module.

A top-level declaration in a JavaScript module belongs to that module. It does not become a shared global that another file can use automatically. A named export exposes a selected declaration; a matching named import makes that declaration available in the importing module.

For this extraction:

  • src/pricing.js owns reusable subtotal calculation and price formatting.
  • src/main.js remains the entry module and owns state, DOM queries, listeners, and render().

The interface behavior does not change: it still starts at quantity 2 with a subtotal of $25.00.

What moves out of the entry module

A diagram separates entry-module interface coordination from reusable pricing functions through an explicit import connection.

main.js coordinates the interface and imports reusable functions from pricing.js.

Create the pricing module

src/pricing.js

Create src/pricing.js beside src/main.js. Each export makes that function a named export. These functions receive every value they need through parameters, so they do not read the DOM or the entry module's state.

javascript
export function calculateSubtotal(unitPriceCents, quantity) {
  return unitPriceCents * quantity;
}

export function formatPrice(cents) {
  return '$' + (cents / 100).toFixed(2);
}

Expose only the module interface

The exported names are exactly calculateSubtotal and formatPrice. Their parameters keep the module reusable: 1250 and 2 produce 2500, and formatting 2500 produces $25.00.

The entry module will request these names explicitly. It does not gain access to every top-level declaration automatically, and pricing.js does not need to import main.js to reach interface state.

Import the named functions

src/main.js

Replace src/main.js with this complete entry module. The static import declaration is at module top level, before code that calls the imported functions. Braces request named exports, so the requested names must match the names exported by pricing.js.

javascript
import { calculateSubtotal, formatPrice } from './pricing.js';

const product = {
  name: 'Notebook',
  unitPriceCents: 1250
};

const state = {
  quantity: 2
};

const productName = document.querySelector('#product-name');
const quantity = document.querySelector('#quantity');
const subtotal = document.querySelector('#subtotal');
const decrease = document.querySelector('#decrease');
const increase = document.querySelector('#increase');

function render() {
  productName.textContent = product.name;
  quantity.textContent = state.quantity;
  subtotal.textContent = formatPrice(
    calculateSubtotal(product.unitPriceCents, state.quantity)
  );
  decrease.disabled = state.quantity === 0;
}

decrease.addEventListener('click', function () {
  if (state.quantity > 0) {
    state.quantity -= 1;
    render();
  }
});

increase.addEventListener('click', function () {
  state.quantity += 1;
  render();
});

render();
Expected output
Initial render
Notebook
Quantity: 2
Subtotal: $25.00
Decrease: enabled

After selecting Decrease twice
Notebook
Quantity: 0
Subtotal: $0.00
Decrease: disabled

After selecting Increase twice
Notebook
Quantity: 2
Subtotal: $25.00
Decrease: enabled

Expected browser interface states after saving the files and using the controls. This is an interface result, not Console output.

Tip

Connect files through the entry module

Keep the single entry script in index.html:

<script type="module" src="/src/main.js"></script>

Do not add another HTML script element for pricing.js. The relative file specifier ./pricing.js starts from src/main.js, so it resolves to the neighboring src/pricing.js file. Keep the .js extension in this project.

Practice: reconnect the module

Complete and verify the extraction

Complete the named export and named import connection. Then create or save the two complete files from the preceding screens on your computer. With your Vite server running, verify the initial display, decrease twice, and increase twice. Confirm that the helpers are reached through the import rather than as globals.

javascript
// src/pricing.js
export function (unitPriceCents, quantity) {
  return unitPriceCents * quantity;
}

export function (cents) {
  return '$' + (cents / 100).toFixed(2);
}

// src/main.js
import { ,  } from './pricing.js';

Step 3 of 8

Use Aliases and Default Exports

Use an import alias and a default export while preserving the Notebook calculator’s behavior.

Use a named-export alias

Rename only the local import

pricing.js keeps its public function names. An import alias changes only the identifier used inside the importing module.

Here, calculateSubtotal remains the named export in pricing.js, while main.js calls it subtotal. formatPrice keeps the same local name. The braces still mean these are named imports.

An alias is local to the importer

Diagram showing pricing.js exporting calculateSubtotal and formatPrice while main.js imports calculateSubtotal under the local alias subtotal.

The source export name remains calculateSubtotal; only main.js uses the local alias subtotal.

Named exports stay in the source module

src/pricing.js

javascript
export function calculateSubtotal(unitPriceCents, quantity) {
  return unitPriceCents * quantity;
}

export function formatPrice(cents) {
  return '$' + (cents / 100).toFixed(2);
}

Import one named export under an alias

src/main.js

javascript
import {
  calculateSubtotal as subtotal,
  formatPrice
} from './pricing.js';

Use a default import without braces

Move product data with a default export

A module may expose multiple named exports, but it has at most one default export. A default import has no braces, and the importer chooses its local name.

Create src/product.js, move the Notebook record there, and remove that record from src/main.js. The entry module can use the default import beside its aliased named import. Its state, DOM work, event listeners, and render() stay in the entry module.

Export the product as the default

src/product.js

javascript
const product = {
  name: 'Notebook',
  unitPriceCents: 1250
};

export default product;

Finished entry module

src/main.js

javascript
import { calculateSubtotal as subtotal, formatPrice } from './pricing.js';
import product from './product.js';

const state = {
  quantity: 2
};

const productName = document.querySelector('#product-name');
const quantity = document.querySelector('#quantity');
const subtotalElement = document.querySelector('#subtotal');
const decrease = document.querySelector('#decrease');
const increase = document.querySelector('#increase');

function render() {
  const totalCents = subtotal(product.unitPriceCents, state.quantity);

  productName.textContent = product.name;
  quantity.textContent = state.quantity;
  subtotalElement.textContent = formatPrice(totalCents);
  decrease.disabled = state.quantity === 0;
}

decrease.addEventListener('click', function () {
  if (state.quantity > 0) {
    state.quantity -= 1;
    render();
  }
});

increase.addEventListener('click', function () {
  state.quantity += 1;
  render();
});

render();
Expected output
Notebook
Quantity: 2
Subtotal: $25.00
Decrease (enabled)
Increase (enabled)

This is the initial rendered interface after the module loads. The button states change after interaction.

Tip

Apply the extraction and preserve behavior

Save src/product.js and the updated src/main.js while your Vite server is running. Refresh the local page if needed. Confirm that Notebook appears at quantity 2 with $25.00; decrease twice to confirm $0.00 and a disabled Decrease button; then increase twice to confirm $12.50 and $25.00 again.

For comparison only, import notebook from './product.js'; would receive the same default-exported object. The final calculator uses the clearer local name product.

Match imports to exports

Identify the import form

Match each import declaration to the export interface it requires.

Tap an item and then its matching pair.

Apply the final import syntax

Complete the module connections

Complete the import declarations used by the finished entry module.

javascript
import {
  calculateSubtotal as ,
  formatPrice
} from './pricing.js';
import  from './product.js';

Confirm the behavior after refactoring

Verify the extracted product module locally

After completing the imports, save src/product.js and src/main.js and check the calculator in your local Vite page. Report the product name and initial quantity/subtotal. Then report the subtotal after each of two Decrease clicks, whether Decrease is available at the lowest quantity, and the subtotal after each of two Increase clicks. Finally, state which identifier main.js calls for the subtotal calculation and which source export it refers to.

Write at least 180 characters (0/180).

Step 4 of 8

Keep Dependencies Pointing One Way

Read the calculator's one-way module graph and keep reusable modules independent from entry-module state.

Map the calculator dependencies

Read the graph direction

A module graph is a map of files and their import relationships. Each file is a node. Draw each arrow from the importer to the dependency, and label the arrow imports.

For this calculator, main.js is the entry module: it coordinates the DOM and state. It imports the product record and pricing utilities. The reusable modules do not need to know that main.js exists.

Calculator module graph

A graph with arrows labeled imports pointing from main.js to pricing.js and product.js.

Arrows point from the module that imports to the module it needs.

Tip

One-way dependencies

This direction keeps responsibilities clear:

  • main.js coordinates browser interaction.
  • pricing.js provides reusable price calculations and formatting.
  • product.js provides product data.

The graph describes dependencies, not the order in which code runs. At this point, focus on which file needs which other file.

Protect reusable modules

Pass values across the boundary

Keep reusable modules independent of entry-module details. pricing.js receives values through parameters; it does not import main.js to reach state.

That makes the function useful wherever a unit price and quantity are available. It also prevents the pricing module from being tied to DOM coordination.

Reusable pricing function

src/pricing.js

javascript
export function calculateSubtotal(unitPriceCents, quantity) {
  return unitPriceCents * quantity;
}

Warning

Avoid an unnecessary cycle

If main.js imports pricing.js and pricing.js imports main.js, the files form a circular dependency. Avoid creating that connection merely to read entry-module state. Pass the required value as a function argument instead.

Useful boundaries are small and explicit, not maximal: related reusable pricing functions belong together in pricing.js; every declaration does not need its own file.

Check the dependency decision

Spot the risky edge

Which proposed dependency edge would create an unnecessary circular dependency in the calculator?

Pass the needed value

Complete the call in src/main.js so the entry module passes its current quantity to the imported pricing function.

javascript
const totalCents = subtotal(product.unitPriceCents, );

Step 5 of 8

Trace Module Evaluation

Trace dependency-first evaluation in a small module graph with a reused shared dependency.

See evaluation in the module graph

Evaluation is top-level execution

Module evaluation is the execution of a module’s top-level statements. It is different from calling a function that the module exports.

For this small acyclic graph with synchronous module bodies, dependencies evaluate before the importing module’s own top-level body. If two modules import the same resolved module URL during one page load, that shared module is evaluated once and then reused.

A full page reload creates a new evaluation context, so modules evaluate again for that new load. This trace does not cover hot updates.

One graph, one shared module

A four-file module graph where entry imports left and right, both import one shared file, and shared evaluates before left, right, and entry.

Arrows show imports from importer to dependency. The highlighted sequence shows evaluation moving from dependencies back to the entry module; shared.js is one node, not two copies.

Tip

Read the two directions

The direction of an import arrow and the evaluation sequence are related but opposite in this trace: entry.js imports its dependencies, while those dependencies run their top-level code before entry.js does.

Importing a function makes its exported value available. It does not execute the statements inside that function. Those run only when code explicitly calls the function.

Follow the imports through source files

shared.js

shared.js

javascript
console.log('shared');

export function useShared() {
  console.log('function call');
}

left.js

left.js

javascript
import { useShared } from './shared.js';

console.log('left');

export function runLeft() {
  useShared();
}

right.js

right.js

javascript
import { useShared } from './shared.js';

console.log('right');

export function runRight() {
  useShared();
}

entry.js

entry.js

javascript
import { runLeft } from './left.js';
import { runRight } from './right.js';

console.log('entry');

export { runLeft, runRight };

Trace the source

The browser begins with entry.js. Its first import leads to left.js, which first needs shared.js; therefore shared logs before left. Next, right.js is evaluated, but its import resolves to the already evaluated shared.js, so shared does not log again. Finally, the top-level body of entry.js logs.

No file calls runLeft() or runRight() here. If code later called runLeft(), only then would useShared() run and log function call. That later function call is not another evaluation of shared.js.

Check your evaluation prediction

Order the evaluation trace

Arrange the top-level console labels in the order they appear for this graph during one page load.

  1. entry
  2. right
  3. shared
  4. left

Explain reuse and function calls

After ordering the labels, choose the statement that completes the trace.

Step 6 of 8

Separate Import Syntax from Vite Resolution

Distinguish local file imports from package imports and identify Vite’s development-time role versus browser execution.

Recognize local files and package names

Two kinds of specifier

A relative file specifier identifies a project file from the location of the importing module. In src/main.js, ./pricing.js means the neighboring src/pricing.js file. This project writes the .js extension explicitly.

A bare package specifier names a package rather than describing a local route. For example, lodash-es has no ./, ../, or / prefix. A browser cannot find an npm package from that name alone without a tool or configured mapping.

Where imports point

A diagram contrasting a nearby project file with a separate package outside the project folder.

A relative file specifier follows the project structure; a bare package specifier names a package.

Calculator imports use relative file specifiers

src/main.js

javascript
import { calculateSubtotal as subtotal, formatPrice } from './pricing.js';
import product from './product.js';

Run an isolated package import

Run a temporary package demonstration

The Notebook calculator still needs only src/main.js, src/pricing.js, and src/product.js; do not add this package import to its finished code.

For a temporary, runnable demonstration, install lodash-es, create one temporary module, and connect it to the existing entry module. Keep the Vite server running. After saving, open the browser DevTools Console to inspect the logged value.

Install the temporary demonstration package

Terminal, in the project folder

bash
npm install lodash-es
Expected output
added 1 package, and audited 12 packages in 1s

3 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

The package count, audit details, and timing vary by project and npm version.

Import the package by its supported specifier

Create this temporary file: src/isolated-package-demo.js

javascript
import { camelCase } from 'lodash-es';

const label = camelCase('module demo');
console.log(label);
Expected output
moduleDemo

This output appears in the browser DevTools Console after the entry module imports this temporary file.

Connect the temporary module to the entry module

At the very top of src/main.js, add this temporary side-effect import before the calculator imports. Leave the existing calculator imports below it unchanged.

javascript
import './isolated-package-demo.js';
import { calculateSubtotal as subtotal, formatPrice } from './pricing.js';
import product from './product.js';

Separate Vite's work from browser execution

Vite prepares modules; the browser runs them

During development, Vite follows the import graph. It resolves relative project files and installed packages, then serves browser-loadable module references. It can transform application source and package code when needed for development.

That preparation is not your interface running: Vite does not call render(), change calculator state, or respond to button clicks. After Vite serves the modules, the browser evaluates them and executes the calculator's application logic.

Preparation versus execution

A diagram showing Vite resolving project files and an installed package before the browser executes a calculator interface.

Vite resolves, transforms, and serves modules during development; the browser executes the served application modules.

Tip

Clean up the temporary demonstration

After you see moduleDemo in the browser Console, restore the final calculator state:

  1. Remove import './isolated-package-demo.js'; from the top of src/main.js.
  2. Delete src/isolated-package-demo.js.
  3. Run the uninstall command below in the project folder.

This removes the temporary package declaration so lodash-es is not a dependency of the finished calculator. Do not replace the supported package specifier with a manually constructed path inside node_modules.

Remove the temporary package

Terminal, in the project folder

bash
npm uninstall lodash-es
Expected output
removed 1 package, and audited 11 packages in 1s

3 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

The removed-package count, audit details, and timing vary by project and npm version.

Check the complete distinction

Distinguish syntax, resolution, and execution

Complete the classification labels, the illustrative package import, and the two runtime responsibilities.

javascript
import { formatPrice } from './pricing.js'; // 
import {  } from ''; // 

//  resolves packages and transforms modules during development.
// The  executes the imported code and application logic.

Step 7 of 8

Repair Broken Module Connections

Use visible module-loading clues to repair a wrong relative path and an import/export mismatch, then confirm that the calculator still works.

Read the clues before editing

Use the error as a comparison guide

A module-loading message can be worded differently by Vite and by the browser. Do not focus only on its wording. Find three clues:

  1. The importing file: where the failing import appears.
  2. The requested path: the relative file specifier after from.
  3. The requested export: the name or import form the importer asked for.

For a missing module, compare the requested path with the real file location, including spelling, capitalization, and the .js extension. For an export error, open the target module and compare its exported declarations with the import.

Trace the broken connection

Diagram showing an importer, a requested module path, a target file, and an error clue being compared.

Start at the importing file, then compare what it requests with the file and export that actually exist.

A focused repair sequence

Use this short sequence before changing code.

Repair one connection at a time so a later change does not hide the original issue.
ClueCompareThen do
Module not foundRequested path and actual filenameCheck folder, spelling, capitalization, and .js
Export not foundImported name and target declarationUse the declared named export or the default-import form
Error clearsPage behaviorUse the calculator controls to confirm rendering still works

Diagnose import and export mismatches

Match the import to the module interface

An export mismatch is a contract mismatch between two files. A named import must use the name declared by the target module. A default-only export must be imported without braces.

Braces are not decorative: import { product } ... specifically requests a named export called product. It does not request the module's default export. Likewise, a small spelling difference such as formatPrices versus formatPrice is a different requested name.

A default-only product module

Source file: src/product.js

javascript
const product = {
  name: 'Notebook',
  unitPriceCents: 1250
};

export default product;

Two common export mismatches

Compare the request with the declaration in the target file.

Inspect the target module instead of guessing from the importing file.
Target declarationBroken requestRequired repair
`export default product;`Named request using bracesUse a default import with no braces
`export function formatPrice(...)`Requests `formatPrices`Match the declared named-export spelling

Observe one broken connection at a time

Prepare controlled repair cases

These are two independent broken connections. Test only one at a time. For each case, temporarily replace the corresponding valid import in src/main.js with the shown broken import—do not add it beside the valid import. Save, inspect the visible error, then restore that connection before trying the other case.

If your calculator is already correct, this controlled replacement lets you connect an error clue to the source code without leaving the project broken.

Case A: incorrect relative file specifier

Case A — src/main.js

Temporarily replace the existing pricing import with this line. The actual target file is src/pricing.js.

javascript
import {
  calculateSubtotal as subtotal,
  formatPrice
} from './price.js';
Expected output
Failed to resolve import "./price.js" from "src/main.js". Does the file exist?

Representative Vite development-server output. Exact overlay, URL, and browser wording can vary; the important clue is the requested `./price.js` path from `src/main.js`.

Case B: incorrect import form

Case B — src/main.js

Temporarily replace the existing product import with this line. Do not keep the valid default import at the same time. The target module is the default-only src/product.js shown on the previous screen.

javascript
import { product } from './product.js';
Expected output
Uncaught SyntaxError: The requested module '/src/product.js' does not provide an export named 'product'

Representative browser module error after Vite serves the files. Exact filename URLs and wording can vary; the important clue is that `product` was requested as a named export.

Apply the repairs and check the interface

Repair locally, then verify behavior

In your own project, repair both cases from the preceding screen, one connection at a time. Save after each repair and inspect the Vite overlay or browser Console.

Then verify the calculator after the connections are restored: it should start at quantity 2 with a subtotal of $25.00, and the Increase and Decrease controls should update the interface. In your response, identify the two repairs and report whether the error disappeared and interaction resumed. Do not upload files; report what you observed locally.

Write at least 100 characters (0/100).

Step 8 of 8

Verify the Finished Module Refactor

Assemble the final Notebook quantity calculator modules, verify its browser behavior, and explain how imports, Vite resolution, and browser execution connect.

Confirm the finished structure

One interface, three module responsibilities

The refactor is complete when each file has one clear job:

  • product.js provides the Notebook data.
  • pricing.js provides reusable price logic.
  • main.js is the entry module: it owns interface state, DOM updates, and event listeners.

The behavior must remain unchanged. A clean module structure is valuable only if the calculator still starts at quantity 2 with a subtotal of $25.00 and responds correctly to its controls.

The finished dependency direction

Diagram showing main.js importing pricing.js and product.js while main.js coordinates the calculator interface.

Arrows point from the importer to its dependencies. The reusable modules do not import the entry module.

Copy the supporting files

index.html

index.html — the entry document

html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Notebook quantity calculator</title>
  </head>
  <body>
    <main>
      <h1 id="product-name"></h1>
      <p>Quantity: <span id="quantity"></span></p>
      <p>Subtotal: <span id="subtotal"></span></p>
      <button id="decrease" type="button">Decrease</button>
      <button id="increase" type="button">Increase</button>
    </main>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

src/pricing.js

src/pricing.js — reusable calculations with named exports

javascript
export function calculateSubtotal(unitPriceCents, quantity) {
  return unitPriceCents * quantity;
}

export function formatPrice(cents) {
  return '$' + (cents / 100).toFixed(2);
}

src/product.js

src/product.js — one default export for the product record

javascript
const product = {
  name: 'Notebook',
  unitPriceCents: 1250
};

export default product;

Run the calculator and check its behavior

src/main.js

src/main.js — the entry module imports the public interfaces and coordinates the DOM. Save all four files, keep your Vite development server running, and open its local URL.

javascript
import { calculateSubtotal as subtotal, formatPrice } from './pricing.js';
import product from './product.js';

const state = {
  quantity: 2
};

const productName = document.querySelector('#product-name');
const quantity = document.querySelector('#quantity');
const subtotalElement = document.querySelector('#subtotal');
const decrease = document.querySelector('#decrease');
const increase = document.querySelector('#increase');

function render() {
  const totalCents = subtotal(product.unitPriceCents, state.quantity);

  productName.textContent = product.name;
  quantity.textContent = state.quantity;
  subtotalElement.textContent = formatPrice(totalCents);
  decrease.disabled = state.quantity === 0;
}

decrease.addEventListener('click', function () {
  if (state.quantity > 0) {
    state.quantity -= 1;
    render();
  }
});

increase.addEventListener('click', function () {
  state.quantity += 1;
  render();
});

render();
Expected output
Initial page state:
Notebook
Quantity: 2
Subtotal: $25.00
Decrease: enabled

After two Decrease clicks:
Notebook
Quantity: 0
Subtotal: $0.00
Decrease: disabled

These are visible browser states, not Console output. Use the checklist below for the complete click-by-click sequence.

Expected zero-quantity state

Screenshot-style calculator interface at quantity zero with a disabled Decrease button and an active Increase button.

After the second Decrease click, confirm the visible state is quantity 0, subtotal $0.00, and a disabled Decrease control.

Loading is not behavior verification

Vite resolving the imports and a page loading without an error proves that the module connections can be served. It does not prove that the refactor preserved behavior. The browser evaluates the served modules, calls render(), and runs the click listeners; verify those visible results deliberately.

Manual verification checklist

Use this sequence in the browser after saving the files.

Expected visible results for the unchanged calculator behavior.
ActionQuantitySubtotalDecrease control
Initial page load2$25.00Enabled
Click Decrease once1$12.50Enabled
Click Decrease again0$0.00Disabled
Click Increase once1$12.50Enabled
Click Increase again2$25.00Enabled

Summary

What the completed refactor demonstrates

  • main.js imports product data and reusable pricing functions, then coordinates state and the DOM.
  • pricing.js stays independent by receiving values through parameters instead of importing main.js.
  • Vite resolution prepares browser-loadable module references; the browser evaluates modules and handles clicks.
  • Manual interaction checks confirm behavior that successful module loading alone cannot confirm.

Apply and verify the finished refactor

Reconnect the entry module

Complete the two imports used by the final entry module.

javascript
import { calculateSubtotal as , formatPrice } from './pricing.js';
import  from './product.js';

Final verification report

After completing the imports, save the files and run the calculator through the verification checklist. In 5–8 sentences, report the quantity and subtotal at initial load, after each Decrease click, and after each Increase click. State the Decrease control’s state at quantity zero. Then explain why pricing.js does not import main.js, and how Vite resolution differs from the browser executing the calculator.

Write at least 300 characters (0/300).

Module refactor complete

Congratulations! You completed: Split Browser Code into JavaScript Modules

You assembled a browser interface from explicit modules, verified that its behavior survived the refactor, and connected module resolution to browser execution.

Download the App now to have access to + 5000 free courses, exercises, certificates and lots of content without paying anything!

  • 100% free online courses from start to finish

    Thousands of online courses in video, ebooks and audiobooks.

  • More than 60 thousand free exercises

    To test your knowledge during online courses

  • Valid free Digital Certificate with QR Code

    Generated directly from your cell phone's photo gallery and sent to your email

Cursa app on the ebook screen, the video course screen and the course exercises screen, plus the course completion certificate