+ Start a Discussion
Daniel Vu 6Daniel Vu 6 

Showing a loading indicator while calling Apex in Salesforce LWC

What is the best way to show a loading indicator while retrieving data from Apex in a Lightning Web Component?

I have this approach:
import { LightningElement, api } from "lwc";
import shouldShowCard from "@salesforce/apex/ApexClass.shouldShowCard";

/**
 * Card component that is conditionally shown based on Apex.
 */
export default class ConditionalCard extends LightningElement {
    @api recordId;

    @api isDone = false;

    @api shouldShow = false;

    connectedCallback() {
        shouldShowCard({ id: this.recordId })
            .then(result => {
                this.shouldShow = result;
            })
            .finally(() => {
                this.isDone = true;
            });
    }
}



And this HTML
<template>
  <template if:false={isDone}>
    <div>Loading...</div>
  </template>
  <template if:true={shouldShow>
    <div>Card</div>
  </template>
</template>

Now, this works but I'm using the LWC ESLint rules, and when I do this, I get an error/warning "no-api-reassignment" because I'm assigning the api properties in my connectedCallback.
https://github.com/salesforce/eslint-plugin-lwc/blob/master/docs/rules/no-api-reassignments.md

Which seems reasonable, though it very similar to the pattern that the Salesforce Lightning Spinner shows.
https://developer.salesforce.com/docs/component-library/bundle/lightning-spinner/documentation

So I'm just looking for advice on the best way to handle this or if I should just disable the ESLint rule. Other things to consider are how to test this stuff, the reactivity with the API decorator has made things pretty easy on my end but I don't want to continue if I'm not using the best approach.
Best Answer chosen by Daniel Vu 6
Daniel Vu 6Daniel Vu 6

Thanks. I ended up not requiring a loading indicator, but I think using the wire attached to a method would allow it to happen. Not my exact code but an adaption that I think might work if people want to do something like this.

import { LightningElement, api } from "lwc";
import shouldShowCard from "@salesforce/apex/ApexClass.shouldShowCard";

/**
 * Card component that is conditionally shown based on Apex.
 */
export default class ConditionalCard extends LightningElement {
    @api recordId;

    isDone = false;

    shouldShow = false;

    error = null;

    @wire(shouldShowCard, { id: '$recordId' })
   retrieveResults({ error, data }) {
        // No information has returned, still waiting on Apex results
        if (!error && !data) {
          this.isDone = false;
        } else {
          if (error) {
            this.error = error;
          } else  if (data) {
            this.shouldShow = data;
          }

          this.isDone = true;
        }
    }
}
Tests would end up looking like this
import { registerApexTestWireAdapter } from "@salesforce/sfdx-lwc-jest";
import Card from "c/card";
import shouldShowCard from "@salesforce/apex/ApexClass.shouldShowCard";

const shouldShowCardTestAdapter = registerApexTestWireAdapter(shouldShowCard);

describe("Card", () => {
    it("is displayed if Apex says so", async () => {
        // render is some utility I wrote, just a wrapper around createElement from LWC
        const element = await render(Card);

        await shouldShowCardTestAdapter.emit(true);

        return Promise.resolve().then(() => {
            const instructions = element.shadowRoot.querySelectorAll('.unique-html-class-for-card');
            expect(instructions.length).toBe(1);
        });
    });

    it("is not displayed if Apex does not say so", async () => {
        const element = await render(Card);

        await shouldShowCardTestAdapter.emit(false);

        return Promise.resolve().then(() => {
            const instructions = element.shadowRoot.querySelectorAll('.unique-html-class-for-card');
            expect(instructions.length).toBe(0);
        });
    });

    it("displays loading indicator until results are retrieved", async () => {
        const element = await render(Card);

        await shouldShowCardTestAdapter.emit(null);

        return Promise.resolve().then(() => {
            const instructions = element.shadowRoot.querySelectorAll('.unique-html-class-for-loading-indicator');
            expect(instructions.length).toBe(0);
        });
    });
});

Thanks for the advice about wire, wasn't aware that wire would automatically call based on the reactive property like this.

All Answers

3 Creeks3 Creeks
Try using @wire instead of @api for isDone.  I now the doc is showing it using @api but I always use @wire and works for me.
Daniel Vu 6Daniel Vu 6

Thanks. I ended up not requiring a loading indicator, but I think using the wire attached to a method would allow it to happen. Not my exact code but an adaption that I think might work if people want to do something like this.

import { LightningElement, api } from "lwc";
import shouldShowCard from "@salesforce/apex/ApexClass.shouldShowCard";

/**
 * Card component that is conditionally shown based on Apex.
 */
export default class ConditionalCard extends LightningElement {
    @api recordId;

    isDone = false;

    shouldShow = false;

    error = null;

    @wire(shouldShowCard, { id: '$recordId' })
   retrieveResults({ error, data }) {
        // No information has returned, still waiting on Apex results
        if (!error && !data) {
          this.isDone = false;
        } else {
          if (error) {
            this.error = error;
          } else  if (data) {
            this.shouldShow = data;
          }

          this.isDone = true;
        }
    }
}
Tests would end up looking like this
import { registerApexTestWireAdapter } from "@salesforce/sfdx-lwc-jest";
import Card from "c/card";
import shouldShowCard from "@salesforce/apex/ApexClass.shouldShowCard";

const shouldShowCardTestAdapter = registerApexTestWireAdapter(shouldShowCard);

describe("Card", () => {
    it("is displayed if Apex says so", async () => {
        // render is some utility I wrote, just a wrapper around createElement from LWC
        const element = await render(Card);

        await shouldShowCardTestAdapter.emit(true);

        return Promise.resolve().then(() => {
            const instructions = element.shadowRoot.querySelectorAll('.unique-html-class-for-card');
            expect(instructions.length).toBe(1);
        });
    });

    it("is not displayed if Apex does not say so", async () => {
        const element = await render(Card);

        await shouldShowCardTestAdapter.emit(false);

        return Promise.resolve().then(() => {
            const instructions = element.shadowRoot.querySelectorAll('.unique-html-class-for-card');
            expect(instructions.length).toBe(0);
        });
    });

    it("displays loading indicator until results are retrieved", async () => {
        const element = await render(Card);

        await shouldShowCardTestAdapter.emit(null);

        return Promise.resolve().then(() => {
            const instructions = element.shadowRoot.querySelectorAll('.unique-html-class-for-loading-indicator');
            expect(instructions.length).toBe(0);
        });
    });
});

Thanks for the advice about wire, wasn't aware that wire would automatically call based on the reactive property like this.
This was selected as the best answer