A lightweight reactive state management library for Salesforce Lightning Web Components.

- 🚀 Fine-grained reactivity
- 📦 Zero dependencies
- 🔄 Deep reactivity for objects and collections
- 📊 Computed values with smart caching
- 🎭 Batch updates for performance
- ⚡ Small and efficient
This library brings the power of signals to Salesforce Lightning Web Components today. While Salesforce has conceptualized signals as a future feature for LWC, it's currently just a concept and not available for use.
This library provides:
- Complete signals implementation
- Rich feature set beyond basic signals:
- Computed values
- Effects
- Batch updates
- Deep reactivity
- Manual subscriptions
- Design aligned with Salesforce's signals concept for future compatibility
Inspired by:
- Preact Signals - Fine-grained reactivity system
- Salesforce's signals concept and API design principles
Production / Dev:
https://login.salesforce.com/packaging/installPackage.apexp?p0=04tbm0000006D7RAAU
Sandbox / Scratch:
https://test.salesforce.com/packaging/installPackage.apexp?p0=04tbm0000006D7RAAU
You can also install using the SF CLI:
sf package install --package "lwc-signals@1.1.0-1"
Installation from NPM
In your project folder, run:
npm install --save lwc-signals
After installation, link the LWC component from node_modules
into your Salesforce project so it’s available as a standard Lightning Web Component.
Run:
ln -s ../../../../node_modules/lwc-signals/dist/signals ./force-app/main/default/lwc/signals
Option A: Using Command Prompt (run as Administrator)
mklink /D "force-app\main\default\lwc\signals" "..\..\..\..\node_modules\lwc-signals\dist\signals"
Option B: Using PowerShell
New-Item -ItemType SymbolicLink -Path "force-app\main\default\lwc\signals" -Target "..\..\..\..\node_modules\lwc-signals\dist\signals"
Note: If you are not running as Administrator, enable Developer Mode on Windows to allow symlink creation.
const name = signal('John');
console.log(name.value); // Get value: 'John'
name.value = 'Jane'; // Set value: triggers updates
const firstName = signal('John');
const lastName = signal('Doe');
// Updates whenever firstName or lastName changes
const fullName = computed(() => `${firstName.value} ${lastName.value}`);
console.log(fullName.value); // 'John Doe'
effect(() => {
// This runs automatically when name.value changes
console.log(`Name changed to: ${name.value}`);
// Optional cleanup function
return () => {
// Cleanup code here
};
});
const counter = signal(0);
// Subscribe to changes
const unsubscribe = counter.subscribe(() => {
console.log('Counter changed:', counter.value);
});
counter.value = 1; // Logs: "Counter changed: 1"
// Stop listening to changes
unsubscribe();
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.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>
// 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;
}
}
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';
import { LightningElement } from 'lwc';
import { WithSignals, effect } from 'c/signals';
export default class Component extends WithSignals(LightningElement) {
connectedCallback() {
effect(() => {
console.log("Effect created.");
return () => {
console.log("Effect disposed."); // Automatically called when the component is disconnected
}
})
}
}
For components using the WithSignals
mixin, it's crucial to maintain proper lifecycle behavior by following specific requirements.
Here's what you need to know:
- constructor:
Always call
super()
as the first statement in your constructor. This ensures proper initialization of both theLightningElement
base class and signals functionality. - render:
You must call
super.__triggerSignals()
before returning your template. This method ensures that all signal updates are properly processed before the component renders. - renderedCallback:
When overriding
renderedCallback()
, always includesuper.renderedCallback()
. This maintains the parent class's rendering lifecycle behavior while adding your custom logic. - disconnectedCallback:
Include
super.disconnectedCallback()
when implementingdisconnectedCallback()
. This ensures proper cleanup of signal subscriptions, effects and prevents memory leaks.
import { LightningElement } from 'lwc';
import template from "./template.html";
import { WithSignals } from 'c/signals';
export default class Component extends WithSignals(LightningElement) {
constructor() {
super(); // Required: Initialize parent class
}
render() {
super.__triggerSignals(); // Required: Process signal updates
return template;
}
renderedCallback() {
super.renderedCallback(); // Required: Maintain parent lifecycle
// Your custom logic here
}
disconnectedCallback() {
super.disconnectedCallback(); // Required: Clean up signals and effects
// Your cleanup code here
}
}
MIT © Leandro Brunner