How to use Apex natively with Svelte, Vue, and Preact within LWC

Leveraging existing code is often the fastest way to bring your experience online for your end users. At Salesforce, we strive to ensure that interoperability makes this achievable. This blog is going to show you how to leverage an existing investment you may have made in another framework, and integrate more seamlessly into the Salesforce Experience.

Frameworks working together

Interoperability is an important aspect of every framework. While there are solutions like lightning:container that allow you to embed other frameworks within Lightning Experience, they often don’t feel native. Here we’ll show you a new technique, on how you can embed Svelte, Vue, or Preact apps natively with Lightning Web Components (LWC). Additionally, we’ll cover how to use props and events for bi-directional communication, and even directly run Apex from these apps.

It’s important to understand that mixing UI frameworks does come with a bit of overhead and runtime costs, so it’s important to weigh the overall cost/benefit of leveraging other frameworks to achieve the code reuse.

Everything is an ES6 module

If you developed with LWC before, you likely came across the documentation about sharing JavaScript code between components. Or you read the definition of a JavaScript file within LWC. It is all documented, but eventually not so obvious.

JavaScript files in Lightning web components are ES6 modules.

Repeat it: Everything is an ES6 module. And I recommend you repeat it multiple times to really internalize it, as this is one of the big (overlooked) elements of LWC. It means that you can use almost any ES6 module in LWC. And this is what we’ll use to embed third party frameworks.

Implementation example

To showcase this, I built the implementations for using three different frameworks natively using ES6. I’m not going to explain how I did it for all these frameworks in this post, but I’m going to tell you the most important bits that you have to know when using this technique. All the details can be found in code on GitHub.

In the animation below, an input is being typed into a field within an LWC. The inputs’ value is then passed to a Vue component. After that, a click on an account list item in the Vue component sends the account Id using a CustomEvent to the Lightning Web Component.

In this use-case it’s important to keep a few things in mind:

  • The component works this way in LWC Local Development or in a Salesforce org.
  • No static resources, no iFrame, no lightning:container is used – the component is directly part of the Lightning Web Components DOM.
  • For bi-directional communication, props are passed down to the Vue component, and the Vue component sends data back up using standard DOM events.
  • The data list within the Vue component shows Account data from the org – and the Vue component executes the Apex call to fetch that data. Natively. 🤯
  • All this works the same for Preact, Svelte, and potentially for any other framework.

Now that you’re all excited about using “any” framework natively within Salesforce, keep in mind that things may come at a cost. Running a framework within another framework needs some additional consideration. This approach will always load the framework that you are using, and potentially add runtime cost.

This approach will not work for everything. 128 KB of compiled app/component code are the maximum on the Salesforce platform for Lightning Web Components, and this includes ES6 modules included in the compiled app/component. If you need to use bigger components/apps from another framework you should consider lightning:container. We’ve published a previous blog post about that.

Now, why and where should you use this new approach? Well, there are many cases where everything boils down to sustainable code. You may have existing code, written in another framework like Svelte, and you want to avoid re-building this for usage in Salesforce. Or you find a cool, third-party component, and would like to use it as is.

Or maybe you’re like me, and just like to do some nerdy stuff on Salesforce. In the next sections, you’ll see different examples using different frameworks.

Bundle all the things

The foundation for including any third party framework is bundling. On a high level, bundling is a process that takes your existing code base and resources, and packages them up to generate a new output. During this process, many different build steps may occur, like compiling SASS to CSS, JavaScript module resolution, transpilation, resource minification, and much more. The most popular bundling solutions these days are Webpack, Rollup, or Parcel, and all of them provide similar functionality.

As Rollup provides a slick configuration, we’re using this to bundle up an application for their usage within LWC. For example, this is the Rollup configuration for packaging up the Preact app:

import babel from "rollup-plugin-babel";
import commonjs from "@rollup/plugin-commonjs";
import resolve from "@rollup/plugin-node-resolve";

export default {
    input: "src/preact/main.js",
    treeshake: true,
    output: {
        name: "App",
        format: "esm",
        file: "force-app/main/default/lwc/preact/preact.js"
    },
    external: [/@salesforce/, /lightning\//],
    plugins: [babel(), resolve(), commonjs()]
};

Code (or should I say “Configuration”) highlights:

  • Line 6: Here you specify the main entry point JavaScript file of the third party app. From here on, Rollup will resolve anything and package the app.
  • Line 10 : This is where the magic happens – by specifying “esm” as the value for the format, Rollup creates the output as an ES6 module! 🚀
  • Line 11: Because we don’t want to copy and paste, we tell Rollup to place its output directly into the LWCs folder of the DX project.

Some other things happen in the above configuration, which will differ from framework to framework. I suggest you read more about Rollup in the official documentation. And then you should dig into the different Rollup configurations on GitHub.

Next we look into what you’ll have to do to embed the ES6 modules into LWC.

Embedding in Lightning Web Components

Before we look at the actual LWC code, lets inspect the main.js file of our Svelte app (I took this example as it’s the shortest one):

import App from "./App.svelte";

function createApp(target, props) {
    return new App({
        target,
        props
    });
}

export default createApp;

Here we have a simple ES6 module that returns as its default export, a new Svelte app. And to that Svelte app, we’re passing two values – target and props. target represents the DOM element that the app should attach to, and props should be a JavaScript object that holds the public properties. Remember, this is the entry point for our Svelte app, and Rollup will generate a new ES6 module for us in force-app/main/default/lwc/svelte/svelte.js.

To use these ES6 modules we will need an LWC that then consumes the generated ES6 module. These two abbreviated snippets show the relevant parts:

<div class="svelte"
     lwc:dom="manual"
     onsendaccount={handleSendAccount}>
</div>

In the markup of the consuming component you will need a div that has lwc:dom=“manual” set. That way you tell LWC to ignore that part of the DOM, and you have full control over it. Notice also that there is a declarative event listener attached for the sendaccount event. Using lwc:dom=“manual” has some performance cost since the framework is observing DOM changes to preserve synthetic Shadow DOM semantics.

import { LightningElement } from "lwc";
import createSvelteApp from "c/svelte";

export default class ContainerSvelte extends LightningElement {
  // Class property to hold the instance of the framework
  svelteApp;

  ...

  renderedCallback() {
    if (this._isRendered) return;

    // Svelte all the things, where we call `createApp` from `src/svelte/main.js`
    this.svelteApp = createSvelteApp(
      this.template.querySelector("div.svelte"),
      { title: this.titleValue }
    );

    this._isRendered = true;
  }

  ...
}

Code highlights:

  • Line 2 : We import the ES6 module (read: the full Svelte app) that we built using Rollup just as we would consume any other ES6 module.
  • Lines 14-16: This calls the default exported function from the apps’ main.js file. As parameters, we pass the DOM element that the app should attach itself to, as well as a data object that holds the (to be set) properties for that app.

That’s mostly it. Really. Now, it is noteworthy that you have to do things a bit different based on the framework. For example, for Preact and Svelte you can use the via lwc:dom=“manual” attributed div directly, while for Vue you need to create a new div.

Now that we have the foundation sorted, let’s take a look at how to establish communication between LWC and an embedded app.

Props down, events up

One of the huge benefits of LWC is its strong adherence to web standards. Props down, events up – that’s the way parent-child communication happens. This is not about how LWC works, it’s really about unfolding how the third party framework works. And also to dispel the myth that Lightning Locker would prevent any kind of communication between LWC and such a framework.

To stick with our Svelte app, this is the code to pass down data from a keyup event in LWC, setting a public property of the Svelte app after it has been attached to the DOM.

handleKeyUp(event) {
    this.svelteApp.$set({
        title: event.target.value
    });
}

As we have a handle to the Svelte app with this.svelteApp we can just call $set, and pass new data down. Svelte’s reactivity system takes care of updating its DOM for us (the same applies to the other frameworks).

Now, if we want to pass data from Svelte to our LWC, we just send a CustomEvent:

// Events
function sendAccount(accountId) {
    const evt = new CustomEvent("sendaccount", {
        detail: { accountId },
        bubbles: true,
        composed: true
    });
    el.dispatchEvent(evt);
}

This should look familiar to you, as this is the way events are dispatched on the web in general (and in LWC).

What these snippets do not show are the different implementation details for each framework. Not every framework has a convenience method like Svelte with $set for updating props. And it’s not always as simple to identify from where and how you should dispatch the event. The good news: you can find everything well documented in the examples. Be aware that there is always going to be an impedance miss-match in terms of lifecycle or APIs, so if you run into corner cases, they will be tricky to resolve. This is when you have to weigh the benefits of re-using such existing code vs. building directly new with LWC.

Now, one last thing to add in order to complete our demo scenario we reviewed earlier. How to call Apex (and eventually the UI API) from any of these frameworks.

Calling Apex and others

Guess what an Apex method within LWC is? Yes, it’s an ES6 module. So let’s use it as such within Svelte:

<script>
    import { onMount, createEventDispatcher } from "svelte";
    import getAccounts from "@salesforce/apex/AccountController.getAccounts";

    ...

    onMount(() => {
        getAccounts()
            .then(result => (accounts = result))
            .catch(error => console.log(error));
    });

    ...
</script>

Double-Boom! That’s it. It is not different to use than imperative Apex within LWC, as you just work with an ES6 module. You see the reason for the repetition pattern now, right?

When we look at the generated code, you’ll see that the @salesforce/* dependencies are untouched. It’s really just yet another module that the LWC compiler can ingest.

import getAccounts from '@salesforce/apex/AccountController.getAccounts';

function noop() { }
function add_location(element, file, line, column, char) {
    element.__svelte_meta = {
        loc: { file, line, column, char }
    };
}

The magic happens as the Lightning framework executes all the data operations for you based on the imports. And it does not matter where you have them, as they all run within the Lightning framework context in your LWC. It’s as simple as that. And it’s not possible with lightning:container, as that runs your code from a hosted JavaScript file within an iFrame.

During bundling, Rollup will determine that these ES6 imports don’t exist in the framework, and will consider them as unresolved external dependency. This usually gets logged to the console during bundling, but we disable that specifically in the Rollup configuration.

If you ask: “Can you use everything?” Likely not. That’s something that I leave to you to play with. What I can tell you already is that you won’t be able to use any @wire implementation direct within a third party framework. Not yet, at least. What you can do is use @wire in the parent LWC, and pass down its data as props.

Next steps

This blog post showed you how you can use any(?) framework natively within Salesforce, and how you can communicate with LWC and another framework using props down and events up. You even learned that everything is an ES6 module, and how to leverage this knowledge for accessing Salesforce data.

Check out the GitHub repo to dig into the different framework implementation examples, and how to leverage your newly gained knowledge to add better interoperability to your Salesforce implementations and third party framework usage.

And again, the best way to develop is using only one framework, like using LWC on Salesforce directly, as you’ll get the benefits of a framework that has been built for the platform. Plus, in my next blog post you’ll learn how to use (nearly) any npm package with your Lightning Web Components – on Salesforce!

About the author

René Winkelmeyer works as Architect, Developer Relations, at Salesforce. He focuses on enterprise integrations, JavaScript, node, and all the other cool stuff that you can do with the Salesforce Platform. You can follow him on Twitter @muenzpraeger or on GitHub @muenzpraeger.