1.1.0 β€’ Published 10 months ago

lwc-signals v1.1.0

Weekly downloads
-
License
MIT
Repository
github
Last release
10 months ago

LWC Signals

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

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

1.1.0

10 months ago

1.0.7

10 months ago

1.0.6

10 months ago

1.0.5

10 months ago

1.0.4

10 months ago

1.0.3

10 months ago

1.0.2

10 months ago

1.0.1

10 months ago

1.0.0

10 months ago