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.

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