1

I'm in a bit of a position as we've done all we can with LWC and we now need to get very complex components that load data that we can't have in Salesforce displayed within the Salesforce interface.

I've been asked to look into if there's a way for the engineering team to develop apps once as standalone pages but also have the code run within Salesforce.

The easy solution is obviously iframes, but I was wondering if there's any way to achieve it without iframes.

What I've tried

Importing the bundle

I assumed I'd be able to build my React app, upload it as a Static Resource and then do some magic with LWC to load its scripts.

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.tsx";

// my thought here is that this would be added to the window
// object and I'd be able to call it from salesforce
window.initReactApp = function () {
  createRoot(document.getElementById("salesforce-root-test")!).render(
    <StrictMode>
      <App />
    </StrictMode>
  );
};

Then in the LWC component:

import { LightningElement } from 'lwc';
import { loadScript } from 'lightning/platformResourceLoader';
// my built bundle
import myScript from '@salesforce/resourceUrl/my_js_file';

export default class MyComponent extends LightningElement {
    connectedCallback() {
        loadScript(this, myScript)
            .then(() => {
                console.log('Script loaded successfully');
                this.initializeReactApp();
            })
            .catch((error) => {
                console.error('Error loading script', error);
            });
    }

    initializeReactApp() {
        // Assuming your React app exposes a global function to initialize itself
        if (window.initReactApp) {
            window.initReactApp();
        } else {
            console.error('React app initialization function not found');
        }
    }
}

However window.initReactApp is undefined even if the script is loaded correctly.

Using lightning:container

I found this page, and it seemed promising until I realised it's just a different name for an iframe.

I'm also confused by how to get it to work. I uploaded a zip of my whole app but when doing:

<aura:component>
    <lightning:container src="{!$Resource.myApp + '/index.html'}" />
</aura:component>

I get told Salesforce is unable to find index.html.

Is this a fool's errand or is it actually possible?

7
  • Have you considered writing LWCs that run on LWR, and so off platform, or using external objects to access the non-Salesforce data as if it is hosted in Salesforce?
    – Phil W
    Commented Aug 16 at 15:13
  • @PhilW our team is heavy on non-salesforce devs and the product team is quite insistent on relying on Salesforce less as we scale so writing more Salesforce components is kind of a no-go at this point Commented Aug 16 at 15:17
  • Use of external objects could be a quick win to avoid the need for custom UI creation at all (depending on your needs). If you are planning to migrate away from Salesforce, develop your new UI entirely off platform for later.
    – Phil W
    Commented Aug 16 at 15:31
  • We can't unfortunately, because we're still dependant on it at the moment. So we want to add new stuff that we can also use in Salesforce... Commented Aug 16 at 15:35
  • Perhaps adopting web components directly would help as LWCs are somewhat similar in structure? Obviously there are differences in the way everything is pulled together, but you might then find it easier to port from one context to another when you need to? Turning to your question - in terms of the problem with script loading, that's covered in other SFSE Q&As already. You may find this of interest.
    – Phil W
    Commented Aug 16 at 15:45

2 Answers 2

3

It is technically possible to run React, Angular, etc, assuming it is scoped to a single DOM element.

First, make a container to host the app:

<div lwc:ref="container" lwc:dom="manual"></div>

Then, you initialize the app:

loadScript(this, MY_SCRIPT).then(() => {
  window.initializeApp(this.refs.container);
});

Where initializeApp is scoped appropriate from the Static Resource (not from another LWC component).

Note that this assumes that (a) the initialization function can accept a DOM element rather than a query selector/ID, and (b) it does not try to modify the global DOM or violate any other LS/LWS rules.

That said, it's almost always easier to use lightning:container, and then use the exposed functions to control the size of the components and communicate with the rest of LWC/Salesforce.

2
  • I can't seem to get a function added to the window object. Even by loading a single script with nothing but a window function assignment it's still undefined. Is there an example of what a script loaded from static resources should look like? Commented Aug 17 at 9:32
  • @LeonardoPetrucci I wrote up a very simple example that calls an alert dialog, and includes a Tab so you can see it immediately. Here's the repo. To deploy, simply clone the repo, then use sf project deploy start.
    – sfdcfox
    Commented Aug 17 at 20:20
0

The answers above ended up pointing me towards the right solution, so thank you both for commenting!

Here's what I ended up doing.

Using the standard Vite React template make a couple of changes.

In your main.tsx add the following code:

function initReactApp(element: HTMLElement) {
  createRoot(element!).render(<App />);
}

Then go to your tsconfig.json and change:

"target": "ES2020",

to

"target": "ES5",

Changes for your LWC component

import { LightningElement } from 'lwc';
import { loadScript } from 'lightning/platformResourceLoader';
// this is just a js file, no idea how to upload bundles yet
import myApp from '@salesforce/resourceUrl/my_js_file';

export default class MyComponent extends LightningElement {
    renderedCallback() {
        loadScript(this, myApp)
            .then(() => {
                console.log('Script loaded successfully');
                this.initializeReactApp();
            })
            .catch((error) => {
                console.error('Error loading script', error);
            });
    }

    initializeReactApp() {
        if (window.initReactApp) {
            // salesforce blocks "getElementById"
            const root = this.template.querySelector('.root');
            console.log('root', root);
            window.initReactApp(root);
        } else {
            console.error('React app initialization function not found');
        }
    }
}

Then your template will be something like this:

<template>
    <div lwc:dom="manual" class="root"></div>
</template>

That's it, your React App should render within your LWC.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .