Architect Lightning Web Components that Weather any Storm

This comprehensive guide provides key design patterns for developing robust Lightning Web Components (LWC). It covers concepts such as separation of concerns, encapsulation of business logic, loose coupling, inter-component communication, declarative rendering and styling, unit testing and modular architecture.

Introduction : Creating durable and resilient lightning web components

Lightning Web Components (LWC) is a modern web component framework for building reusable UI elements for Salesforce. As with any UI development, writing scalable and maintainable code is crucial when building enterprise LWC applications. In this comprehensive guide, we will explore key design patterns and best practices for developing robust and sustainable Lightning web components.

Specifically, we will cover:

  • Separation of concerns and modular architecture
  • Encapsulation of business logic
  • Loose coupling and reusability
  • Inter-component communication
  • Declarative rendering and styling
  • Unit testing approach

By the end of this guide, you will have a solid understanding of architecting LWC code for scalability along with sample examples based on that you can create your components.

Advertisements

☞ Principles of Scalable LWC Code

Before diving into specific design patterns, let’s outline some overarching principles that lend themselves to scalable LWC development:

💥 Separation of Concerns

Each component should focus on one single responsibility and strictly encapsulate its behavior and dependencies from other components. Adhering to the single responsibility principle is key for both reusability and maintainability.

For example, a data table component for displaying rows of records should not contain logic for fetching or processing data. That should be handled externally, and data simply passed in:

// DataTable.js
import { LightningElement, api }  from 'lwc';

export default class DataTable extends LightningElement {

  @api rows = [];

  @api get columns() {
    // columns derived from row data  
  }

}


// ParentComponent.js 
import { LightningElement } from 'lwc';  
import getData from '/dataService';

export default class ParentComponent extends LightningElement {

  data = [];

  async connectedCallback() {
    this.data = await getData(); 
  }

}

// Markup
<c-parent-component>

  <c-data-table rows={data}></c-data-table>

</c-parent-component>

➡ Encapsulation

Implementation details of a component should be hidden to avoid tight coupling with other components. Complex logic and API calls should be abstracted into a service layer whenever possible.

➡ Loose Coupling

Components should have minimal dependencies on concrete details of other components or services. This reduces churn from changes and enables easier substitution of one component for another.

➡ Declarative Rendering

Components should leverage external configuration for rendering via inputs, slots, and custom CSS properties rather than explicit rendering logic:

// Header.js
import { LightningElement, api } from 'lwc';

export default class Header extends LightningElement {

  @api title;

  @api iconName; 

  @api backgroundColor = 'grey'; 

}


// Usage
<c-header 
  title="Sales Report"
  icon-name="standard:account"
  background-color="red">
</c-header>
Advertisements

💥 Modular Architecture

Now let’s explore a modular architecture optimized for scalable LWC development.

Component Hierarchy

Structure components in a hierarchy from generic to specific:

  • Base components (e.g. Button, Icon, Card)
  • Shared components (e.g. PageLayout, Notification, LinkMenu)
  • Domain components (e.g. AccountCard, ContactTable)

Child components should only ever reference parent or base components, never siblings or cousins. This reduces dependencies between components.

Unidirectional Data Flow

Data in our optimal architecture should flow in one direction down:

  • Parent to child via properties
  • Child to parent via events
  • Services to components via methods

This prevents convoluted data management logic.

➡ Encapsulating Business Logic

Business logic like data access and processing should be extracted out from visual components into a dedicated service layer. These services then expose a simplified interface for components to leverage internally.

// contactService.js
import { LightningElement } from 'lwc';
const CONTACTS_ENDPOINT = '/contacts';

export const getContactList = async () => {

  try {
    const results = await fetch(CONTACTS_ENDPOINT);
    return await results.json();

  } catch(error) {
    throw error;
  }
}  

export const getContactDetails = async (id) => {

  try { 
    const result = await fetch(CONTACTS_ENDPOINT + '/' + id);
    return await result.json();

  } catch(error) {
     throw error; 
  }

}


// ContactViewer.js
import { LightningElement } from 'lwc';
import { getContactList, getContactDetails } from 'contactService';

export default class ContactViewer extends LightningElement {

  contacts = [];
  selectedContact; 

  async connectedCallback() {
    this.contacts = await getContactList(); 
  }

  handleContactSelect(event) {
     const id = event.detail;
     this.selectedContact = await getContactDetails(id);
  }

}

This keeps components simple and focused on UI logic while leveraging services for the heavy lifting.

💥 Inter-Component Communication

Properties and Events
Parent to child data flow should occur via properties, child to parent via events:

// Child Component
import { LightningElement, api } from 'lwc';

export default ContactTile extends LightningElement {

  @api contact;

  handleSelect() {
    const selectEvent = new CustomEvent('select', {  
      detail: this.contact.Id
    });
    this.dispatchEvent(selectEvent);
  } 

}

// Parent Component
<c-contact-tile
  contact={selectedContact}
  onselect={handleSelection}>  
</c-contact-tile>

Lightning Message Service
For loosely coupled communication between components in isolation, leverage the Lightning Message Service.

// Publisher
import { LightningElement } from 'lwc';
import { publish } from 'lightning/messageService'; 

export default class SearchComponent extends LightningElement {

  handleSearch() {
     const message = {
        results: //... 
     };
     publish(this.MESSAGE_CHANNEL, message);
  }

}


// Subscriber 
import { LightningElement } from 'lwc';
import { subscribe } from 'lightning/messageService';

export default class ResultsComponent extends LightningElement {

  subscription = null;

  connectedCallback() {
    this.subscription = subscribe(
      this.MESSAGE_CHANNEL,
      this.handleMessage
    );
  }

  handleMessage(message) {
    this.results = message.results; 
  } 

} 
Advertisements

💥 Reusable Component Design

Slots
Leverage slots to make components customizable and composable:

// Tile.js 
import { LightningElement, api } from 'lwc';

export default class Tile extends LightningElement {

  @api title;

  @api icon; 

  @slot header {
    // custom header content
  }

  @slot footer {
    // custom footer content 
  }

}

// Usage
<c-tile>
   <span slot="header">Custom Tile</span>
   <p>Body Content</p>  
   <span slot="footer">Footer Information</span> 
</c-tile>

Wrappers
Create wrapper components to standardize behavior and layout around other components:

// TileWrapper.js
import { LightningElement } from 'lwc';

export default class TileWrapper extends LightningElement {

  @api src;

  render() {
    return <c-tile>
              <img slot="media" src={this.src}></img>
           </c-tile>

  }

}

// Usage
<c-tile-wrapper 
  src="/image.jpg">
</c-tile-wrapper>

CSS Properties
Expose styling hooks for components to override styles without touching component implementation:

// Button.js
import { LightningElement, api } from 'lwc';

export default class Button extends LightningElement {

  @api customColor; 

  get customStyle() {
    return `color: ${this.customColor}`; 
  }

}

// Usage
<c-button custom-color="deeppink">
  Click Me  
</c-button> 

💥 Testing Approach

Comprehensive testing is critical for long term maintainability.

Unit Tests
Cover individual functions and methods with unit tests:

// contactService.test.js

it('gets contact list', async () => {

  const results = await getContactList();

  expect(results.length).toBeGreaterThan(0);

});

it('handles contact request error', async () => {

  // Mock fetch error 
  jest.spyOn(global, 'fetch').mockRejectedValue('API Error'); 

  await expect(getContactsList()).rejects.toEqual('API Error'); 

}) 

Integration Tests
Test component interactions and data flows:

// ContactViewer.test.js  

const VIEWER = 'c-contact-viewer';

it('shows selected contact when tile clicked', () => {

  const TILE = `${VIEWER} c-contact-tile`; 

  const viewerEl = createElement(VIEWER, { is: VIEWER });  
  document.body.appendChild(viewerEl);

  const tileEl = viewerEl.shadowRoot.querySelector(TILE);

  tileEl.click(); 

  return Promise.resolve().then(() => {  
     expect(viewerEl.selectedContact).toBeDefined();
  })

});

UI Tests
Validate real experience with end-to-end UI tests.

Conclusion

In this guide we covered several key patterns for developing scalable LWC applications:

  • Strict separation of concerns between components
  • Encapsulating business logic in services
  • Unidirectional data flow models
  • Loosely coupled communication between components
  • Declarative rendering and styling hooks
  • Reusable wrappers and slot based composition
  • Comprehensive testing strategy

Applying these patterns will lend to cleaner component abstractions, more streamlined data flows, improved reusability, simpler integration, and reduced maintenance overhead in the long run.

Explore lwc related post / content here

For more join, our telegram channel, Subscribe SFDCLesson YouTube channel.

About the blog

SFDCLessons is a blog where you can find various Salesforce tutorials and tips that we have written to help beginners and experienced developers alike. we also share my experience and knowledge on Salesforce best practices, troubleshooting, and optimization. Don’t forget to follow us on:

Newsletter

Subscribe to our email newsletter to be notified when a new post is published.

Arun Kumar
Arun Kumar

Arun Kumar is a Salesforce Certified Platform Developer I with over 7+ years of experience working on the Salesforce platform. He specializes in developing custom applications, integrations, and reports to help customers streamline their business processes. Arun is passionate about helping businesses leverage the power of Salesforce to achieve their goals.

Articles: 162

Leave a Reply

Discover more from SFDC Lessons

Subscribe now to keep reading and get access to the full archive.

Continue reading

Discover more from SFDC Lessons

Subscribe now to keep reading and get access to the full archive.

Continue reading