Skip to content

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

Notifications You must be signed in to change notification settings

leandrobrunner/lwc-signals

Repository files navigation

LWC Signals

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

Deploy to Salesforce

Features

  • 🚀 Fine-grained reactivity
  • 📦 Zero dependencies
  • 🔄 Deep reactivity for objects and collections
  • 📊 Computed values with smart caching
  • 🎭 Batch updates for performance
  • ⚡ Small and efficient

About This Library

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

Unlocked Package

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

Step 1: Install the Package

In your project folder, run:

npm install --save lwc-signals

Step 2: Link the Component to Your Salesforce Project

After installation, link the LWC component from node_modules into your Salesforce project so it’s available as a standard Lightning Web Component.

On macOS / Linux

Run:

ln -s ../../../../node_modules/lwc-signals/dist/signals ./force-app/main/default/lwc/signals

On Windows

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.

Core Concepts

Signals

const name = signal('John');
console.log(name.value);  // Get value: 'John'
name.value = 'Jane';      // Set value: triggers updates

Computed Values

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'

Effects

effect(() => {
    // This runs automatically when name.value changes
    console.log(`Name changed to: ${name.value}`);
    
    // Optional cleanup function
    return () => {
        // Cleanup code here
    };
});

Manual Subscriptions

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();

Usage

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';

Effects auto-dispose

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
            }
        })
    }
}

Considerations

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:

  1. constructor: Always call super() as the first statement in your constructor. This ensures proper initialization of both the LightningElement base class and signals functionality.
  2. render: You must call super.__triggerSignals() before returning your template. This method ensures that all signal updates are properly processed before the component renders.
  3. renderedCallback: When overriding renderedCallback(), always include super.renderedCallback(). This maintains the parent class's rendering lifecycle behavior while adding your custom logic.
  4. disconnectedCallback: Include super.disconnectedCallback() when implementing disconnectedCallback(). 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
    }
}

Documentation

License

MIT © Leandro Brunner

About

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

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Sponsor this project

Packages

No packages published