LearnFrenzy
Sign in Take a free test

Classes and Modules · Lesson 12 of 25

ES Modules in LWC: Import, Export and Shared Code

6 min read Updated Nov 8, 2026 Saurabh Samir

Copy-pasting the same date formatter into ten components works – until the format changes. LWC is built on standard ES modules, so the cure is simple: put shared code in its own module and import it everywhere. Salesforce uses the same mechanism to give you Apex methods, custom labels, field references and user information.

In this lesson

  • Named and default exports
  • A shared utility component
  • The @salesforce scoped modules
  • Module-level state shared by every instance

Named and default exports

// --- a module can export many named things… ---
export const DEFAULT_PAGE_SIZE = 25;
export function initials(name) {
    return name.split(' ').map((part) => part[0]).join('').toUpperCase();
}

// --- …and at most one default ---
export default function formatPhone(raw) {
    const digits = String(raw).replace(/\D/g, '');
    return digits.length === 10 ? `${digits.slice(0, 5)} ${digits.slice(5)}` : raw;
}

// another module would import them like this:
// import formatPhone, { initials, DEFAULT_PAGE_SIZE as PAGE_SIZE } from 'c/formatUtils';
console.log(initials('Asha Verma'), formatPhone('(98765) 43210'), DEFAULT_PAGE_SIZE);

Output

AV 98765 43210 25
  • Named exports are imported with braces and the exact name (rename with as). Prefer them for utilities – editors auto-import them and typos fail at build time.
  • The default export is imported without braces under any name. Every component's class is its module's default export.

A shared utility component

In LWC, a shared module is simply a component folder with a JavaScript file and no template. Here formatUtils lives in lwc/formatUtils/formatUtils.js and is imported as c/formatUtils:

Project layout

  • force-app/main/default/lwc
    • formatUtils/ – formatUtils.js, formatUtils.js-meta.xml (isExposed false)
    • contactBadge/ – contactBadge.html, contactBadge.js (imports c/formatUtils)
// formatUtils.js
const currencyFormatter = new Intl.NumberFormat('en-IN', { style: 'currency', currency: 'INR', maximumFractionDigits: 0 });

export function initials(name = '') {
    return name.split(' ').filter(Boolean).map((part) => part[0]).join('').slice(0, 2).toUpperCase();
}

export function formatCurrency(amount) {
    return amount == null ? '—' : currencyFormatter.format(amount);
}

export function timeAgo(isoDate, now = Date.now()) {
    const minutes = Math.round((now - new Date(isoDate).getTime()) / 60000);
    if (minutes < 60) return `${minutes} min ago`;
    const hours = Math.round(minutes / 60);
    return hours < 24 ? `${hours} h ago` : `${Math.round(hours / 24)} d ago`;
}
// contactBadge.js
import { LightningElement, api } from 'lwc';
import { initials, formatCurrency, timeAgo } from 'c/formatUtils';

const NOW = Date.parse('2026-11-08T12:00:00Z');      // fixed so the example is repeatable

export default class ContactBadge extends LightningElement {
    @api contact = {};

    get avatar() {
        return initials(this.contact.Name);
    }

    get lifetimeValue() {
        return formatCurrency(this.contact.LifetimeValue__c);
    }

    get lastSeen() {
        return timeAgo(this.contact.LastActivity__c, NOW);
    }
}
<!-- contactBadge.html -->
<template>
    <p>({avatar}) {contact.Name}</p>
    <p>Value: {lifetimeValue} · active {lastSeen}</p>
</template>

Result

(RK) Ravi Kumar
Value: ₹12,50,000 · active 3 h ago

Notice that the formatter is created once at module level instead of inside the function. Creating Intl formatters is relatively expensive, and module code runs only once no matter how many components use it.

The @salesforce modules

Salesforce exposes platform resources as importable modules. They're resolved when your component is compiled, so references are checked and dependencies tracked (you can't delete a field a component imports):

import getAccounts from '@salesforce/apex/AccountController.getAccounts';      // an Apex method
import saveLabel from '@salesforce/label/c.Save_Button';                      // a custom label (translatable)
import ACCOUNT_OBJECT from '@salesforce/schema/Account';                       // object reference
import NAME_FIELD from '@salesforce/schema/Account.Name';                       // field reference
import USER_ID from '@salesforce/user/Id';                                      // current user's Id
import LOCALE from '@salesforce/i18n/locale';                                   // e.g. "en-IN"
import CURRENCY from '@salesforce/i18n/currency';                               // user's currency code
import hasExportPermission from '@salesforce/customPermission/Export_Reports';  // true / undefined
import LOGO from '@salesforce/resourceUrl/companyLogo';                         // static resource URL
UseInstead ofWhy
@salesforce/schema field importsthe string 'Account.Name'the platform knows the dependency; renames are caught
@salesforce/labelhard-coded English texttranslations and admin edits without a deploy
@salesforce/i18nassuming 'en-US' and USDnumbers and dates in the user's own format
@salesforce/customPermissionchecking profile namespermission-based UI that admins control

Module-level state is shared

A module is evaluated once per page. Variables declared at module level – outside the class – are shared by every instance of every component that imports the module. That's handy for caches and dangerous for per-component state:

// --- labelCache.js (a shared module) ---
const cache = new Map();                 // one Map for the whole page
let lookups = 0;

function getLabel(key) {
    lookups++;
    if (!cache.has(key)) cache.set(key, key.replaceAll('_', ' ').toLowerCase());
    return cache.get(key);
}

// --- two different components importing it ---
const cardA = { title: getLabel('OPEN_CASES') };
const cardB = { title: getLabel('OPEN_CASES'), sub: getLabel('CLOSED_CASES') };

console.log(cardA.title, '|', cardB.title, '|', cardB.sub);
console.log(`lookups: ${lookups}, cached entries: ${cache.size}`);

Output

open cases | open cases | closed cases
lookups: 3, cached entries: 2

Never keep per-record state at module level

If let selectedId sits outside the class, two copies of the component on the same page share it – select a row in one and the other changes too. Per-instance state belongs in class fields. Module scope is for constants, formatters and deliberate page-wide caches.

Practice

Try it yourself

  1. Five components each define the same reduceErrors function. How would you refactor?

    Show answer

    Create a template-less component lwc/ldsUtils/ldsUtils.js that does export function reduceErrors(errors) { … }, then import { reduceErrors } from 'c/ldsUtils'; in each component and delete the copies.

  2. Why is import NAME_FIELD from '@salesforce/schema/Account.Name' better than the string 'Name' in getRecord's fields?

    Show answer

    The platform validates the reference and records the dependency, so the field can't be deleted or renamed out from under the component and typos fail at deploy time.

Quick check

How many default exports can a module have?

One.

What makes a component folder a utility module?

It has a JavaScript file that exports functions and no HTML template.

How often does module-level code run?

Once per page, shared by all importers.

Summary

  • Prefer named exports for utilities; a component's class is the default export.
  • Share logic through template-less components imported as c/name.
  • Use @salesforce modules for Apex, labels, schema, user, i18n and permissions.
  • Module scope is shared page-wide – use it for constants and caches only.

Finished this lesson?

Mark it complete to track your progress, then carry on with the next lesson.

Next lesson

Search LearnFrenzy

Esc

Quick links