Salesforce · · 14 min read

LWC Modules and JavaScript Static Resources

How to create reusable JavaScript modules for LWC and how to import third-party JavaScript libraries as static resources into your Lightning Web Components.

Part 89: LWC Modules and JavaScript Static Resources

As your LWC projects grow, you start noticing the same patterns appearing across multiple components. Maybe you are formatting currency values in three different places. Maybe you have date manipulation logic duplicated across a parent and child component. Maybe you wrote a really solid validation function and now you need it somewhere else entirely.

Copy-pasting that code is the fastest way to create a maintenance nightmare. Change one copy and forget the others, and suddenly your components behave inconsistently. This is exactly the problem that JavaScript modules solve — and Lightning Web Components support them natively.

But modules only cover code that you write yourself. What about third-party libraries? What if you need a charting library, a PDF generator, or a specialized date-handling library that does not exist on the Salesforce platform? For that, you need static resources. Salesforce lets you upload JavaScript libraries as static resources and load them into your LWC at runtime.

If you have been following the series, you have already seen pieces of both patterns. In Part 82, we created shared CSS modules that multiple components could import — the same structural idea applies to JavaScript modules. And in Part 85, we built a reusable error handling module that centralized toast notifications and error formatting. This post formalizes both approaches and adds the static resource technique to your toolkit.

This post covers:

  1. How to Create a Reusable LWC Module — Building shared JavaScript utilities that any component can import.
  2. How to Create a JS Static Resource and Import It to an LWC — Uploading third-party libraries and loading them at runtime.
  3. Section Notes — Key takeaways and patterns to remember.

Let’s get into it.


How to Create a Reusable LWC Module

A reusable LWC module is just a standard JavaScript file that lives inside its own LWC bundle and exports functions, constants, or classes that other components can import. There is no HTML file, no CSS file, and no component class — just plain JavaScript with exports.

The Basic Structure

Let’s say you have formatting logic that you use across several components. You want a utility that formats currency, percentages, and dates consistently. Here is how you create it.

First, create a new LWC bundle. In your project’s force-app/main/default/lwc/ directory, create a folder called utils. Inside that folder, create a file called utils.js:

// utils/utils.js

/**
 * Formats a number as USD currency.
 * @param {Number} value - The numeric value to format.
 * @returns {String} The formatted currency string.
 */
const formatCurrency = (value) => {
    if (value === null || value === undefined) return '$0.00';
    return new Intl.NumberFormat('en-US', {
        style: 'currency',
        currency: 'USD'
    }).format(value);
};

/**
 * Formats a number as a percentage with two decimal places.
 * @param {Number} value - The decimal value (e.g., 0.75 for 75%).
 * @returns {String} The formatted percentage string.
 */
const formatPercentage = (value) => {
    if (value === null || value === undefined) return '0.00%';
    return new Intl.NumberFormat('en-US', {
        style: 'percent',
        minimumFractionDigits: 2,
        maximumFractionDigits: 2
    }).format(value);
};

/**
 * Formats an ISO date string into a readable format.
 * @param {String} isoString - An ISO date string.
 * @returns {String} The formatted date string.
 */
const formatDate = (isoString) => {
    if (!isoString) return '';
    const date = new Date(isoString);
    return new Intl.DateTimeFormat('en-US', {
        year: 'numeric',
        month: 'long',
        day: 'numeric'
    }).format(date);
};

export { formatCurrency, formatPercentage, formatDate };

You also need a minimal metadata file. Create utils.js-meta.xml in the same folder:

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>59.0</apiVersion>
    <isExposed>false</isExposed>
</LightningComponentBundle>

Notice that isExposed is set to false. This module is not a component that users drag onto pages. It is a utility that other components import programmatically. It does not need to be exposed in the Lightning App Builder.

Importing the Module

Now any LWC in your project can import from this module using the c/utils namespace:

// accountCard/accountCard.js
import { LightningElement, api } from 'lwc';
import { formatCurrency, formatDate } from 'c/utils';

export default class AccountCard extends LightningElement {
    @api account;

    get formattedRevenue() {
        return formatCurrency(this.account?.AnnualRevenue);
    }

    get formattedCreatedDate() {
        return formatDate(this.account?.CreatedDate);
    }
}

That is the entire pattern. You write functions in a module, export them, and import them wherever you need them. The c/ prefix tells the LWC framework to look for a component bundle in your default namespace.

Exporting Constants and Configuration

Modules are not limited to functions. You can export constants, configuration objects, or lookup maps that keep your components in sync:

// constants/constants.js

export const RECORD_PAGE_SIZE = 10;
export const MAX_FILE_SIZE_MB = 5;
export const ALLOWED_FILE_TYPES = ['.pdf', '.png', '.jpg', '.docx'];

export const STATUS_OPTIONS = [
    { label: 'New', value: 'New' },
    { label: 'In Progress', value: 'In_Progress' },
    { label: 'Completed', value: 'Completed' },
    { label: 'Cancelled', value: 'Cancelled' }
];

export const FIELD_LABELS = {
    accountName: 'Account Name',
    industry: 'Industry',
    annualRevenue: 'Annual Revenue',
    numberOfEmployees: 'Number of Employees'
};

Now when a business requirement changes — say the page size goes from 10 to 25 — you change it in one place and every component that imports RECORD_PAGE_SIZE picks up the new value after redeployment.

Exporting Classes

You can also export classes from a module. This is useful when you need structured data objects or utility classes with internal state. Remember the reusable error handler from Part 85? That followed this exact pattern — a class exported from a module that any component could instantiate or call:

// errorHandler/errorHandler.js
import { ShowToastEvent } from 'lightning/platformShowToastEvent';

export default class ErrorHandler {
    static showError(component, title, error) {
        let message = 'An unknown error occurred.';

        if (typeof error === 'string') {
            message = error;
        } else if (error?.body?.message) {
            message = error.body.message;
        } else if (error?.message) {
            message = error.message;
        }

        component.dispatchEvent(
            new ShowToastEvent({
                title: title,
                message: message,
                variant: 'error'
            })
        );
    }

    static showSuccess(component, title, message) {
        component.dispatchEvent(
            new ShowToastEvent({
                title: title,
                message: message,
                variant: 'success'
            })
        );
    }
}

Any component can then import and use it:

import ErrorHandler from 'c/errorHandler';

// Inside a method:
ErrorHandler.showError(this, 'Save Failed', error);
ErrorHandler.showSuccess(this, 'Record Saved', 'The account was updated successfully.');

Using a default export means you import the class directly without curly braces. Named exports use curly braces. Pick whichever pattern makes sense for your module — named exports are better when a module has multiple things to offer, default exports are cleaner when a module represents a single concept.

Organizing Multiple Modules

As your project grows, you might end up with several utility modules. A reasonable structure looks like this:

force-app/main/default/lwc/
    utils/
        utils.js
        utils.js-meta.xml
    constants/
        constants.js
        constants.js-meta.xml
    errorHandler/
        errorHandler.js
        errorHandler.js-meta.xml
    dateUtils/
        dateUtils.js
        dateUtils.js-meta.xml
    validationUtils/
        validationUtils.js
        validationUtils.js-meta.xml

Each module is its own LWC bundle with its own metadata file. Each has isExposed set to false. Each exports whatever functions, constants, or classes it is responsible for. And each can be imported by any component using the c/ prefix.

One thing to keep in mind: LWC modules cannot import from each other in circular patterns. If utils imports from dateUtils and dateUtils imports from utils, the framework will throw an error. Keep your dependency graph clean and one-directional.


How to Create a JS Static Resource and Import It to an LWC

Modules work great for code you write yourself. But what about third-party JavaScript libraries? Maybe you need Lodash for deep cloning and collection manipulation, or Moment.js for complex date parsing, or Chart.js for rendering visualizations. These libraries are not part of the LWC framework, and you cannot install them with npm in a Salesforce project the way you would in a Node.js application.

The solution is static resources. You upload the library file to Salesforce as a static resource, and then you load it into your component at runtime using the loadScript function from the lightning/platformResourceLoader module.

Step 1: Download the Library

First, get the library file. Most JavaScript libraries offer a standalone build — a single .js file that you can download and use without a module bundler. For this example, we will use Lodash.

Go to the Lodash website or CDN and download the minified build (lodash.min.js). You want the UMD or standalone build, not the ES module version, because loadScript loads the file as a traditional script tag — it executes in the global scope.

Step 2: Upload as a Static Resource

In Salesforce Setup, navigate to Static Resources and click New. Give it a name like lodash, set the cache control to Public, and upload the lodash.min.js file.

Alternatively, if you are working in a SFDX project, create the static resource in your project structure:

force-app/main/default/staticresources/
    lodash.js
    lodash.resource-meta.xml

The metadata file looks like this:

<?xml version="1.0" encoding="UTF-8"?>
<StaticResource xmlns="http://soap.sforce.com/2006/04/metadata">
    <cacheControl>Public</cacheControl>
    <contentType>application/javascript</contentType>
</StaticResource>

Deploy the static resource to your org with sf project deploy start.

Step 3: Import and Load the Library in Your Component

Now you can use the library in any LWC. The key imports are loadScript from lightning/platformResourceLoader and a reference to your static resource:

// dataProcessor/dataProcessor.js
import { LightningElement, track } from 'lwc';
import { loadScript } from 'lightning/platformResourceLoader';
import LODASH from '@salesforce/resourceUrl/lodash';

export default class DataProcessor extends LightningElement {
    @track processedData = [];
    lodashLoaded = false;

    renderedCallback() {
        if (this.lodashLoaded) {
            return;
        }
        this.lodashLoaded = true;

        loadScript(this, LODASH)
            .then(() => {
                console.log('Lodash loaded successfully');
                this.processData();
            })
            .catch((error) => {
                console.error('Error loading Lodash:', error);
            });
    }

    processData() {
        // Lodash is now available on the global window object as "_"
        const rawData = [
            { name: 'Acme', industry: 'Technology', revenue: 500000 },
            { name: 'Globex', industry: 'Manufacturing', revenue: 750000 },
            { name: 'Initech', industry: 'Technology', revenue: 300000 }
        ];

        // Use Lodash to group by industry
        const grouped = _.groupBy(rawData, 'industry');
        console.log('Grouped:', grouped);

        // Use Lodash to sort by revenue descending
        this.processedData = _.orderBy(rawData, ['revenue'], ['desc']);
    }
}

Let’s break down what is happening here.

The import LODASH from '@salesforce/resourceUrl/lodash' line does not actually load the library. It gives you a URL reference to the static resource. The actual loading happens when you call loadScript(this, LODASH).

We use renderedCallback to trigger the load because we need the component to be in the DOM before loading external scripts. The lodashLoaded flag ensures we only load the library once — renderedCallback fires every time the component re-renders, so without the flag you would load the script repeatedly.

loadScript returns a promise. Once it resolves, the library is available on the global window object. For Lodash, that means the _ variable is now accessible. For other libraries, it depends on how the library registers itself — check the library’s documentation.

Loading Multiple Libraries

If you need multiple libraries, you can load them in parallel using Promise.all:

import { LightningElement } from 'lwc';
import { loadScript, loadStyle } from 'lightning/platformResourceLoader';
import CHARTJS from '@salesforce/resourceUrl/chartjs';
import CHARTJS_CSS from '@salesforce/resourceUrl/chartjsCSS';
import LODASH from '@salesforce/resourceUrl/lodash';

export default class Dashboard extends LightningElement {
    librariesLoaded = false;

    renderedCallback() {
        if (this.librariesLoaded) {
            return;
        }
        this.librariesLoaded = true;

        Promise.all([
            loadScript(this, CHARTJS),
            loadScript(this, LODASH),
            loadStyle(this, CHARTJS_CSS)
        ])
            .then(() => {
                console.log('All libraries loaded');
                this.initializeChart();
            })
            .catch((error) => {
                console.error('Error loading libraries:', error);
            });
    }

    initializeChart() {
        // Both Chart.js and Lodash are now available
        const canvas = this.template.querySelector('canvas');
        const ctx = canvas.getContext('2d');

        new Chart(ctx, {
            type: 'bar',
            data: {
                labels: ['Q1', 'Q2', 'Q3', 'Q4'],
                datasets: [{
                    label: 'Revenue',
                    data: [120000, 190000, 170000, 250000],
                    backgroundColor: 'rgba(54, 162, 235, 0.5)'
                }]
            }
        });
    }
}

Notice the loadStyle import alongside loadScript. If your library includes a CSS file, you upload that as a separate static resource and load it with loadStyle. Both functions work the same way — they return promises and load the resource into the component’s context.

Loading Libraries from ZIP Archives

For larger libraries that include multiple files (JavaScript, CSS, images, fonts), you can upload a ZIP file as the static resource. When you reference files inside the ZIP, you append the path after the resource URL:

import CHART_BUNDLE from '@salesforce/resourceUrl/chartBundle';

// In renderedCallback:
Promise.all([
    loadScript(this, CHART_BUNDLE + '/chartBundle/chart.min.js'),
    loadStyle(this, CHART_BUNDLE + '/chartBundle/chart.min.css')
])

The path after the resource URL must match the folder structure inside the ZIP. If the ZIP contains a folder called chartBundle with chart.min.js inside it, then CHART_BUNDLE + '/chartBundle/chart.min.js' is the correct path.

Important Considerations for Static Resources

There are a few things to watch for when using static resources with LWC.

Locker Service and Lightning Web Security. Salesforce enforces security through Locker Service (older) or Lightning Web Security (newer). These security layers restrict what third-party scripts can do. Libraries that directly manipulate the DOM outside of the component’s shadow DOM may not work. Libraries that rely on global browser APIs in unexpected ways might behave differently. Always test your library in the Salesforce context after loading it — do not assume it will work just because it works in a plain HTML page.

File size limits. Static resources have a maximum file size of 5 MB per resource, and an org-wide limit of 250 MB total. Use minified versions of libraries to keep file sizes small. If a library exceeds 5 MB even when minified, you probably need to evaluate whether you really need the entire library or if a lighter alternative exists.

Caching. When you set the cache control to Public, Salesforce caches the static resource on the CDN. This makes subsequent loads fast, but it also means that if you update the library, users might still get the cached version for a while. Changing the static resource name or clearing the cache can help during development.

No ES modules. The loadScript function loads files as classic scripts, not ES modules. Libraries that only ship as ES modules (using export and import at the top level) will not work with loadScript. You need the UMD, IIFE, or standalone build of the library.

Async loading. Because loadScript is asynchronous, you cannot use the library immediately when the component initializes. Any code that depends on the library must run inside the .then() callback or after an await if you use async/await syntax. Structure your component so that it shows a loading state until the library is ready.


Section Notes

Here are the key points from this post:

  • LWC modules are JavaScript files inside their own LWC bundles that export functions, constants, or classes. Other components import them using the c/ namespace prefix. This is the same structural pattern we used for shared CSS in Part 82, just applied to JavaScript logic.

  • Every module needs a .js-meta.xml file with isExposed set to false. The module is not a visual component — it is a utility that gets imported programmatically.

  • Use named exports (export { func1, func2 }) when a module has multiple things to offer. Use default exports (export default class MyClass) when a module represents a single concept. Import syntax differs: named exports use curly braces, default exports do not.

  • For third-party libraries, upload the library file as a static resource in Salesforce. Use the UMD or standalone build, not the ES module version.

  • Load static resources at runtime using loadScript from lightning/platformResourceLoader. Always load in renderedCallback with a flag to prevent re-loading on every render cycle.

  • loadScript returns a promise. The library becomes available on the global window object once the promise resolves. All code that depends on the library must run after the promise resolves.

  • Use Promise.all to load multiple libraries in parallel. Use loadStyle for CSS files that accompany a library.

  • Be aware of Locker Service / Lightning Web Security restrictions. Not every JavaScript library works inside the Salesforce security context. Always test in your org.

  • Static resources have a 5 MB per file limit and a 250 MB org-wide limit. Use minified builds and evaluate whether you need the full library or a lighter alternative.

  • Keep module dependencies one-directional. Circular imports between LWC modules will cause errors.

In the next post, we will continue building on these patterns as we explore more advanced LWC features. The combination of reusable modules for your own code and static resources for third-party libraries gives you a solid foundation for building complex, maintainable applications on the Salesforce platform.