Lightning Web Component (LWC) - Core

Web component:

LWC file types:

Lightning Data Service

Lightning Messaging Service

Decorators

  1. @api

    • To expose a public property, decorate a field with @api. Public properties define the API for a component. Public properties used in a template are reactive

    • A Component is re-rendered when the value of a referenced public property is modified or changed

    • To pass data from parent component to child component, @api decorator in the child component exposes a property by making it public so that parent component can update it

    • @api properties can be exposed in an App builder

  2. @track

    • Previously, you had to use the @track decorator to make a field reactive. Beginning in Spring '20, all fields in a LWC class are reactive.

    • If a field is assigned an object or an array, the framework observes some changes to the internals of the object or array, such as when you assign a new value.

      You still need to decorate a field with @track in these use cases:

      • Observing changes to the properties of an object

      • Observing changes to the elements of an array

  3. @wire

    • Reactive wire service is utilized in LWC to read the Salesforce data from apex class into LWC

    • Component is re-rendered when wire service provisions the data from apex class. The output from apex method is set to a property

Communicate with Events

  1. To get a reference to the object that dispatched the event, use the Event.target property which is part of the DOM API for events.

  2. To create an event, use the CustomEvent() constructor. To dispatch an event, call the EventTarget.dispatchEvent() method.

  3. The CustomEvent() constructor has one required parameter, which is a string indicating the event type

     selectHandler(event) {
         // Prevents the anchor element from navigating to a URL.
         event.preventDefault();
         // Creates the event with the contact ID data.
         const selectedEvent = new CustomEvent("selected", { detail: this.contact.Id });
         // Dispatches the event.
         this.dispatchEvent(selectedEvent);
       }
    

Attach an Event Listener Declaratively

  1. Declare the listener in markup in the template of the owner component, in this example, c-parent

     <!-- parent.html -->
     <template>
       <c-child onnotification={handleNotification}></c-child>
     </template>
    

Attach an Event Listener Programmatically

  1. Define both the listener and the handler function in the c-parent JavaScript.

     // parent.js
     import { LightningElement } from "lwc";
     export default class Parent extends LightningElement {
       constructor() {
         super();
         this.template.addEventListener("notification", this.handleNotification);
       }
       handleNotification = () => {};
     }
    

    To add an event listener to an element within the shadow boundary, use template.

     this.template.addEventListener();
    

    To add an event listener to an element that a template doesn’t own, call addEventListener directly.

     this.addEventListener();
    

Event Retargeting

  1. When an event bubbles up the DOM, if it crosses the shadow boundary, the value of Event.target changes to match the scope of the listener. This change is called “event retargeting.” The event is retargeted so the listener can’t see into the shadow DOM of the component that dispatched the event. Event retargeting preserves shadow DOM encapsulation.

  2. Imagine an event is dispatched from a div element in the c-todo-item component. Within the component’s shadow DOM, Event.target is div. But to a listener on the p element in the containing c-todo-app component, the Event.target is c-todo-item, because the p element can’t see into the c-todo-item shadow DOM.

     <c-todo-app>
       #shadow-root
       <div>
         <p>Your To Do List</p>
       </div>
       <c-todo-item>
         #shadow-root
         <div>
           <p>Go to the store</p>
         </div>
       </c-todo-item>
     </c-todo-app>
    
  3. It’s interesting to note that to a listener on c-todo-item, the Event.target is c-todo-item, not div, because c-todo-item is outside the shadow boundary.

Event Propagation

  • After an event is fired, it can propagate up through the DOM. To understand where events can be handled, understand how they propagate

  • Events bubble up through the DOM; that’s how children and parents communicate— props down, events up

  • Event targets don’t propagate beyond the shadow root of the component instance. From outside the component, all event targets are the component itself. However, inside the shadow tree, you can handle events from specific targets in the tree

  • Every web component’s DOM is encapsulated in a shadow DOM that other components can’t see. When an event bubbles (bubbles = true), it doesn’t cross a shadow boundary unless you configure it to (composed = true).

  • Slots - A web component can contain <slot></slot> elements. Other components can pass elements into a slot, which allows you to compose components dynamically.

    When a component has a <slot></slot>, a container component can pass light DOM elements into the slot.

      <!-- flattened DOM -->
      <body>
           <c-container> #shadow-root     
              <c-child-slot>
                      #shadow-root         
                      <h2>I can host other elements via slots</h2>         
                      <slot>
                              // To the outside world, 
                              // I'm not part of c-child-slot shadow DOM
                              // I'm part of c-container shadow DOM            
                              <p> To c-container, I'm light DOM.</p>      
                      </slot>     
              </c-child-slot>
       </c-container>
      </body>
    

    The browser renders the flattened tree, which is what you see on the page. The important thing to understand is that the DOM passed into the slot doesn’t become part of the child’s shadow DOM; it’s part of the container’s shadow DOM.

  • When you create an event, define event bubbling behavior using two properties: bubbles and composed.

    • bubbles : A Boolean value indicating whether the event bubbles up through the DOM or not. Defaults to false.

    • composed : A Boolean value indicating whether the event can pass through the shadow boundary. Defaults to false.

  • event.target — A reference to the element that dispatched the event. As it bubbles up the tree, the value of target changes to represent an element in the same scope as the listening element. This event retargeting preserves component encapsulation. We’ll see how this works later on.

  • event.currentTarget — A reference to the element that the event handler is attached to.

  • event.composedPath() — Interface that returns the event’s path, which is an array of the objects on which listeners will be invoked, depending on the configuration used.

Static Composition:

  • A static composition doesn’t use slots. Here we have the simplest example: c-app composes component c-parent, which in turn composes c-child.

  • We fire an event, buttonclick, from c-child whenever a click action happens on its button element. We have attached event listeners for that custom event

      <body> <!-- Listening for "buttonclick" event -->
          <c-app> <!-- Listening for "buttonclick" event -->
          #shadow-root 
            <h2>My app</h2>
            <c-parent> <!-- Listening for "buttonclick" event -->
              #shadow-root
                <h3>I'm a parent component</h3>
                <div><!-- Listening for "buttonclick" event -->
                  <c-child> <!-- Listening for "buttonclick" event -->
                    #shadow-root
                      <h3>I'm c-child component</h3>
                      <button>click me</button>
                    #/shadow-root
                  </c-child>
                </div>
              #/shadow-root
            </c-parent>
            #/shadow-root
          </c-app>
      </body>
    
  1. bubbles: false, composed: false

    • only c-child gets to react to the buttonclick event fired from c-child. The event doesn’t bubble past the host. This is the recommended configuration because it provides the best encapsulation for your component

      we inspect c-child handler’s we find these values on the event object:

  2. bubbles: true, composed: false

    • the buttonclick event from c-child event travels from bottom to top until it finds a shadow root or the event gets canceled. The result, in addition to c-child, div.wrapper can also react to the event.

    • Use this configuration to bubble up an event inside the component’s template

      c-child handler:

div.childWrapper handler:

  • event.currentTarget = div.childWrapper

  • event.target= c-child

  1. bubbles : false, composed : true

    • This configuration is supported for native shadow DOM, which means it

      ⚠️isn’t supported on Lightning Platform. Even for LWC open source, this configuration isn’t suggested

    • Composed events can break shadow boundaries and bounce from host to host along their path. They don’t continue to bubble beyond that unless they also set bubbles:true.

  2. bubbles : true, composed : true

    • isn’t suggested because it creates an event that crosses every boundary.

    • Every element gets the event, even the regular DOM elements that aren’t part of any shadow.

    • The event can bubble all the way up to the body element.

Lifecycle Hooks

Below is the Lifecycle hooks Flow:

  1. Constructor:

    • The constructor() method fires when a component instance is created.

    • Don’t add attributes to the host element during construction. You can add attributes to the host element in any other lifecycle hook

    • The constructor flows from parent to child.

    • The first statement must be super() with no parameters. This call establishes the correct prototype chain and value for this. Always call super() before touching this.

    • Don’t use a return statement inside the constructor body, unless it is a simple early-return (return or return this).

    • Don’t use the document.write() or document.open() methods.

    • Don’t inspect the element's attributes and children, because they don’t exist yet.

    • Don’t inspect the element’s public properties, because they’re set after the component is created.

    • Can we call an apex method inside the constructor method in Lightning Web Component? YES

    • Can we create and dispatch events in constructor method of Lightning Web Component? NO

  2. connectedCallback():

    • This hook is invoked when a component is connected to the document. This callback is invoked after all the public properties are set and can be used to set the state of the component.

    • connectedCallback() fires when a component is inserted into the DOM

    • flow from parent to child

    • To access the host element, use this. To access elements in a component’s template, use this.template

    • Perform initialization tasks, such as fetch data, set up caches, or listen for events

    • Subscribe and Unsubscribe from a Message Channel.

    • The connectedCallback() hook can fire more than one time. For example, if you remove an element and then insert it into another position

    • You can’t access child elements from the callbacks because they don’t exist yet.

    • public properties can be updated after the connectedCallback is invoked. The connectedCallbackis invoked only one time with the initial properties passed to the component. If a component derives its internal state from the properties, it's better to write this logic in a setter than in connectedCallback

  3. disconnectedCallback():

    • fires when a component is removed from the DOM

    • flow from parent to child

    • Use disconnectedCallback() to clean up work done in the connectedCallback(), like purging caches or removing event listeners.

  4. renderedCallback():

    • The renderedCallback() is unique to Lightning Web Components. Use it to perform logic after a component has finished the rendering phase.

    • This hook flows from child to parent.

    • A component is usually rendered many times during the lifespan of an application. To use this hook to perform a one-time operation, use a boolean field like hasRendered to track whether renderedCallback() has been executed.

    • When a template is rerendered, the LWC engine attempts to reuse the existing elements. In the following cases, the engine uses a diffing algorithm to decide whether to discard an element.

      • Elements created using the for:each directive. The decision to reuse these iteration elements depends on the key attribute. If key changes, the element may be rerendered. If key doesn’t change, the element isn’t rerendered, because the engine assumes that the iteration element didn’t change.

      • Elements received as slot content. The engine attempts to reuse an element in a <slot>, but the diffing algorithm determines whether to evict an element and recreate it.

      • Updating the state of your component inrenderedCallback() can cause an infinite loop - Don’t update a public property or field in renderedCallback()

  5. errorCallback():

    • Implement it to create an error boundary component that captures errors in all the descendent components in its tree.

    • It captures errors that occur in the descendant's lifecycle hooks or during an event handler declared in an HTML template.

    • errorCallback() catches errors that occurs in the descendant components but not itself.

    • You can create an error boundary component and reuse it throughout an app. It’s up to you where to define those error boundaries. You can wrap the entire app, or every individual component. Most likely, your architecture falls somewhere in between. Think about where you’d like to tell users that something went wrong.

      1.     <!-- boundary.html -->
            <template>
              <template lwc:if={error}>
                <error-view error={error} info={stack}></error-view>
              </template>
              <template lwc:else>
                <healthy-view></healthy-view>
              </template>
            </template>
        
    • The error-view component displays the error returned by errorCallback(). Otherwise, it displays the healthy-view component. By wrapping your component inside the boundary component, any unhandled errors are still caught by the boundary component and displayed by error-view. When an error is thrown, healthy-view is unmounted and removed from the DOM.

        // boundary.js
        import { LightningElement } from "lwc";
        export default class Boundary extends LightningElement {
          error;
          stack;
          errorCallback(error, stack) {
            this.error = error;
          }
        }
      

Shadow DOM

  1. Shadow DOM is a standard that encapsulates the internal document object model (DOM) structure of a web component.

  2. Encapsulating the DOM gives developers the ability to share a component and protect the component from being manipulated by arbitrary HTML, CSS, and JavaScript.

  3. Consistent Cross-Browser Experience: Not all browsers had full support for the native Shadow DOM when LWC was conceived. Using the Synthetic Shadow DOM ensures that LWC components provide consistent behaviour across all browsers.

  4. Encapsulation Maintained: Just like the native counterpart, the Synthetic Shadow DOM encapsulates styles and scripts. Styles defined in an LWC component stay within it, and external global styles don’t influence the component’s appearance.

  5. Scoped Events: Events that are fired from within an LWC component are also encapsulated. This ensures that event handlers outside the component cannot detect events that are meant to stay inside.

  6. DOM Query Limitations: When querying the DOM from within an LWC component using methods like querySelector, the search is limited to the component’s template. It respects the encapsulation and doesn’t reach out to the entire DOM of the page.

  7. Elements below the shadow root are in the shadow tree

     <c-todo-app>
       #shadow-root
       <div>
         <p>Your To Do List</p>
       </div>
       <c-todo-item>
         #shadow-root
         <div>
           <p>Go to the store</p>
         </div>
       </c-todo-item>
     </c-todo-app>
    

How Client-Side Caching works in LWC

cacheable methods enhances your component's performance by swiftly displaying cached data from client-side storage, eliminating the need for a server round trip

  1. Expiration Age (Client-side Cache Timeout): In Winter '21, this duration is set to 8 hours and dictates the period for which data is retained in the cache.

  2. Refresh Age (Refresh Interval): Set to 15 minutes, in Winter'21, this parameter regulates how long cached data remains usable before requiring a refresh.

  3. Cache Expired Response: If the response is not present in the cache or has expired, Salesforce initiates a call to the server method, caches the response, and subsequently invokes the action callback method.

  4. Cached Response Available and Unchanged: In the event that the response is available in the cache and doesn't require refreshing, Salesforce directly calls the action callback method using the cached response.

  5. Cached Response Available but Requires Refresh: When the response is present in the cache but necessitates refreshing, Salesforce invokes the action callback method using the cached response. Simultaneously, it makes a background call to the server method. If the server response differs from the cached one, Salesforce triggers a second invocation of the action callback method.

  6. To call an Apex method imperatively, you can choose to set cacheable=true

  7. The caching refresh time is the duration in seconds before an entry is refreshed in storage. The refresh time is automatically configured in Lightning Experience

Order of execution of @wire in lwc

  1. In a typical wire setup, the life cycle will be:

    • constructor

    • wire provisions empty object ({data, error} are each undefined)

    • connectedCallback

    • render

    • renderedCallback

    • wire provisions results from server

    • render (if dirty data)

    • renderedCallback (ditto

  2. Using imperative Apex in the constructor, you end up with:

    • constructor

    • connectedCallback

    • render

    • renderedCallback

    • Apex call returns data

    • render (if dirty data)

    • renderedCallback (ditto)

With either scenario, any server-side code will be delayed until after the first renderedCallback. The initialization process of components is atomic (here, meaning a single indivisible unit of execution). All asynchronous actions (both wire and Apex), up to the allowed limit of 2,500 actions, will be queued up and sent to the server in a single call

async connectedCallback() {
  await Promise.resolve();
  let apexResults = await Promise.all([ method1(params), method2(params) ])
  // Do something with results
}

The await Promise.resolve() allows all of the wire methods to go first, in a separate server roundtrip, then the Apex methods will be called afterwards in a second roundtrip.

Refresh the Cache When Calling a Method Imperatively

  1. To refresh stale Apex data, invoke the Apex method and then call notifyRecordUpdateAvailable(recordIds) to update the Lightning Data Service (LDS) cache.

  2. Lightning Data Service doesn’t manage data provisioned by imperative Apex calls. After you invoke the Apex method, call notifyRecordUpdateAvailable(recordIds) to signal to Lightning Data Service that some records are stale and refresh those records in the cache.

     import { LightningElement, wire } from 'lwc';
     import { getRecord, notifyRecordUpdateAvailable } from 'lightning/uiRecordApi';
     import apexUpdateRecord from '@salesforce/apex/Controller.apexUpdateRecord';
    
     export default class Example extends LightningElement {
         @api recordId;
         // Wire a record
         @wire(getRecord, { recordId: '$recordId', fields: ... })
         record;
         async handler() {
           // Do something before the record is updated
           // Update the record via Apex
           await apexUpdateRecord(this.recordId);
           // Notify LDS that you've changed the record outside its mechanisms
           // Await the Promise object returned by notifyRecordUpdateAvailable()
           await notifyRecordUpdateAvailable([{recordId: this.recordId}]);
         }
     }
    

Refresh the Cache When Using @wire

  1. To refresh Apex data provisioned via an Apex @wire, call refreshApex()

  2. The parameter you refresh with refreshApex() must be an object that was previously emitted by an Apex @wire

  3. The refreshApex() function returns a Promise. When the Promise is resolved, the data in the wire is fresh

     import { updateRecord } from 'lightning/uiRecordApi';
     import { refreshApex } from '@salesforce/apex';
     import getOpptyOverAmount from '@salesforce/apex/OpptyController.getOpptyOverAmount;
    
     @wire(getOpptyOverAmount, { amount: '$amount' })
     opptiesOverAmount;
    
     // Update the record using updateRecord(recordInput)
     // Refresh Apex data that the wire service provisioned
     handler() {
       updateRecord(recordInput).then(() => {
         refreshApex(this.opptiesOverAmount);
       });
     }
    
     //for wired functions
     @wire(getActivityHistory, { accountId: '$recordId', max: '500' })
     wiredGetActivityHistory(value) {
         // Hold on to the provisioned value so we can refresh it later.
         this.wiredActivities = value;
         // Destructure the provisioned value
         const { data, error } = value;
         if (data) { ... }
         else if (error) { ... }
     }
     handler() {
       refreshApex(this.wiredActivities);
     }