Over the past two years, I’ve had the opportunity to review hundreds of Lightning Web Components developed by internal Salesforce developers and by customers and partners building on the Salesforce platform. This post is the second post of a two parts series covering some of the recurring observations and feedback.
Make invalid state hard to represent
When building an LWC component, you should structure your component so that it’s impossible for the component to be invalid. Think of your LWC component as a state machine, where each interaction with this component makes the component transition from one state to another. A component enters an invalid state when its public and private properties (the component state) have values that shouldn’t be possible.
It would deserve a full blog post to cover the subtle art of state structuring, but for brevity’s sake, I will focus on the simple yet common issue of state duplication.
Let’s look at a simple example to illustrate this issue. This component tracks a counter value and shows some text if the value is strictly greater than 0.
This approach suffers from the fact that this component needs to deal with two fields,
isPositive, when only a single property is actually needed.
- Both the
handleDecmethods need to make sure that
isPositivestay in sync.
- If the initial value of the
valueproperty is changed from
isPositiveinitial value must also be set to
- If we added a reset functionality, both
isPositivewould need to be reset.
Let’s refactor the example to use a single property. With this approach, the
isPositive property is derived from
value using a getter. By keeping a single tracked property, the component can’t be in an invalid state.
Do you really need those libraries?
With more than 1 million packages published on NPM, there is a high chance that someone has already released a library for what you’re trying to build. While reusing existing libraries greatly increases your productivity, keep in mind that this productivity boost comes with some trade-offs.
- Performance degradation: Depending on the size, each library you load may impact end-user perceived performance.
- Locker integration issues: The Locker Service enforces strict constraints for code that runs in Lightning. When using some third-party code, you lose control of which APIs are used, and those APIs may not be compatible with Locker. Debugging Locker related integration issue can be quite challenging. When you are lucky, an error is thrown at runtime. In some cases, the third-party library might fail silently. The easiest way to know if an issue is due to Locker is to load your library and reproduce the same code in the Locker Console, where you can turn Locker on and off.
- jQuery (29 KB minified + gzip): https://github.com/nefe/You-Dont-Need-jQuery
- Lodash (26 KB minified + gzip): https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore
- MomentJs (core: 16 KB minified + gzip / core + locales: 61 KB minified + gzip): https://github.com/you-dont-need/You-Dont-Need-Momentjs
If you decide that you really need some third-party code, there are a couple of ways to load it in your LWC component.
An alternative approach is to load the third-party code via a static resource and
lightning/platformResourceLoader module exposes two methods–
loadStyle–that are equivalent to creating a script tag or a style tag and inserting it into the component’s shadow root.
I’ll leave you with two pieces of advice that can drive your decisions when it comes to evaluating third-party code.
- Always check the size of the third-party code. If the library size is greater than 30KB minified + gzip, ask yourself twice if this code is really needed.
- Always favor self-contained libraries over libraries with external dependencies. I would always prefer a standalone carousel library (for example, glidejs) over a jQuery plugin carousel (for example, slick). Using a self-contained library will save you from some debugging interoperability nightmares.
Organize your class fields & methods
It’s a good practice to group the class fields and methods in a consistent order, because it helps navigate the code. Here’s the way I structure my components.
Organize your imports
Import statements are the standard way to reference Salesforce platform features from your LWC components: Apex methods, labels, static resources, permissions, and so on. Complex components have the tendency to import a lot of external modules. As the number of
import statements increases, it becomes necessary to start structuring them.
Each developer has their own preferences for styling. This is the way I group my imports.
When there are too many imports in a single file you can externalize them. This is a common approach when a component imports a LOT of labels. Instead of importing all the labels in the component file, import the labels in a separate file (
label.js) and export them as a single object. To make all the labels accessible to the template, the component file (
cmp.js) imports the object containing all the labels and assigns it to a property.
You can note that this trick not only works with label imports, but also with all the other salesforce imports in the LWC.
Don’t create public properties for testing purposes
LWC intentionally doesn’t provide an equivalent to the Apex @TestVisible annotation. If you’re used to this escape hatch, it might be challenging to write LWC unit tests. It might be tempting to use
@api to expose properties and methods just so that you can test them. This approach has the undesirable side effect of exposing those properties at runtime, which makes the component more fragile.
Instead, LWC encourages writing blackbox unit tests. Picture your component responding to a set of inputs and producing different outputs. With blackbox unit testing, a test should check if a set of inputs produces the expected output.
Inputs are what triggers a component to change:
- Public properties
- Events received from a child
- Global events
- External side effects
Outputs reflect how the component reacts to inputs:
- DOM state
- External side effects
To illustrate, let’s use a simple example. Consider a component that displays a
<lightning-spinner> while the data is being loaded. A private field named
isLoading switches from
true to display the spinner.
How would you test the loading state of the component?
The most straightforward approach is to check if the
isLoading field is
true when the data is loading and then set it to
false after the reception of the data. Such an approach would require making the
isLoading field public, which is the opposite of what we want.
We don’t want a component consumer to set the
isLoading field. The
isLoading field is only used inside the component to transition the spinner from visible to hidden. In this case, what we need to test is not that the
isLoading flag (internal state) is set, but rather than the spinner is actually rendered (output: DOM state).
This test, instead of checking the
isLoading field value, queries the shadow root to see if the
<lightning-spinner> is actually rendered. We’re testing what the software does, not how it does it. (For brevity, the code required to mock the
loadRecord method isn’t present.)
Remove all method
console.* methods before production
System.debug(message) in Apex. As the application grows, it’s easy for the console to be flooded with log lines from dozens of components. Furthermore, these logs occur for all users; the code isn’t removed in non-
I recommend removing all the
console.* usages in the code before you commit it to your source control management (like Git) or save it to your org. The no-console eslint rule, included in eslint-config-lwc, causes
console.* references to appear as errors. The same rule can be enforced in your CI and developer flow.
Developer tools have come a long way since the
alert() debugging days. To debug my components, I regularly use the following features instead of
- Conditional breakpoints: Chrome / Firefox / Safari
- Pretty print file: Chrome / Firefox
- Expression watch: Chrome / Firefox
- Script black-boxing: Chrome / Firefox
- Watchpoint: Firefox
Adopt a linter
Linters are awesome static analysis tools that make development much easier. These tools analyze code without actually running it, and report errors for known issues. The linter is a huge time-saver and productivity-booster when used properly! Most of the obvious mistakes can be caught without pushing the code to your org, refreshing the browser, and triggering your component.
For LWC, we recommend using ESLint along with the eslint-config-lwc. The linting is automatically set up and configured by the Salesforce Extensions Pack for VS Code when you create an SFDX project. Other popular code editors also offer an integration with ESLint (VSCode, Intellij).
Note that only the
@salesforce/eslint-config-lwc/base rules are enforced when saving a component to your Salesforce org. Locally, you can change the default linting configuration to be more or less strict depending your own preference.
Enforce a consistent code style
It’s beneficial to enforce a certain set of code styling rules, especially as the number of developers involved in a project increases. A well-defined set of code styling rules offers advantages.
- Eases the onboarding of new team member.
- Eliminates pointless code formatting arguments during code review.
- Simplifies reviewing someone else’s code. This is especially true when the person is external to the project.
Going a step further, the code formatting might be delegated to a tool such as Prettier. Using such tools removes the need to worry about manually formatting code before submitting a code change.
Embrace continuous integration (CI)
Continuous integration automatically integrates the code changes of multiple contributors into a single software project. As the application grows in complexity and the number of contributors increases, pulling down each submitted code change locally to run the test and do manual verification becomes impossible. It’s important to automate these tasks as soon as you start a new project.
Things you can run as part of the CI:
- Run your linter and make the build fail if the linter reports an error.
- Run your code formatter and make the build fail if the code style guide isn’t respected.
- Run unit and integration tests.
- Deploy a canary version of the application for manual testing purposes.
To find out how to set up your own CI, follow the Continuous Integration Using Salesforce DX trail.
Well, everything comes to an end. I hope this series gave you some insights into how to improve your LWC skills, regardless of whether you just started or if you’re an experienced developer. Let us know via Twitter if one of these tips worked well for you!
Big shoutout to Alba Rivas and Jody Bleyle for their guidance and review of this blog post series.
About the author
Pierre-Marie Dartus works a software engineer at Salesforce as part of the LWC core team. He focuses mainly on compilation and performance.