In Salesforce Lightning Web Components (LWC), we have powerful features like @track
, @wire
, Custom Events, and Lightning Message Service (LMS) that work effectively. However, these tools often require significant additional effort when managing complex states or sharing data between multiple components.
What are Signals?
Signals is a concept utilized by numerous modern libraries and frameworks including SolidJs, Preact, React, and Angular. It enables automatic reactivity in all locations when a value changes from any source.
This isn't a new concept - KnockoutJs implemented this mechanism in their observables back in 2010.
While each Signal implementation differs, the core concept remains consistent across frameworks.
How is this related to Salesforce?
Salesforce is currently experimenting with the Signals concept for LWC. We can explore its potential implementation by examining this package: https://www.npmjs.com/package/@lwc/signals
The implementation closely mirrors Preact Signals (https://github.com/preactjs/signals).
It introduces a primitive signal()
with a .value
property that can be accessed and modified. Components then react to changes and re-render, similar to when using a @track
property.
import { signal } from 'some/signals';
export default class ExampleComponent extends LightningElement {
count = signal(0);
increment() {
this.count.value++;
}
}
<template>
<button onclick="{increment}">Increment</button>
<p>{count.value}</p>
</template>
Additionally, there's a subscribe()
method that enables notifications about value changes from a signal.
const firstName = signal("Joe");
firstName.subscribe(() => {
console.log(`First Name new value: ${firstName.value}`);
});
firstName.value = "John";
First Name new value: John
Whats the difference with @track?
Salesforce LWC automatically reacts to property changes - you don't even need @track anymore.
In this example, both properties (firstName
and lastName
) are reflected in the template when their values change.
// example/example.js
export default class Example extends LightningComponent {
@track firstName; // <-- tracked
lastName; // <-- tracked
handleFirstNameChange(event) {
this.firstName = event.detail.value;
}
handleLasttNameChange(event) {
this.lastName = event.detail.value;
}
}
<!-- example/example.html -->
<template>
<div>
<lightning-input type="text" label="First Name" onchange={handleFirstNameChange}></lightning-input>
</div>
<div>
<lightning-input type="text" label="Last Name" onchange={handleLastNameChange}></lightning-input>
</div>
<div>Full Name: {firstName} {lastName}</div>
</template>
But there are some limitations
To achieve reactivity for properties, they must be declared first.
For instance, in this case, changes to lastName
are not reflected:
// example/example.js
export default class Example extends LightningComponent {
firstName; // <-- tracked
handleFirstNameChange(event) {
this.firstName = event.detail.value;
}
handleLastNameChange(event) {
this.lastName = event.detail.value; // <-- not tracked
}
}
Furthermore, sharing and reflecting state between components presents challenges.
Let's attempt to share our state with a child component:
// parent/parent.js
export default class Parent extends LightningComponent {
firstName;
lastName;
handleFirstNameChange(event) {
this.firstName = event.detail.value;
}
handleLastNameChange(event) {
this.lastName = event.detail.value;
}
}
<!-- parent/parent.html -->
<template>
<div>
<lightning-input type="text" label="First Name" onchange={handleFirstNameChange}></lightning-input>
</div>
<div>
<lightning-input type="text" label="Last Name" onchange={handleLastNameChange}></lightning-input>
</div>
<c-child first-name={firstName} last-name={lastName}></c-child>
</template>
Here, we need to pass the tracked properties to the child component, which can then receive them via @api
.
// child/child.js
export default class Child extends LightningComponent {
@api firstName = "";
@api lastName = "";
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
}
<!-- child/child.html -->
<template>
<div>Full Name: {fullName}</div>
</template>
However, a challenge arises when trying to modify the state from the child component:
// child/child.js
export default class Child extends LightningComponent {
@api firstName = "";
@api lastName = "";
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
handleClearName() {
this.firstName = ""; // <-- fails
this.lastName = ""; // <-- fails
}
}
<!-- child/child.html -->
<template>
<div>Full Name: {fullName}</div>
<div>
<lightning-button label="Clear Name" onclick={handleClearName}></lightning-button>
</div>
</template>
Directly overriding an @api
property's value is not possible.
We can solve this using custom events:
// child/child.js
export default class Child extends LightningComponent {
@api firstName = "";
@api lastName = "";
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
handleClearName() {
this.dispatchEvent(new CustomEvent("clearname"));
}
}
// parent/parent.js
export default class Parent extends LightningComponent {
firstName;
lastName;
handleFirstNameChange(event) {
this.firstName = event.detail.value;
}
handleLastNameChange(event) {
this.lastName = event.detail.value;
}
handleClearName() {
this.firstName = "";
this.lastName = "";
}
}
<!-- parent/parent.html -->
<template>
<div>
<lightning-input type="text" label="First Name" onchange={handleFirstNameChange}></lightning-input>
</div>
<div>
<lightning-input type="text" label="Last Name" onchange={handleLastNameChange}></lightning-input>
</div>
<c-child first-name={firstName} last-name={lastName} onclearname={handleClearName}></c-child>
</template>
Alternatively, we could declare a Message Channel, add a @wire
property, publish a message, and so on.
Now, imagine implementing this in a large-scale application with complex state management requirements - the code becomes increasingly difficult to maintain and implement effectively.
Signals to the rescue!
This is where Signals truly shines! Let's refactor the code to utilize signals:
// parent/signals.js
import { signal } from 'some/signals';
export const firstName = signal();
export const lastName = signal();
// parent/parent.js
import { firstName, lastName } from "./signals";
class Parent extends LightningComponent {
handleFirstNameChange(event) {
firstName.value = event.detail.value;
}
handleLastNameChange(event) {
lastName.value = event.detail.value;
}
}
<!-- parent/parent.html -->
<template>
<div>
<lightning-input type="text" label="First Name" onchange={handleFirstNameChange}></lightning-input>
</div>
<div>
<lightning-input type="text" label="Last Name" onchange={handleLasttNameChange}></lightning-input>
</div>
<c-child></c-child>
</template>
// child/child.js
import { firstName, lastName } from "c/parent/signals";
export default class Child extends LightningComponent {
get fullName() {
return `${firstName.value} ${lastName.value}`;
}
handleClearName() {
firstName.value = null;
lastName.value = null;
}
}
<!-- child/child.html -->
<template>
<div>Full Name: {fullName}</div>
<div>
<lightning-button label="Clear Name" onclick={handleClearName}></lightning-button>
</div>
</template>
That's significantly more straightforward!
In this new implementation, Signals can be shared between components, and the components react automatically when a signal value changes, requiring minimal additional effort.
Can I use signals in my LWC projects?
The answer is both no and yes!
Native Signal support for LWC remains in the conceptual experimental phase and isn't yet available.
However, you can leverage external libraries to implement the Signals concept today.
Introducing lwc-signals!
Due to my enthusiasm for implementing Signals in my projects, I created a custom implementation for LWC.
Github Repo: https://github.com/leandrobrunner/lwc-signals
This library provides a comprehensive Signals implementation inspired by Preact Signals), featuring:
- Computed values
- Effects
- Batch updates
- Deep reactivity
- Manual subscriptions
- Design aligned with Salesforce's signals concept for future compatibility
How it works?
The implementation features a straightforward reactive system.
- Signals & Computed: Notify subscribers when values change
- Effects: Subscribe to signals and run when changes occur
The library includes a WithSignals
Mixin that enables LWC components to react to signal changes.
- WithSignals: Uses an internal effect to track signal dependencies
- Render Process:
- Captures which signals are used
- Reads internal __updateTimestamp property
- __updateTimestamp becomes a dependency
- Updates: Changes to signals trigger timestamp update, causing re-render
Examples
Basic Component
import { LightningElement } from 'lwc';
import { WithSignals, signal } from 'c/signals';
export default class Counter extends WithSignals(LightningElement) {
count = signal(0);
increment() {
this.count.value++;
}
get doubleCount() {
return this.count.value * 2;
}
}
<template>
<div>
<p>Count: {count.value}</p>
<p>Double: {doubleCount}</p>
<button onclick={increment}>Increment</button>
</div>
</template>
Parent-Child Communication
// parent.js
import { LightningElement } from 'lwc';
import { WithSignals, signal } from 'c/signals';
// Signal shared between components
export const parentData = signal('parent data');
export default class Parent extends WithSignals(LightningElement) {
updateData(event) {
parentData.value = event.target.value;
}
}
<!-- parent.html -->
<template>
<div>
<input value={parentData.value} onchange={updateData} />
<c-child></c-child>
</div>
</template>
// child.js
import { LightningElement } from 'lwc';
import { WithSignals } from 'c/signals';
import { parentData } from './parent';
export default class Child extends WithSignals(LightningElement) {
// Use the shared signal directly
get message() {
return parentData.value;
}
}
<!-- child.html -->
<template>
<div>
Message from parent: {message}
</div>
</template>
Global State
// store/userStore.js
import { signal, computed } from 'c/signals';
export const user = signal({
name: 'John',
theme: 'light'
});
export const isAdmin = computed(() => user.value.role === 'admin');
export const updateTheme = (theme) => {
user.value.theme = theme;
};
// header.js
import { LightningElement } from 'lwc';
import { WithSignals } from 'c/signals';
import { user, updateTheme } from './store/userStore';
export default class Header extends WithSignals(LightningElement) {
// You can access global signals directly in the template
get userName() {
return user.value.name;
}
get theme() {
return user.value.theme;
}
toggleTheme() {
updateTheme(this.theme === 'light' ? 'dark' : 'light');
}
}
// settings.js
import { LightningElement } from 'lwc';
import { WithSignals } from 'c/signals';
import { user, isAdmin } from './store/userStore';
export default class Settings extends WithSignals(LightningElement) {
// Global signals and computed values can be used anywhere
get showAdminPanel() {
return isAdmin.value;
}
updateName(event) {
user.value.name = event.target.value;
}
}
Deep Reactivity
const user = signal({
name: 'John',
settings: { theme: 'dark' }
});
// Direct property mutations work!
user.value.settings.theme = 'light';
const list = signal([]);
// Array methods are fully reactive
list.value.push('item');
list.value.unshift('first');
list.value[1] = 'updated';
Summary
Signals provide a powerful and elegant solution for state management in LWC, simplifying component communication and reducing boilerplate code. While we await native support from Salesforce, the lwc-signals library brings this functionality to your projects today.
The project is available on Github and is open for contributions and feedback.
Happy coding!
Author Of article : Leandro Brunner Read full article