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
In your project folder, run:
npm install 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.
You can now import and use the library in your Lightning Web Components. See the Usage section of the README for examples.
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';
MIT © Leandro Brunner