Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: signals in Oryx #1989

Merged
merged 32 commits into from
Aug 15, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
bfef562
signals
dunqan Jul 10, 2023
4b98e40
updates
dunqan Jul 10, 2023
88318bf
Merge remote-tracking branch 'origin/master' into docs/oryx-signals
dunqan Jul 10, 2023
09ea18c
CR fixes
dunqan Jul 10, 2023
e60c009
CR fixes 2
dunqan Jul 11, 2023
ca1b85b
CR fixes 2
dunqan Jul 11, 2023
6ee1c8c
CR fixes 2
dunqan Jul 11, 2023
ab91ffe
review
Jul 11, 2023
16d376a
Update docs/scos/dev/front-end-development/202212.0/oryx/reactivity/s…
dunqan Jul 11, 2023
1892c2f
Update docs/scos/dev/front-end-development/202212.0/oryx/reactivity/s…
dunqan Jul 11, 2023
f474f76
Update docs/scos/dev/front-end-development/202212.0/oryx/reactivity/s…
dunqan Jul 11, 2023
1316320
Update docs/scos/dev/front-end-development/202212.0/oryx/reactivity/s…
dunqan Jul 11, 2023
1281fba
Update docs/scos/dev/front-end-development/202212.0/oryx/reactivity/s…
dunqan Jul 11, 2023
60f5aa5
Update docs/scos/dev/front-end-development/202212.0/oryx/reactivity/s…
dunqan Jul 11, 2023
ac8113a
Update docs/scos/dev/front-end-development/202212.0/oryx/reactivity/s…
dunqan Jul 11, 2023
ecaac73
Update docs/scos/dev/front-end-development/202212.0/oryx/reactivity/s…
dunqan Jul 11, 2023
50d4cab
Update docs/scos/dev/front-end-development/202212.0/oryx/reactivity/s…
dunqan Jul 11, 2023
ed07cce
Update docs/scos/dev/front-end-development/202212.0/oryx/reactivity/s…
dunqan Jul 11, 2023
f8902f4
Update docs/scos/dev/front-end-development/202212.0/oryx/reactivity/s…
dunqan Jul 11, 2023
8761e3a
Update docs/scos/dev/front-end-development/202212.0/oryx/reactivity/s…
dunqan Jul 11, 2023
2776488
TW CR fixes 3
dunqan Jul 11, 2023
e6510ed
TW CR fixes 4
dunqan Jul 13, 2023
193e78a
Update signals.md
andriitserkovnyi Jul 24, 2023
510f481
review
andriitserkovnyi Jul 24, 2023
14ea221
review
andriitserkovnyi Jul 25, 2023
853d8b7
small improvements
tobi-or-not-tobi Jul 25, 2023
a0f0782
Update signals.md
andriitserkovnyi Jul 25, 2023
f8df1fb
Update signals.md
andriitserkovnyi Aug 14, 2023
16dd513
Merge branch 'master' into docs/oryx-signals
andriitserkovnyi Aug 14, 2023
2462a79
sidebar
andriitserkovnyi Aug 14, 2023
94caf7b
Merge branch 'master' into docs/oryx-signals
Aug 14, 2023
e3ee9c4
Merge branch 'master' into docs/oryx-signals
andriitserkovnyi Aug 15, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: Key concepts of Reactivity
description: Understanding Reactivity concepts will help you understand how Oryx works
template: concept-topic-template
last_updated: Apr 3, 2023
last_updated: Jul 11, 2023
---

## Reactive data streams
Expand Down Expand Up @@ -61,7 +61,7 @@ While observables and RxJS operators provide a great setup for an in-memory reac

Oryx has a standardized library of web components and uses [Lit](https://lit.dev) to develop those components. Lit can update only the mutable parts of the components, and maintains the static parts unchanged. This results in a highly efficient rendering performance.

Oryx offers the `@asyncState` decorator for Lit components, which simplifies the use of reactive streams and reduces code complexity. However, if you are integrating Oryx into a different web framework, use the reactive patterns of that particular framework.
Oryx offers signals implementation and @signalAware decorator for Lit components, which simplifies the use of reactive streams and reduces code complexity. However, if you are integrating Oryx into a different web framework, use the reactive patterns of that particular framework.

## Next steps

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: Reactive components
description: Reactive components are built with Lit
template: concept-topic-template
last_updated: Apr 3, 2023
last_updated: Jul 11, 2023
---

Components are organized by domains-for example, components in a product domain. They can leverage domain logic to communicate with the associated backend API. Each domain is shipped with a domain service that provides an API to communicate with a backend API. For example, when rendering product data, `ProductService` can be used as follows:
Expand Down Expand Up @@ -45,30 +45,33 @@ While the user navigates thorugh a single page application, it is crucial for co

RxJS operates on data streams and updates them in memory, but it doesn't synchronize this to the UI automatically. Each JavaScript framework ships its own opinionated method to update the DOM. The selected method contributes significantly to the performance and user experience of the application.

The components provided in the Oryx libraries are built with Lit. Lit provides a highly efficient system to only synchronize the minimum required updates to the DOM. When updates are loaded asynchronously, the UI needs to be updated every time new data is emitted. To make this as transparent as possible, the `@asyncState()` decorator is used in the UI components. Under the hood, the decorator uses `AsyncStateController` which requests updates to the view when needed.
The components provided in the Oryx libraries are built with Lit. Lit provides a highly efficient system to only synchronize the minimum required updates to the DOM. When updates are loaded asynchronously, the UI needs to be updated every time new data is emitted. To make this as transparent as possible, the `@signalAware()` decorator is used in the UI components, m allowing for the use of `signals` when building UI, which automatically requests updates to the view when needed.

The following example shows the usage of the `asyncState` decorator. The decorator subscribes to the assigned observable and requests an update to the component when needed. This means that component developers do not need to worry about how the reactive system works under the hood.

Oryx components are built in TypeScript, and we provide types everywhere to increase the developer experience and avoid errors upfront. The original type of the assigned observable needs to be adjusted. It's impossible to resolve a correct type from the observable by using a decorator, which is why the `valueType` function is used to resolve the observed type.
The following example shows the usage of the `computed` signal. `Computed` wraps observable into `signal` that subscribes to the underlying observable automatically and triggers updating the view. This means that component developers do not need to worry about how the reactive system works under the hood.

```ts
export class ProductPriceComponent {
@asyncState()
protected prices = valueType(
this.productController
.getProduct()
.pipe(switchMap((product) => this.formatPrices(product?.price)))
);
protected $prices = computed(() => {
return this.formatPrices(this.$product()?.price);
});

protected override render(): TemplateResult | void {
return html`${this.prices.defaultPrice}`;
return html`${this.$prices().defaultPrice}`;
}
}
```

## Multiple data streams

Components often use multiple data streams. For example, the product price component renders a product price in a certain currency and a _local_ price format. The currency and locale are part of the application context and may change during the application's lifecycle. The product price changes from product to product. RxJS operators combine the various streams. They can combine multiple observables and operate on the combined results.
Components often use multiple data streams. For example, the product price component renders a product price in a certain currency and a _local_ price format. The currency and locale are part of the application context and may change during the application's lifecycle. The product price changes from product to product. For managing these streams, you can leverage signals, which have the ability to combine multiple observables and operate on the combined results.


{% info_block infoBox "Note" %}

This guide emphasizes the use of signals; however, if you are experienced with RxJS, its operators can be applied to manage more complex data stream operations.

{% endinfo_block %}


In the following example, `ProductPriceComponent` observes the product data from `ProductService` and _combines_ it with the formatted price given by `PriceService`.

Expand All @@ -77,23 +80,20 @@ export class ProductPriceComponent {
protected productService = resolve(ProductService);
protected priceService = resolve(PriceService);

protected prices = this.productService
.getProduct()
.pipe(switchMap((product) => this.formatPrices(product?.price)));
protected $product = signal(this.productService.getProduct());

protected $prices = computed(() => this.formatPrices(this.$product()?.price));

protected formatPrices(
price?: ProductPrices
): Observable<{ originalPrice: string | null; salesPrice: string | null }> {
const salesPrice = this.priceService.format(price?.defaultPrice);
const originalPrice = this.priceService.format(price?.originalPrice);
return combineLatest({ salesPrice, originalPrice });
protected formatPrices(price?: ProductPrices): Observable<Prices> {
return combineLatest({
sales: this.pricingService.format(price?.defaultPrice),
original: this.pricingService.format(price?.originalPrice),
});
}
}
```

**Note:** The example is simplified to focus on the RxJs part.

In this example, the product data is observed from `ProductService`, but switches to the price formatting logic. This means that, whenever the product and its route change, new product data is emitted and formatted. `PriceService` is used to format both the sales and original prices. `PriceService.format()` uses the current currency and locale for the formatting, which is why it also exposes an observable. Since there are two prices involved, the two streams are _combined_ in an object.
In this example, the product data is observed from `ProductService`, by creating a `$product` signal. This means that, whenever the product and its route change, new product data is emitted and formatted. `PriceService` is used to format both the sales and original prices. `PriceService.format()` uses the current currency and locale for the formatting, which is why it also exposes an observable. Since there are two prices involved, the two streams are _combined_ in an object, and exposed as a computed signal to the component.

## Next steps

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Oryx Framework - Signals
dunqan marked this conversation as resolved.
Show resolved Hide resolved

## Overview
dunqan marked this conversation as resolved.
Show resolved Hide resolved

Signals are a crucial tool within the Oryx framework, offering a clean and efficient reactivity API for components. They allow for the declarative formulation of component logic while seamlessly integrating with observables from domain services.

## Implementation of Signals in Oryx

The Oryx implementation of signals has a core mechanism and a simplified API. The core is well-suited for advanced usage, while the simplified API is sufficiently robust for most components. This documentation will focus on the simplified API.
dunqan marked this conversation as resolved.
Show resolved Hide resolved

### Creating Signals
dunqan marked this conversation as resolved.
Show resolved Hide resolved

To create a signal, use the `signal()` function. This function can either take a raw value or accept an observable.

Here's an example of creating a simple signal:

```ts
const counter = signal(1);
```

Changing the signal value:

```ts
counter.set(2);
```

Creating a signal from an observable:

```ts
const values = signal(observable$);
```

You can initialize signals with options to adjust their behavior:

- `equal` function: This option allows for a custom equality function between two consecutive signal values. By default, strict comparison is used. Implementing your own function gives control over when a signal updates, avoiding unnecessary updates and performance costs when new and old values are practically identical.
dunqan marked this conversation as resolved.
Show resolved Hide resolved

- `initialValue` option: This is used when creating a signal from an observable. It sets the first value of the signal, so you don't have to wait for the observable to give a value.
dunqan marked this conversation as resolved.
Show resolved Hide resolved

Here's an example of using options:

```ts
const values = signal(observable$, { initialValue: 1, equal: (a, b) => a === b });
```

### Computed Signals
dunqan marked this conversation as resolved.
Show resolved Hide resolved

A computed signal derives its value from other signals. It automatically reevaluates its value when any of the signals it depends on changes.

Here's an example of a computed signal:

```ts
const counter = computed(() => 3 * counter(1));
```

Computed signals can also convert observables to signals transparently:

```ts
const counter = computed(() => productService.get({ sku: productSku() }));
```

In the example above, `productSku` is a signal, and `productService.get` returns an observable.
dunqan marked this conversation as resolved.
Show resolved Hide resolved

Computed signals can use the same set of options as regular signals.

### Effects

Effects are functions that run whenever a signal's value changes.

Here's an example:

```ts
const counter = effect(() => {
console.log('counter changed', counter());
});
```

You can configure effects using options to modify their behavior:

- `defer`: If this is set to true, your effect won't run until you explicitly call the `start()` method.
dunqan marked this conversation as resolved.
Show resolved Hide resolved
- `async`: Set this to true if you want your effect to run asynchronously.
dunqan marked this conversation as resolved.
Show resolved Hide resolved

Example:

```ts
const counter = effect(() => {
console.log('counter changed', counter());
}, { defer: true, async: true });
```

## Using Signals in Components
dunqan marked this conversation as resolved.
Show resolved Hide resolved

### @signalAware directive
VadymSachenko marked this conversation as resolved.
Show resolved Hide resolved

The `@signalAware` decorator provides additional functionality when using signals in components.

```ts
@signalAware()
class MyComponent extends LitElement {}
```

This decorator is required to make your component work with signals as expected.
When used, the component will automatically detect signals and render changes whenever a signal alters. It does this intelligently, considering only the signals relevant to the last render.

Some Oryx domain components are not using this decorator directly, as it is already applied to some common domain mixins (eg. `ContentMixin`, `ProductMixin`, etc.).

### `@elementEffect` Directive
dunqan marked this conversation as resolved.
Show resolved Hide resolved

The `@elementEffect` directive integrates effects with component lifecycles for seamless management. It activates an effect when a component is connected to the DOM and deactivates it once the component is disconnected.

```ts
class MyComponent extends LitElement {
/* ... */
@elementEffect()
logProductCode = () => console.log('Product code ', this.$product().code);
}
```

In the above example, the `logProductCode` effect will automatically start when `MyComponent` is connected to the DOM, logging the product code everytime $product signal will update. Once the component is disconnected from the DOM, the effect will automatically stop. This directive simplifies effect management by automatically linking them to component lifecycles, making your component code cleaner and easier to manage.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, avoid future tenses as much as possible. Also, this sentence looks unclear, so consider rephrasing or splitting it into two smaller ones: "In the preceding example, the logProductCode effect automatically starts when MyComponent is connected to the DOM, logging the product code every time, and the $product signal updates."

Suggested change
In the above example, the `logProductCode` effect will automatically start when `MyComponent` is connected to the DOM, logging the product code everytime $product signal will update. Once the component is disconnected from the DOM, the effect will automatically stop. This directive simplifies effect management by automatically linking them to component lifecycles, making your component code cleaner and easier to manage.
In the above example, the `logProductCode` effect starts automatically when `MyComponent` is connected to the DOM, logging the product code every time the $product signal updates. Once the component is disconnected from the DOM, the effect automatically stops. This directive simplifies effect management by automatically linking them to component lifecycles, making your component code cleaner and easier to manage.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

improved