From ebfad95f4b49f8e0bdbe84cf779385567734a0f0 Mon Sep 17 00:00:00 2001 From: Mohamad Reza Golab Date: Sun, 21 Jul 2024 16:21:17 +0330 Subject: [PATCH 1/3] fix: article-details.stories init data --- .../article-details.stories.ts | 29 ++++++++++++++++++- .../ui-article-card.stories.ts | 19 +++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/libs/blog/articles/feature-article/src/lib/article-details/article-details.stories.ts b/libs/blog/articles/feature-article/src/lib/article-details/article-details.stories.ts index 4d390b96..ea9d49f3 100644 --- a/libs/blog/articles/feature-article/src/lib/article-details/article-details.stories.ts +++ b/libs/blog/articles/feature-article/src/lib/article-details/article-details.stories.ts @@ -1,12 +1,28 @@ -import type { Meta, StoryObj } from '@storybook/angular'; +import { importProvidersFrom } from '@angular/core'; +import { + applicationConfig, + moduleMetadata, + type Meta, + type StoryObj, +} from '@storybook/angular'; +import { provideI18n } from '@angular-love/blog/i18n/data-access'; import { Article } from '@angular-love/contracts/articles'; +import { ConfigService } from '@angular-love/shared/config'; + +import { SeoData } from '../../../../../../blog-contracts/articles/src/lib/articles'; import { ArticleDetailsComponent } from './article-details.component'; const meta: Meta = { component: ArticleDetailsComponent, title: 'Articles / details', + decorators: [ + applicationConfig({ + // List of providers and environment providers that should be available to the root component and all its children. + providers: [provideI18n(), importProvidersFrom(ConfigService)], + }), + ], }; const articleDetails: Article = { @@ -27,12 +43,23 @@ const articleDetails: Article = { publishDate: new Date('2021-01-01').toISOString(), content: '

After the Angular team shared news about introducing signals in Angular 16, it was all the entire community could talk about. 

\n

With everyone having that level of interest and fascination with this new feature, you might wonder if Angular is moving towards completely removing RxJs from the codebase. However, that is not the case. 

\n

In this article, I will explain why RxJs won’t be replaced by signals and remain a library available to developers. 

\n

The impact of signals

\n

Angular signals are a new reactive primitive introduced to Angular in version 16. Before, we were able to use signals occasionally. For example, in Solid.js. So this concept is well known by Angular developers. You can find more information about signals in this article.

\n

Signals will change our approach to writing code, change detection, and improve application performance. Considering the nature of signals and their synchronous data flow, they have some limitations which are perfectly filled by RxJs.

\n

Signals vs RxJs

\n

RxJs can be asynchronous. What does that mean? RxJs can do many operations in parallel, independently. The perfect example of a situation like that was presented at an angular.love meetup by Maciej Wójcik 

\n

\n

In this case, it is possible that the `data` variable doesn’t have any value when it is assigned to the `someValue` variable. That is because data is still being downloaded, and the subscription doesn’t run yet.

\n

On the other hand, signals are synchronous. That means that they run operations in a specific order. That works great when we use signals to store data or in the template.

\n

Why RxJs is staying

\n

Angular contains an API to convert signals to observable and observable to signal. This looks like a clear statement that we should use both approaches and use their different benefits. 

\n

Here is an example of what  the above-mentioned conversion looks like:

\n
  step = signal(\'create\');\n  step$ = toObservable(this.step);\n  ngOnInit() {\n    this.step$.subscribe((step) => {\n      console.log(step);\n    });\n  }\n\n\n  users$ = of([{ id: 1, name: \'John Smith\' }]);\n  users = toSignal(this.users$);\n
\n

Most of us know the cases where signals are better than RxJs. But when is RxJs better than signals? Let’s take a look.

\n

First, RxJs is better equipped to handle events from different sources, which need to be mapped and filtered. Signals can also handle those events, but they become less readable and more complicated than with RxJs. A significant number of RxJs operators gives us a wide range of possibilities.

\n

A good and classic example where using RxJs is simpler than signals is listening for input change with debounceTime operator. Code written only using RxJs looks like that: 

\n
 this.control.valueChanges.pipe(\n      debounceTime(300),\n    ).subscribe(value => {\n      this.search.emit(value)\n    })\n
\n

Whereas code using  only signals can look like that:

\n
@Component({\nselector: \'app-root\',\ntemplate: `\n<input [ngModel]="searchValue()" (ngModelChange)="searchValue.set($event)">\n`,\nstyleUrls: [\'./app.component.scss\']\n})\nexport class AppComponent {\nsearchValue = signal(\'\');\ndebouncedSearchValue = signal(this.searchValue());\n\n\nconstructor() {\nlet timeoutId: ReturnType<typeof setTimeout> | undefined;\neffect((onCleanup) => {\nconst search = this.searchValue();\n\n\ntimeoutId = setTimeout(() => {\nthis.debouncedSearchValue.set(search);\n}, 500);\n\n\nonCleanup(() => {\nclearTimeout(timeoutId);\n});\n});\n\n\neffect(() => {\nconsole.log(this.debouncedSearchValue());\n});\n}\n}\n\n
\n

As we can see, code that uses RxJs is simpler and more readable for a developer. But we should remember that we are able to write this code with both approaches. Using signals and RxJs together gives us more benefits than using only one approach.

\n

Generally speaking, asynchronous events handling is really challenging to achieve when only using signals. Additionally, RxJs have existed for many years, are used in a significant number of projects, and with this are reliable and well-tested. 

\n

Michael Hladky spoke on Twitter about re-creating and adjusting all RxJs operators to be compatible with signals. He said this is not the most efficient approach.

\n

\n

Network requests are another case when RxJs come in handy. XHR requests are asynchronous. Observable allows us to handle, map, and handle events like success, error, and completion. 

\n

With RxJs operators, we can re-run requests or cancel them, which is impossible with signals. Signals don’t have classic and well-known operators like catchError or switchMap, which are responsible for canceling requests at a specific moment the new one comes with new data. 

\n

Additionally, one of the most significant Angular features is backward compatibility. Completely removing RxJs from the framework will force Angular developers to spend an enormous amount of time on refactoring, something quite challenging to carry out.  In the case of many projects, a change like that will mean rewriting the entire project. 

\n

Summary

\n

Signals are, without a doubt, a revolution when it comes to performance and the approach to creating applications in Angular. Still, RxJs is a powerful tool that helps us handle asynchronous events easily and comfortably. 

\n

From what I’ve seen, I am convinced that signals will never fully replace RxJs. Signals are able to replace only the synchronous parts of RxJs. But it’s best to combine these two approaches when building Angular applications. That will be more beneficial to app performance and the developer experience. 

\n', + id: 0, + slug: '', + otherTranslations: [], + lang: '', + seo: {} as SeoData, }; export default meta; type Story = StoryObj; export const primary: Story = { + decorators: [ + applicationConfig({ + // List of providers and environment providers that should be available to the root component and all its children. + providers: [ConfigService, provideI18n()], + }), + ], args: { articleDetails, }, diff --git a/libs/blog/articles/ui-article-card/src/lib/ui-article-card/ui-article-card.stories.ts b/libs/blog/articles/ui-article-card/src/lib/ui-article-card/ui-article-card.stories.ts index dced1786..bec927f3 100644 --- a/libs/blog/articles/ui-article-card/src/lib/ui-article-card/ui-article-card.stories.ts +++ b/libs/blog/articles/ui-article-card/src/lib/ui-article-card/ui-article-card.stories.ts @@ -1,6 +1,15 @@ -import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; +import { importProvidersFrom } from '@angular/core'; +import { + applicationConfig, + Meta, + moduleMetadata, + StoryObj, +} from '@storybook/angular'; +import { provideI18n } from '@angular-love/blog/i18n/data-access'; +import { AlLocalizeService } from '@angular-love/blog/i18n/util'; import { ArticleCard } from '@angular-love/blog/shared/types'; +import { ConfigService } from '@angular-love/shared/config'; import { ArticleRegularCardComponent } from '../components/article-regular-card/article-regular-card.component'; @@ -36,6 +45,10 @@ const meta: Meta = { moduleMetadata({ imports: [ArticleRegularCardComponent], }), + applicationConfig({ + // List of providers and environment providers that should be available to the root component and all its children. + providers: [importProvidersFrom(ConfigService), provideI18n()], + }), ], }; @@ -48,6 +61,10 @@ export const compact: Story = { cardType: layoutCompact, }, render: (args) => ({ + applicationConfig: { + // The providers will be merged with the ones defined in the applicationConfig decorators providers array of the global meta object + providers: [ConfigService, provideI18n()], + }, props: args, template: `
From eb5eb8f38747fb81be26329f2666cb27e7291fe2 Mon Sep 17 00:00:00 2001 From: Mohamad Reza Golab Date: Tue, 30 Jul 2024 22:57:03 +0330 Subject: [PATCH 2/3] fix: remove unused optional SeoData import --- .../src/lib/article-details/article-details.stories.ts | 7 +------ .../src/lib/ui-article-card/ui-article-card.stories.ts | 3 --- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/libs/blog/articles/feature-article/src/lib/article-details/article-details.stories.ts b/libs/blog/articles/feature-article/src/lib/article-details/article-details.stories.ts index ea9d49f3..ffdd5374 100644 --- a/libs/blog/articles/feature-article/src/lib/article-details/article-details.stories.ts +++ b/libs/blog/articles/feature-article/src/lib/article-details/article-details.stories.ts @@ -1,7 +1,6 @@ import { importProvidersFrom } from '@angular/core'; import { applicationConfig, - moduleMetadata, type Meta, type StoryObj, } from '@storybook/angular'; @@ -10,8 +9,6 @@ import { provideI18n } from '@angular-love/blog/i18n/data-access'; import { Article } from '@angular-love/contracts/articles'; import { ConfigService } from '@angular-love/shared/config'; -import { SeoData } from '../../../../../../blog-contracts/articles/src/lib/articles'; - import { ArticleDetailsComponent } from './article-details.component'; const meta: Meta = { @@ -19,7 +16,6 @@ const meta: Meta = { title: 'Articles / details', decorators: [ applicationConfig({ - // List of providers and environment providers that should be available to the root component and all its children. providers: [provideI18n(), importProvidersFrom(ConfigService)], }), ], @@ -47,7 +43,7 @@ const articleDetails: Article = { slug: '', otherTranslations: [], lang: '', - seo: {} as SeoData, + seo: {}, }; export default meta; @@ -56,7 +52,6 @@ type Story = StoryObj; export const primary: Story = { decorators: [ applicationConfig({ - // List of providers and environment providers that should be available to the root component and all its children. providers: [ConfigService, provideI18n()], }), ], diff --git a/libs/blog/articles/ui-article-card/src/lib/ui-article-card/ui-article-card.stories.ts b/libs/blog/articles/ui-article-card/src/lib/ui-article-card/ui-article-card.stories.ts index bec927f3..792c0b0a 100644 --- a/libs/blog/articles/ui-article-card/src/lib/ui-article-card/ui-article-card.stories.ts +++ b/libs/blog/articles/ui-article-card/src/lib/ui-article-card/ui-article-card.stories.ts @@ -7,7 +7,6 @@ import { } from '@storybook/angular'; import { provideI18n } from '@angular-love/blog/i18n/data-access'; -import { AlLocalizeService } from '@angular-love/blog/i18n/util'; import { ArticleCard } from '@angular-love/blog/shared/types'; import { ConfigService } from '@angular-love/shared/config'; @@ -46,7 +45,6 @@ const meta: Meta = { imports: [ArticleRegularCardComponent], }), applicationConfig({ - // List of providers and environment providers that should be available to the root component and all its children. providers: [importProvidersFrom(ConfigService), provideI18n()], }), ], @@ -62,7 +60,6 @@ export const compact: Story = { }, render: (args) => ({ applicationConfig: { - // The providers will be merged with the ones defined in the applicationConfig decorators providers array of the global meta object providers: [ConfigService, provideI18n()], }, props: args, From 09f4f49e43298305deea4aa03b22989eba89e8d8 Mon Sep 17 00:00:00 2001 From: Mohamad Reza Golab Date: Tue, 30 Jul 2024 23:00:08 +0330 Subject: [PATCH 3/3] Merge pull request #1 from timsar2/fix-article-story-init-data fix: article-details.stories init data --- .../article-details.stories.ts | 24 ++++++++++++++++++- .../ui-article-card.stories.ts | 16 ++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/libs/blog/articles/feature-article/src/lib/article-details/article-details.stories.ts b/libs/blog/articles/feature-article/src/lib/article-details/article-details.stories.ts index 4d390b96..ffdd5374 100644 --- a/libs/blog/articles/feature-article/src/lib/article-details/article-details.stories.ts +++ b/libs/blog/articles/feature-article/src/lib/article-details/article-details.stories.ts @@ -1,12 +1,24 @@ -import type { Meta, StoryObj } from '@storybook/angular'; +import { importProvidersFrom } from '@angular/core'; +import { + applicationConfig, + type Meta, + type StoryObj, +} from '@storybook/angular'; +import { provideI18n } from '@angular-love/blog/i18n/data-access'; import { Article } from '@angular-love/contracts/articles'; +import { ConfigService } from '@angular-love/shared/config'; import { ArticleDetailsComponent } from './article-details.component'; const meta: Meta = { component: ArticleDetailsComponent, title: 'Articles / details', + decorators: [ + applicationConfig({ + providers: [provideI18n(), importProvidersFrom(ConfigService)], + }), + ], }; const articleDetails: Article = { @@ -27,12 +39,22 @@ const articleDetails: Article = { publishDate: new Date('2021-01-01').toISOString(), content: '

After the Angular team shared news about introducing signals in Angular 16, it was all the entire community could talk about. 

\n

With everyone having that level of interest and fascination with this new feature, you might wonder if Angular is moving towards completely removing RxJs from the codebase. However, that is not the case. 

\n

In this article, I will explain why RxJs won’t be replaced by signals and remain a library available to developers. 

\n

The impact of signals

\n

Angular signals are a new reactive primitive introduced to Angular in version 16. Before, we were able to use signals occasionally. For example, in Solid.js. So this concept is well known by Angular developers. You can find more information about signals in this article.

\n

Signals will change our approach to writing code, change detection, and improve application performance. Considering the nature of signals and their synchronous data flow, they have some limitations which are perfectly filled by RxJs.

\n

Signals vs RxJs

\n

RxJs can be asynchronous. What does that mean? RxJs can do many operations in parallel, independently. The perfect example of a situation like that was presented at an angular.love meetup by Maciej Wójcik 

\n

\n

In this case, it is possible that the `data` variable doesn’t have any value when it is assigned to the `someValue` variable. That is because data is still being downloaded, and the subscription doesn’t run yet.

\n

On the other hand, signals are synchronous. That means that they run operations in a specific order. That works great when we use signals to store data or in the template.

\n

Why RxJs is staying

\n

Angular contains an API to convert signals to observable and observable to signal. This looks like a clear statement that we should use both approaches and use their different benefits. 

\n

Here is an example of what  the above-mentioned conversion looks like:

\n
  step = signal(\'create\');\n  step$ = toObservable(this.step);\n  ngOnInit() {\n    this.step$.subscribe((step) => {\n      console.log(step);\n    });\n  }\n\n\n  users$ = of([{ id: 1, name: \'John Smith\' }]);\n  users = toSignal(this.users$);\n
\n

Most of us know the cases where signals are better than RxJs. But when is RxJs better than signals? Let’s take a look.

\n

First, RxJs is better equipped to handle events from different sources, which need to be mapped and filtered. Signals can also handle those events, but they become less readable and more complicated than with RxJs. A significant number of RxJs operators gives us a wide range of possibilities.

\n

A good and classic example where using RxJs is simpler than signals is listening for input change with debounceTime operator. Code written only using RxJs looks like that: 

\n
 this.control.valueChanges.pipe(\n      debounceTime(300),\n    ).subscribe(value => {\n      this.search.emit(value)\n    })\n
\n

Whereas code using  only signals can look like that:

\n
@Component({\nselector: \'app-root\',\ntemplate: `\n<input [ngModel]="searchValue()" (ngModelChange)="searchValue.set($event)">\n`,\nstyleUrls: [\'./app.component.scss\']\n})\nexport class AppComponent {\nsearchValue = signal(\'\');\ndebouncedSearchValue = signal(this.searchValue());\n\n\nconstructor() {\nlet timeoutId: ReturnType<typeof setTimeout> | undefined;\neffect((onCleanup) => {\nconst search = this.searchValue();\n\n\ntimeoutId = setTimeout(() => {\nthis.debouncedSearchValue.set(search);\n}, 500);\n\n\nonCleanup(() => {\nclearTimeout(timeoutId);\n});\n});\n\n\neffect(() => {\nconsole.log(this.debouncedSearchValue());\n});\n}\n}\n\n
\n

As we can see, code that uses RxJs is simpler and more readable for a developer. But we should remember that we are able to write this code with both approaches. Using signals and RxJs together gives us more benefits than using only one approach.

\n

Generally speaking, asynchronous events handling is really challenging to achieve when only using signals. Additionally, RxJs have existed for many years, are used in a significant number of projects, and with this are reliable and well-tested. 

\n

Michael Hladky spoke on Twitter about re-creating and adjusting all RxJs operators to be compatible with signals. He said this is not the most efficient approach.

\n

\n

Network requests are another case when RxJs come in handy. XHR requests are asynchronous. Observable allows us to handle, map, and handle events like success, error, and completion. 

\n

With RxJs operators, we can re-run requests or cancel them, which is impossible with signals. Signals don’t have classic and well-known operators like catchError or switchMap, which are responsible for canceling requests at a specific moment the new one comes with new data. 

\n

Additionally, one of the most significant Angular features is backward compatibility. Completely removing RxJs from the framework will force Angular developers to spend an enormous amount of time on refactoring, something quite challenging to carry out.  In the case of many projects, a change like that will mean rewriting the entire project. 

\n

Summary

\n

Signals are, without a doubt, a revolution when it comes to performance and the approach to creating applications in Angular. Still, RxJs is a powerful tool that helps us handle asynchronous events easily and comfortably. 

\n

From what I’ve seen, I am convinced that signals will never fully replace RxJs. Signals are able to replace only the synchronous parts of RxJs. But it’s best to combine these two approaches when building Angular applications. That will be more beneficial to app performance and the developer experience. 

\n', + id: 0, + slug: '', + otherTranslations: [], + lang: '', + seo: {}, }; export default meta; type Story = StoryObj; export const primary: Story = { + decorators: [ + applicationConfig({ + providers: [ConfigService, provideI18n()], + }), + ], args: { articleDetails, }, diff --git a/libs/blog/articles/ui-article-card/src/lib/ui-article-card/ui-article-card.stories.ts b/libs/blog/articles/ui-article-card/src/lib/ui-article-card/ui-article-card.stories.ts index dced1786..792c0b0a 100644 --- a/libs/blog/articles/ui-article-card/src/lib/ui-article-card/ui-article-card.stories.ts +++ b/libs/blog/articles/ui-article-card/src/lib/ui-article-card/ui-article-card.stories.ts @@ -1,6 +1,14 @@ -import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; +import { importProvidersFrom } from '@angular/core'; +import { + applicationConfig, + Meta, + moduleMetadata, + StoryObj, +} from '@storybook/angular'; +import { provideI18n } from '@angular-love/blog/i18n/data-access'; import { ArticleCard } from '@angular-love/blog/shared/types'; +import { ConfigService } from '@angular-love/shared/config'; import { ArticleRegularCardComponent } from '../components/article-regular-card/article-regular-card.component'; @@ -36,6 +44,9 @@ const meta: Meta = { moduleMetadata({ imports: [ArticleRegularCardComponent], }), + applicationConfig({ + providers: [importProvidersFrom(ConfigService), provideI18n()], + }), ], }; @@ -48,6 +59,9 @@ export const compact: Story = { cardType: layoutCompact, }, render: (args) => ({ + applicationConfig: { + providers: [ConfigService, provideI18n()], + }, props: args, template: `