An open API service indexing awesome lists of open source software.

https://github.com/leandrobrunner/lwc-signals

A lightweight reactive state management library for Salesforce Lightning Web Components.
https://github.com/leandrobrunner/lwc-signals

lightning-web-components lwc lwc-component lwc-opensource salesforce salesforce-developers signals

Last synced: 3 months ago
JSON representation

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

Awesome Lists containing this project

README

          

# 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](https://www.npmjs.com/package/@lwc/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](https://preactjs.com/guide/v10/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=04tbm000000LkUTAA0
```

Sandbox / Scratch:
```
https://test.salesforce.com/packaging/installPackage.apexp?p0=04tbm000000LkUTAA0
```

You can also install using the SF CLI:
```bash
sf package install --package "04tbm000000LkUTAA0"
```

## Installation from [NPM](https://www.npmjs.com/package/lwc-signals)

### 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
```javascript
const name = signal('John');
console.log(name.value); // Get value: 'John'
name.value = 'Jane'; // Set value: triggers updates
```

### Computed Values
```javascript
const counter = signal(5);
// Updates when counter changes
const double = computed(() => counter.value * 2);

console.log(double.value); // 10

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
```javascript
effect(() => {
// This runs automatically when name.value changes
console.log(`Name changed to: ${name.value}`);

// Optional cleanup function
return () => {
// Cleanup code here
};
});
```

### Manual Subscriptions
```javascript
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
```javascript
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;
}
}
```

```html


Count: {count.value}


Double: {doubleCount}


Increment

```

### Parent-Child Communication
```javascript
// 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;
}
}
```

```html




```

```javascript
// 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;
}
}
```

```html


Message from parent: {message}

```

### Global State
```javascript
// 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;
};
```

```javascript
// 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');
}
}
```

```javascript
// 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
```javascript
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
```javascript
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.

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

- [Architecture and Internal Concepts](./docs/architecture.md)

## License

MIT Β© Leandro Brunner