Piotr Gajek
written by Piotr Gajek
posted on December 1, 2023
Technical Architect and Full-stack Salesforce Developer. He started his adventure with Salesforce in 2017. Clean code lover and thoughtful solutions enthusiast.
Table of Content

Refs vs QuerySelector in LWC

refs vs query selector in lac

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 method returns Element.
  • querySelectorAll method returns NodeList

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?

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:

  1. 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.
  2. I also tested a case where the HTML had a lot of divs. refs performance remained the same (22 ms), but querySelector 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.


References