Refs vs QuerySelector in LWC
Hi devs!
Let’s talk about access to HTML elements in LWC. Salesforce released Refs during Spring ’23.
Now we have two ways to query DOM elements: refs
and querySelector
.
Which one is better? Let’s cover it in this post.
QuerySelector
The first way to access HTML elements is to use querySelector()
and querySelectorAll()
.
querySelector()
and querySelectorAll()
are standard ways to access DOM elements.
querySelector
is not LWC-specific. It’s available in vanilla JavaScript. You can read it in the official documentation:
What are the definitions?
querySelector()
returns the first Element within the document that matches the specified selector, or group of selectors.querySelectorAll()
returns a static (not live) NodeList representing a list of the document’s elements that match the specified group of selectors.
How to use querySelector
in LWC?
It’s quite easy. You need to specify selectors
condition for the HTML element you would like to get.
<template>
<div data-name="myDiv"></div>
<lightning-button label="Get Element" onclick={handleClick}></lightning-button>
</template>
import { LightningElement } from 'lwc';
export default class QuerySelectorExample extends LightningElement {
handleClick() {
console.log(this.template.querySelector('[data-name="myDiv"]'));
}
}
You can have different selectors:
const matches = document.querySelectorAll("p");
const matches = document.querySelectorAll("div.note, div.alert");
What you will get?
QuerySelector Considerations
renderedCallback
As we can read in Lifecycle Hooks and Run Code When a Component Renders, to access HTML you can use querySelector
in renderedCallback()
. Do not try to access HTML in connectedCallback()
.
Use renderedCallback() to understand the state of the "inside" world (a component’s UI and property state), and use connectedCallback() to understand the state of the "outside" world (a component’s containing environment).
What does it mean?
HTML code is accessible only in renderedCallback()
method!
Accessibility Issues
There are a few problems with accessibility:
The first one – the problem with ids.
Don’t use ID selectors with querySelector. The IDs that you define in HTML templates may be transformed into globally unique values when the template is rendered. If you use an ID selector in JavaScript, it won’t match the transformed ID.
Second one – HTML modification:
Assume you have code like this:
<template>
<div class="slds-hide">Hello World!</div>
<lightning-button label="Toggle" onclick={handleToggle}></lightning-button>
</template>
import { LightningElement } from 'lwc';
export default class QuerySelectorExample extends LightningElement {
handleToggle() {
const myDiv = this.template.querySelector('.slds-hide');
myDiv.toggle('slds-hide');
}
}
The code will work just once. The selector, which in this case is .slds-hide
, will be removed from HTML.
Race Conditions
Different methods can try to manipulate DOM, which can lead to unpredictable behavior. Let’s dive into the code.
<template>
<div class="slds-hide">Hello World!</div>
<lightning-button label="Toggle" onclick={handleToggle}></lightning-button>
<lightning-button label="Update Message" onclick={handleMessageUpdate}></lightning-button>
</template>
import { LightningElement } from 'lwc';
export default class QuerySelectorExample extends LightningElement {
handleToggle() {
const myDiv = this.template.querySelector('.slds-hide');
myDiv.toggle('slds-hide');
}
handleMessageUpdate() {
const myDiv = this.template.querySelector('.slds-hide');
myDiv.innerText = 'Welcome!';
}
}
It’s a really dummy example. When handleToggle
method is executed, handleMessageUpdate
will not work anymore because slds-hide
class will be removed from HTML.
Refs
Refs locate DOM elements without a selector and only query elements contained in a specified template
You can use the refs
object to get a reference to a DOM element.
How to use refs
in LWC?
Usage of refs is even more straightforward than for querySelector
.
<template>
<div lwc:ref="myDiv"></div>
<lightning-button label="Get Ref" onclick={handleClick}></lightning-button>
</template>
import { LightningElement } from 'lwc';
export default class RefExample extends LightningElement {
handleClick() {
console.log(this.refs.myDiv);
}
}
What you will get?
this.refs.myDiv
returns Element.
Refs Considerations
renderedCallback
The same situation as with querySelector
. You can access refs
in renderedCallback()
, but not in connectedCallback()
.
Refs in a loop
Do NOT place lwc:ref
in a for:each
/iterator:*
loop. The template throws an error.
Duplicated refs
If the template contains duplicate lwc:ref
directives, this.refs
references the last <div>
.
Refs are read-only
this.refs
is read-only object. This does NOT mean that you cannot perform operations on the div you received from refs. It means that this.refs.something = {};
is forbidden, and you will get Uncaught TypeError: Cannot add property something, object is not extensible
. The funny thing is that you can do this.refs = {}
, which totally messes up the refs
.
Do not add ref to slot
You cannot add lwc:ref
to a <slot>
element
Refs vs QuerySelector Considerations
Use refs every time you can
Performance
refs
are much more efficient than querySelector
. Query Selector has to scan every DOM element to find elements that match your query.
Time complexity:
- refs: O(1)
- querySelector: O(n)
I prepared simple code to compare refs
and querySelector
performance.
<template>
<div lwc:ref="myDiv" class="myDiv">
Hello World!
</div>
<lightning-button label="Test Refs" onclick={handleRefs}></lightning-button>
<lightning-button label="Test QuerySelector" onclick={handleQuerySelector} class="slds-m-left_x-small"></lightning-button>
</template>
import { LightningElement } from "lwc";
export default class RefsTest extends LightningElement {
handleRefs() {
console.time("Refs");
for (let i = 0; i < 100000; i++) {
const myDiv = this.refs.myDiv;
}
console.timeEnd("Refs");
}
handleQuerySelector() {
console.time("Query Selector");
for (let i = 0; i < 100000; i++) {
const myDiv = this.template.querySelector(".myDiv");
}
console.timeEnd("Query Selector");
}
}
Here are the results:
- Refs ~ 22 ms
- Query Selector ~ 55 ms
refs
are around 60% faster than querySelector
.
Considerations:
- Performance depends on your computer because all JavaScript operations are executed in your browser. The results for you will probably be slightly different, but
refs
will certainly be faster. - I also tested a case where the HTML had a lot of divs.
refs
performance remained the same (22 ms), butquerySelector
increased from 55 ms to 75 ms.
Security
Performance is just one thing. We can also read in the Access Elements the Component Owns that:
If the component runs in an org with Lightning Locker enabled, be aware of a potential memory leak. If possible, the org should enable Lightning Web Security. Alternatively, consider using refs instead of querySelector.
Use querySelectorAll with loop
As we read in the documentation:
If you place lwc:ref in a for:each or iterator:* loop, the template compiler throws an error.
for:each
and iterator:*
are places where you should use querySelector/querySelectorAll
to access DOM elements.
Use querySelector with manual dom
<template>
<div lwc:dom="manual"></div>
</template>
refs
also do not work with lwc:dom="manual"
where code is rendered dynamically.
You should use querySelector/querySelectorAll
to access DOM elements.
Combine ref and querySelectorAll
To mitigate accessibility issues, instead of using just querySelectorAll
combine it with refs
.
<div lwc:ref="myForm">
<lightning-input type="text" label="First Name"></lightning-input>
<lightning-input type="text" label="Last Name"></lightning-input>
<lightning-input type="email" label="Email"></lightning-input>
</div>
<lightning-button label="Save" onclick={handleSave}></lightning-button>
handleSave() {
const formInputs = this.refs.myForm.querySelectorAll('input');
formInputs.forEach(...);
// ...
}
If you have any questions feel free to ask in the comment section below. 🙂
Was it helpful? Check out our other great posts here.