Skip to content

Commit

Permalink
Deprecating $reactTo
Browse files Browse the repository at this point in the history
  • Loading branch information
cesarParra committed May 24, 2024
1 parent 9e91376 commit a6231fd
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 204 deletions.
85 changes: 7 additions & 78 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ This can be cumbersome when you have a lot of components that need to share stat
- You have to make sure manage subscriptions and unsubscriptions to events

An alternative is to use the `wire` service to get the data from the server and let the framework handle the caching
for you, but this only works for data that is signalsd in the server, and still forces you to implement a lot of
for you, but this only works for data that is stored in the server, and still forces you to implement a lot of
boilerplate code to manage each wire adapter for each component.

`LWC Signals` provides a simple way to create reactive data signalss that can be used to share state between components
`LWC Signals` provides a simple way to create reactive data signals that can be used to share state between components
without the need to broadcast messages or manage subscriptions and wires.

## Creating a signals
Expand Down Expand Up @@ -104,76 +104,9 @@ export default class Counter extends LightningElement {

## Reacting to changes

### `$reactTo`

To have your components automatically react to changes in the signals, you can use the `$reactTo`
function to create a reactive value that will update whenever the signals changes.

Let's create another component that displays the counter value and automatically updates when the counter changes.

```html
<!-- display.html -->
<template>
<p>The current count is: {counter.value}</p>
</template>
```

```javascript
// display.js
import { LightningElement } from "lwc";
import { $reactTo } from "c/signals";
import { counter } from "c/counter-signals";

export default class Display extends LightningElement {
get counter() {
return $reactTo(counter);
}
}
```

> `$reactTo` should be used inside a getter to make sure that the UI updates when the value changes.
> Keep reading to see other ways to react to changes in the signals.
<div style="text-align: center;">
<img src="./doc-assets/counter-example.gif" alt="Counter Example" />
</div>

---

### `$computed`

You can also use the `$computed` function to create a reactive value that depends on the signals.
The difference between `$reactTo` and `$computed` is that `$computed` allows you return a derived computed signals (with
the difference of it being read only)
from the original, or multiple signals.

```javascript
// display.js
import { LightningElement } from "lwc";
import { $computed } from "c/signals";
import { counter } from "c/counter-signals";

export default class Display extends LightningElement {
get counterMultiplied() {
return $computed(() => counter.value * 2).value;
}
}
```

---

Notice that in the examples we have been using getters to react to value changes. This is because LWC's reactive system
can automatically detect changes in getters for simple values and updates the UI accordingly, which makes for a cleaner
developer experience
and easier to reason about the code.

But there are cases where we need to use a property in case of a getter, for example when computing values into a
complex object, in which case the LWC
framework won't update the UI automatically. For cases like this, you can leverage the
`$computed` function to create a reactive property that will update whenever the signals changes.

> See the (Reacting to multiple signals)[#reacting-to-multiple-signals] section for an example where we need
> to use a property instead of a getter.
You the `$computed` function to create a reactive value that depends on the signals.

```javascript
// display.js
Expand Down Expand Up @@ -247,17 +180,13 @@ export const contactName = $signal("John Doe");
```javascript
// contactInfoForm.js
import { LightningElement } from "lwc";
import { $reactTo } from "c/signals";
import { $computed } from "c/signals";
import { accountName, contactName } from "c/demoSignalss";

export default class ContactInfoForm extends LightningElement {
get accountName() {
return $reactTo(accountName);
}
accountName = $computed(() => (this.accountName = accountName.value)).value;

get contactName() {
return $reactTo(contactName);
}
contactName = $computed(() => (this.contactName = contactName.value)).value;

handleAccountNameChange(event) {
accountName.value = event.target.value;
Expand Down Expand Up @@ -519,7 +448,7 @@ export default class AccountPicker extends LightningElement {

Notice how we are using a `@wire` service to fetch the accounts from the server and populate the picklist. This is
because in this case we don't care about sharing that data with other components, and we only need it once. Be
pragmatic about when to use signalss and when not to. Opt to use the base Salesforce services when you only need the
pragmatic about when to use signals and when not to. Opt to use the base Salesforce services when you only need the
data
in a single component.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import { LightningElement } from "lwc";
import { $reactTo } from "c/signals";
import { $computed } from "c/signals";
import { accountName, contactName } from "c/demoSignals";

export default class ContactInfoForm extends LightningElement {
get accountName() {
return $reactTo(accountName);
}

get contactName() {
return $reactTo(contactName);
}
accountName = $computed(() => (this.accountName = accountName.value)).value;
contactName = $computed(() => (this.contactName = contactName.value)).value;

handleAccountNameChange(event) {
accountName.value = event.target.value;
Expand Down
2 changes: 0 additions & 2 deletions examples/counter/lwc/countTracker/countTracker.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
<template>
The current count is ($reactTo): {currentCount} <br />
The current count is ($computed reactive property): {reactiveProperty} <br />
The current multiplied count is ($computed): {counterMultiplied} <br />
The counter plus two value is (nested computed): {counterPlusTwo}
</template>
10 changes: 1 addition & 9 deletions examples/counter/lwc/countTracker/countTracker.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,11 @@
import { LightningElement } from "lwc";
import { $computed, $reactTo } from "c/signals";
import { $computed } from "c/signals";
import { counter, counterPlusTwo } from "c/demoSignals";

export default class CountTracker extends LightningElement {
get currentCount() {
return $reactTo(counter);
}

reactiveProperty = $computed(() => (this.reactiveProperty = counter.value))
.value;

get counterMultiplied() {
return $computed(() => counter.value * 2).value;
}

counterPlusTwo = $computed(() => (this.counterPlusTwo = counterPlusTwo.value))
.value;
}
182 changes: 86 additions & 96 deletions force-app/lwc/signals/core.js
Original file line number Diff line number Diff line change
@@ -1,111 +1,101 @@
const context = [];
function _getCurrentObserver() {
return context[context.length - 1];
return context[context.length - 1];
}
function $effect(fn) {
const execute = () => {
context.push(execute);
try {
fn();
}
finally {
context.pop();
}
};
execute();
const execute = () => {
context.push(execute);
try {
fn();
} finally {
context.pop();
}
};
execute();
}
function $computed(fn) {
const computedSignal = $signal(fn());
$effect(() => {
computedSignal.value = fn();
});
return computedSignal.readOnly;
}
function $reactTo(signal) {
let _value = signal.value;
$effect(() => {
_value = signal.value;
});
return _value;
const computedSignal = $signal(fn());
$effect(() => {
computedSignal.value = fn();
});
return computedSignal.readOnly;
}
function $signal(value) {
let _value = value;
const subscribers = new Set();
function getter() {
const current = _getCurrentObserver();
if (current) {
subscribers.add(current);
}
return _value;
let _value = value;
const subscribers = new Set();
function getter() {
const current = _getCurrentObserver();
if (current) {
subscribers.add(current);
}
function setter(newValue) {
_value = newValue;
for (const subscriber of subscribers) {
subscriber();
}
return _value;
}
function setter(newValue) {
_value = newValue;
for (const subscriber of subscribers) {
subscriber();
}
return {
get value() {
return getter();
},
set value(newValue) {
setter(newValue);
},
readOnly: {
get value() {
return getter();
}
}
};
}
return {
get value() {
return getter();
},
set value(newValue) {
setter(newValue);
},
readOnly: {
get value() {
return getter();
}
}
};
}
function $resource(fn, source, options) {
function loadingState(data) {
return {
data: data,
loading: true,
error: null
};
}
let _isInitialLoad = true;
let _value = options?.initialValue ?? null;
let _previousParams;
const _signal = $signal(loadingState(_value));
const execute = async () => {
_signal.value = loadingState(_value);
const derivedSource = source instanceof Function ? source() : source;
if (!_isInitialLoad && derivedSource === _previousParams) {
// No need to fetch the data again if the params haven't changed
return;
}
try {
const data = await fn(derivedSource);
// Keep track of the previous value
_value = data;
_signal.value = {
data,
loading: false,
error: null
};
}
catch (error) {
_signal.value = {
data: null,
loading: false,
error
};
}
finally {
_previousParams = derivedSource;
_isInitialLoad = false;
}
};
$effect(execute);
function loadingState(data) {
return {
data: _signal.readOnly,
refetch: async () => {
_isInitialLoad = true;
await execute();
}
data: data,
loading: true,
error: null
};
}
let _isInitialLoad = true;
let _value = options?.initialValue ?? null;
let _previousParams;
const _signal = $signal(loadingState(_value));
const execute = async () => {
_signal.value = loadingState(_value);
const derivedSource = source instanceof Function ? source() : source;
if (!_isInitialLoad && derivedSource === _previousParams) {
// No need to fetch the data again if the params haven't changed
return;
}
try {
const data = await fn(derivedSource);
// Keep track of the previous value
_value = data;
_signal.value = {
data,
loading: false,
error: null
};
} catch (error) {
_signal.value = {
data: null,
loading: false,
error
};
} finally {
_previousParams = derivedSource;
_isInitialLoad = false;
}
};
$effect(execute);
return {
data: _signal.readOnly,
refetch: async () => {
_isInitialLoad = true;
await execute();
}
};
}
export { $signal, $effect, $computed, $reactTo, $resource };
export { $signal, $effect, $computed, $resource };
13 changes: 2 additions & 11 deletions src/lwc/signals/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ function _getCurrentObserver(): VoidFunction | undefined {
return context[context.length - 1];
}

function $effect(fn: VoidFunction): void {
function $effect(fn: VoidFunction): void {121
const execute = () => {
context.push(execute);
try {
Expand All @@ -39,15 +39,6 @@ function $computed<T>(fn: ComputedFunction<T>): ReadOnlySignal<T> {
return computedSignal.readOnly;
}

function $reactTo<T>(signal: Signal<T>): T {
let _value: T = signal.value;
$effect(() => {
_value = signal.value;
});

return _value;
}

function $signal<T>(value: T): Signal<T> {
let _value: T = value;
const subscribers: Set<VoidFunction> = new Set();
Expand Down Expand Up @@ -162,4 +153,4 @@ function $resource<T>(
};
}

export { $signal, $effect, $computed, $reactTo, $resource };
export { $signal, $effect, $computed, $resource };

0 comments on commit a6231fd

Please sign in to comment.