Lightning:container to the rescue

If you have been developing Lightning Components for a while it is probable that you have come up against the feared Locker Service. If you are aware of Summer 18 Locker Service changes, you will know that, luckily, Locker Service restrictions have been relaxed. However, there are still some libraries that are considered unsafe and that cannot run within this context. For those cases, lightning:container comes to the rescue!

Lightning:container is an alternative mechanism that allows you to run third party code in an isolated iframe, so that the libraries are sandboxed in their own DOM. Libraries not supported by Locker Service can run there, however the communication with the native Lightning Component is limited, as it uses postMessage.

In this post I want to create a sample react app (bear in mind react is currently supported by Locker Service, however I wanted to give it a try and I decided to use it as my third party code). This react app is a tic tac toe game that I have created following this great tutorial. Let’s explore the three mechanisms that are available today to communicate the react app with a Lightning Component in detail.

Creating and transpiling the react app

First of all I invested some time following the tutorial and creating the react tic tac toe. Something to highlight here is that I had to use webpack and babel to transpile not supported features into ES5. I followed this tutorial.

Once you have created and transpiled the app, you will need to compress it into a zip file and upload it as a static resource to the org. Follow the exact instructions in the README of my repo to know how to do it.

Creating the Lightning Component

Our Lightning Component is going to be very simple. We just want to embed the react app in the left hand side area, and we will leave an area to be filled by the Lightning Component in the right hand side in a later step.

Captura de pantalla 2018-07-16 a las 18.00.50.png

This will be the code for the component (simplified):

<aura:component implements="force:appHostable" access="global" controller="TestReactContainerController">

    <aura:attribute type="List" name="moves" default="[]" />

    <lightning:card title="Lightning Container + React POC">
    <div>
        <lightning:formattedText value="React App area" class="slds-text-heading_small" />
        <lightning:container aura:id="reactApp" src="{!$Resource.react + '/index.html'}" onmessage="{!c.handleMessage}" onerror="{!c.handleError}"/>
    </div>
    <div>
        <lightning:formattedText value="Lightning Component area" class="slds-text-heading_small" />
        <lightning:button variant="brand" label="Reset game" title="Reset game" onclick="{! c.handleResetGame }" />
        <p><lightning:formattedText value="Received messages:" /></p>
        <aura:iteration items="{!v.moves}" var="move">
            <p><lightning:formattedText value="{!move}" /></p>
        </aura:iteration>
    </div>
    </lightning:card>
</aura:component>

Sending messages from the react app to the Lightning Component

Let’s explore the first mechanism that we have available. To try this out, we will send a message to the Lightning Container with each move that the players perform in the react tic tac toe and let’s show those messages in the Lightning Component area. For that, each time that there is movement on the board, we will invoke the next function from the react app:

import LCC from 'lightning-container';

handleClick(i) {
    ...
    // Read move and update game. Squares var will contain the info of the move.
    ...

    LCC.sendMessage({name:'move', value: squares});
}

In the Lightning Component controller, we are listening to this message defining a handler on the lightning:container definition (onmessage attribute). You can see it in the Lightning Component code that I showed above. This would be the code for the Javascript handler, that print the movements in the Lightning Component area (I am not doing anything fancy, just printing the received message):

({
    handleMessage: function (component, event, helper) {

        var payload = event.getParams().payload;
        var name = payload.name;
        if (name === "move") {
            var value = payload.value;
            var moves = component.get('v.moves');
            moves.push(JSON.stringify(value));
            component.set('v.moves', moves);
        }
    }
})

Sending messages from the Lightning Component to the react app

The second mechanism that we have, allows us to send messages the other way around. What we will do to test it is to add a button in the Lightning Component that allows me to reset the game,  sending a message to the react tic tac toe.

You can check the button definition in the Lightning Component code. This will be the Javascript function that sends the message to the react app:

({
    handleResetGame: function (component, event, helper) {
        var msg = {
            name: "action",
            value: "reset-game"
        };

        component.find("reactApp").message(msg);
        component.set('v.moves', []);
    }
})

In the react app, we will listen to that message defining a handler in the next way:

import LCC from 'lightning-container';

LCC.addMessageHandler(function(message) {
    // Start over!
    element.jumpTo(0);
});

Calling Apex directly from the react app

Finally, there is a third mechanism that we can use, to call Apex directly from the react app. To explore that, each time a Player wins, we will call an Apex method that updates the score for that player and returns the total number of points that the player has accumulated.

For that, I created a Player__c sObject, and two player records, X and O, for simplicity. First of all we need to expose a method in the Lightning Container Apex controller that allows me to perform that logic. This method must be annotated with @RemoteAction:

global class TestReactContainerController {

    @RemoteAction
    global static Decimal setWinner(String winner) {
        Player__c player = [Select Points__c from Player__c where Name = :winner LIMIT 1];
        player.Points__c += 1;
        update player;
        
        return player.Points__c;
    }
}

Then, we can call Apex directly from the react app in this way:

import LCC from 'lightning-container';

...
if (winner) {
    LCC.callApex("TestReactContainerController.setWinner",
                            winner,
                            this.handleSetWinnerResponse,
                            {escape: true});
}
...

handleSetWinnerResponse(result, event) {
    if (event.status) {
        this.setState({
            points: result,
            apexCalled: true
        });
    } else if (event.type === "exception") {
        console.log(event.message + " : " + event.where);
    }
}

Notice that we have defined a callback for the Apex controller response, as we want to show in the react app area the total points that the player has won, which have been retrieved from the database using the remote action.

One requisite for this to work is to register the Apex controller in a manifest.json file as part of the static resource that contains the react app.

Final result

Here you have the final result of the app that we have built to explore the three communication mechanisms described above:

jul-16-2018 18-42-55.gif

Conclusions

Lightning:container is helpful in those cases in which Locker Service is not supported, but you will have to take into account some caveats of this component. Most of them are documented in the Salesforce help:

  • There can be performance and scrolling issues associated with the use of iframes. This increments with the complexity of the app.
  • The component isn’t designed for the multi-page model, and it doesn’t integrate with browser navigation history.
  • It is not possible to detect the height of the screen an render the iframe in full screen, a fixed height must be set.
  • Can’t be used in Visualforce pages, Community Builder, or in external apps through Lightning Out.
  • Only plain JS objects serialized can be interchanged between the 3rd party app and the Lightning Component. This means we cannot pass large amount of data in an effective way.
  • Development is hard, as we have to upload static resource each time something changes in the JS.
  • External JS libraries cannot be loaded from the static resource at the moment.
  • Only ES5 is supported, so it’s needed to use webpack or similar to adapt not supported JS features.
  • Window.postMessage() messages are broadcast to all listeners and it is listener’s responsibility to filter out messages that are not addressed to it. Which means that we need to take care of privacy issues.

Want to know more? I recommend you to read this interesting post that compares Locker Service with lightning:container and to check also this post that contains some code examples for different lightning:container use cases.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s