สัญญาณจะเปลี่ยนเชิงมุมให้ดีขึ้น

จาก NgRx ComponentStore ถึง SignalStore: ประเด็นสำคัญจากโปรเจ็กต์สาธิตของฉัน

เตรียมตัวอพยพอย่างไรให้มีประสิทธิภาพ

ฉันเชื่อว่า Signals ใน Angular จะเปลี่ยนวิธีการสร้างแอปพลิเคชัน Angular โดยพื้นฐาน บทความนี้เป็นส่วนแรกของซีรีส์ที่มีจุดมุ่งหมายเพื่อแสดงให้คุณเห็นถึงศักยภาพของฟีเจอร์ใหม่นี้ และในขณะเดียวกันก็ช่วยให้คุณเตรียมพร้อมสำหรับการเปลี่ยนแปลงนี้ได้อย่างมีประสิทธิภาพ ในขณะที่ Signals อยู่ในหน้าตัวอย่างสำหรับนักพัฒนาซอฟต์แวร์ และร้านค้าที่ใช้สัญญาณ NgRx เป็นเพียง ต้นแบบ คุณสามารถเริ่มสร้างและปรับโครงสร้างส่วนประกอบของคุณใหม่ในลักษณะที่จะทำให้การโยกย้ายราบรื่นสำหรับคุณ ในส่วนแรกนี้ ฉันจะแสดงให้คุณเห็นว่าฉันใช้แอปพลิเคชันสาธิตเพื่อแสดงความแตกต่างระหว่าง ComponentStore และโมเดลที่ใช้สัญญาณใหม่ได้อย่างไร ในส่วนถัดไปของซีรีส์นี้ ฉันจะเสนอหลักเกณฑ์บางประการเกี่ยวกับวิธีจัดการกับการเปลี่ยนแปลงนี้ ก่อนอื่นให้ฉันแนะนำ Signalsและ NgRx SignalStore

สัญญาณเชิงมุมเป็นโมเดลปฏิกิริยาใหม่ใน Angular 16 สัญญาณช่วยให้เราติดตามการเปลี่ยนแปลงสถานะในแอปพลิเคชันของเราและทริกเกอร์การแสดงผลเทมเพลตที่ปรับให้เหมาะสมที่สุด อัปเดต หากคุณยังใหม่กับ Signals นี่คือบทความที่แนะนำเป็นอย่างยิ่งบางส่วน:

ทีมงาน NgRx และ "Marko Stanimirović" เปิด "RFC ใหม่ (ขอความคิดเห็น) สำหรับโซลูชันการจัดการสถานะตามสัญญาณ SignalStore" มันมีแนวทางเดียวกันกับ @ngrx/component-store การใช้งานครั้งแรกกับเอกสารประกอบ API มีอยู่ใน NgRx SignalStore Playground repo

ดังที่ได้กล่าวไปแล้ว ฉันมั่นใจว่า Signals จะเปลี่ยนวิธีที่เราพัฒนาแอปพลิเคชัน Angular เพื่อให้ได้รับความรู้เพิ่มเติมเกี่ยวกับฟีเจอร์ใหม่นี้และผลกระทบในอนาคต ฉันได้สร้างคอมโพเนนต์ "รายการบทความ" สองเวอร์ชัน ฉันได้สร้างอันที่ใช้ ComponentStore ก่อน จากนั้นจึงย้ายมันไปยังอันที่ใช้ SignalStore ในบทความนี้ ฉันจะอธิบายขั้นตอนการใช้งานและความแตกต่างหลักๆ ที่ฉันพบ เพื่อที่คุณจะได้เข้าใจวิธีการทำงานของ SignalStores ได้ดีขึ้น

ซอร์สโค้ดฉบับเต็มมีอยู่ที่นี่:



แอปพลิเคชันใช้สไตล์และแบ็กเอนด์ที่โฮสต์โดยสาธารณะจาก "โครงการ RealWorld"

แอปพลิเคชันมีคุณลักษณะต่อไปนี้:

  • เมนู ง่ายๆ สำหรับการสลับระหว่างรายการบทความที่ใช้ ComponentStore- และ SignalStore
  • สองรายการบทความ รายการหนึ่งเป็นแบบ ComponentStore และอีกรายการเป็นแบบ SignalStore โดยจะแสดงผู้เขียนบทความ วันที่ตีพิมพ์ จำนวนไลค์ แท็ก และโอกาสในการขาย พวกเขาโหลดรายการบทความจากเซิร์ฟเวอร์ จึงมีการโหลดและมีสถานะข้อผิดพลาด
  • องค์ประกอบ การแบ่งหน้า ด้านล่างรายการบทความแต่ละบทความ ผู้ใช้ยังสามารถเปลี่ยนการแบ่งหน้าตามพารามิเตอร์ URL เช่น: http://localhost:4200/article-list-component-store?selectedPage=3&pageSize=2 หากผู้ใช้เปลี่ยนพารามิเตอร์ URL หรือคลิกที่องค์ประกอบการแบ่งหน้า รายการบทความจะถูกโหลดซ้ำ

สถาปัตยกรรมแอปพลิเคชัน

ฉันใช้ Angular v16 กับส่วนประกอบแบบสแตนด์อโลน เนื่องจาก Signals ยังไม่ทำงานในแอปพลิเคชันที่ไม่มีโซน ฉันจึงใช้กลยุทธ์การตรวจจับการเปลี่ยนแปลง OnPush กับไปป์ async

แอปจะบู๊ต AppComponent ด้วย router-outlet และรายการเมนูสองรายการสำหรับรายการบทความทั้งสองเวอร์ชัน:

  • ArticleListComponent_CS เป็นเวอร์ชันที่ใช้ ComponentStore ของรายการบทความ มันเชื่อมต่อกับ ArticleListComponentStore
  • ArticleListComponent_SS เป็นเวอร์ชันที่ใช้ SignalStore ของรายการบทความ มันเชื่อมต่อกับ ArticleListSignalStore

การใช้งาน "รายการบทความ" ทั้งสองใช้ร้านค้าระดับส่วนประกอบและอาศัยส่วนประกอบ UI ต่อไปนี้:

  • UiArticleListComponent แสดงรายการบทความ (UiArticleLisItemComponent)
  • UiPaginationComponent จัดการการแบ่งหน้า

โครงสร้างไดเร็กทอรีเป็นดังนี้:

src/
|-- app/
|   |-- article-list-ngrx-component-store/ => ArticleListComponent_CS
|   |-- article-list-ngrx-signal-store/ => ArticleListComponent_SS
|   |-- models/
|   |-- services/
|   |-- ui-components/ 
|   |-- app.component.ts
|   |-- app.routes.ts
|-- libs/signal-store/

ส่วนประกอบรายการบทความ

รหัสชั้นเรียนขององค์ประกอบรายการบทความทั้งสองเกือบจะเหมือนกัน:

  • เราฉีดเราเตอร์และร้านค้า
  • เราอัปเดตพารามิเตอร์การแบ่งหน้าในร้านค้าหลังจากสร้างส่วนประกอบแล้ว นอกจากนี้เรายังอัปเดตพารามิเตอร์หากพารามิเตอร์ใน URL เปลี่ยนแปลง

ข้อแตกต่างเพียงอย่างเดียวคือคลาสของร้านค้าที่ฉีด: ArticleListComponentStore และ ArticleListSignalStore:

export class ArticleListComponent_CS {
  readonly store = inject(ArticleListComponentStore);
  readonly route = inject(ActivatedRoute);

  constructor(
  ) {
    this.route.queryParams.pipe(takeUntilDestroyed()).subscribe(
      routeParams => {
      this.store.setPaginationSettings(routeParams);
      this.store.loadArticles();
    });
  }
}
export class ArticleListComponent_SS {
  readonly store = inject(ArticleListSignalStore);
  readonly route = inject(ActivatedRoute);

  constructor(
  ) {
    this.route.queryParams.pipe(takeUntilDestroyed()).subscribe(
      routeParams => {
      this.store.setPaginationSettings(routeParams);
      this.store.loadArticles();
    });
  }
}

เทมเพลตของส่วนประกอบก็คล้ายกันเช่นกัน ความแตกต่างพื้นฐานคือวิธีที่เราอ่านข้อมูลจากร้านค้า:

  • เราใช้ async ไปป์เพื่ออ่านจาก ตัวเลือกของ ComponentStore และ
  • เราเพียงแค่รับค่าของ สัญญาณ ใน SignalStore
@Component({
  selector: ‘app-article-list-cs’,
  // ...
  providers: [ArticleListComponentStore],
  template: `
<ng-container *ngIf="(store.httpRequestState$ | async) === ‘FETCHING’">
  Loading...
</ng-container>
<ng-container *ngIf="store.httpRequestState$ | async | httpRequestStateErrorPipe as errorMessage">
  {{ errorMessage }}
</ng-container>
<ng-container *ngIf="(store.httpRequestState$ | async) === ‘FETCHED’">
  <ng-container *ngIf="store.articles$ | async as articles">
    <app-ui-article-list [articles]="articles"/>
  </ng-container>
  <ng-container *ngIf="store.pagination$ | async as pagination">
    <app-ui-pagination
      [selectedPage]="pagination.selectedPage"
      [totalPages]="pagination.totalPages"
      (onPageSelected)="store.setSelectedPage($event); store.loadArticles();" />
  </ng-container>
</ng-container>
  `
})
@Component({
  selector: ‘app-article-list-ss’,
  // ...
  providers: [ArticleListSignalStore],
  template: `
<ng-container *ngIf="store.httpRequestState() === ‘FETCHING’">
  Loading...
</ng-container>
<ng-container *ngIf="store.httpRequestState() | httpRequestStateErrorPipe as errorMessage">
  {{ errorMessage }}
</ng-container>
<ng-container *ngIf="store.httpRequestState() === ‘FETCHED’">
  <ng-container *ngIf="store.articles() as articles">
    <app-ui-article-list [articles]="articles"/>
  </ng-container>
  <ng-container *ngIf="store.pagination() as pagination">
    <app-ui-pagination
      [selectedPage]="pagination.selectedPage()"
      [totalPages]="pagination.totalPages()"
      (onPageSelected)="store.setSelectedPage($event); store.loadArticles();" />
  </ng-container>
</ng-container>
  `
})

สถานะ

ฉันใช้โครงสร้างข้อมูลที่ไม่เปลี่ยนรูปแบบเดียวกันสำหรับการจัดเก็บสถานะในร้านค้าทั้งสองแห่ง (HttpRequestState และ Articles เป็นประเภทที่ไม่เปลี่ยนรูปแบบด้วย):

export type ArticleListState = {
  readonly selectedPage: number,
  readonly pageSize: number,

  readonly httpRequestState: HttpRequestState,

  readonly articles: Articles,
  readonly articlesCount: number,
}

คุณสมบัติ selectedPage ระบุหน้าที่มองเห็นได้ในปัจจุบัน คุณสมบัติ pageSize กำหนดจำนวนบทความที่มองเห็นได้ ผู้ใช้สามารถเปลี่ยนแปลงค่าเหล่านี้ได้โดยใช้ส่วนประกอบการแบ่งหน้าหรือโดยการใช้พารามิเตอร์ URL

คุณสมบัติ httpRequestState มีสถานะคำขอของรายการบทความ:

export type HttpRequestState = DeepReadonly<
  'EMPTY' | 'FETCHING' | 'FETCHED' |
  { errorMessage: string }
  >;

เริ่มแรกค่าของมันคือ `EMPTY` เราเปลี่ยนเป็น `FETCHING` ก่อนที่เราจะส่งคำขอไปยังเซิร์ฟเวอร์ เมื่อการตอบกลับของเซิร์ฟเวอร์มาถึง เราจะตั้งค่าเป็น `FETCHED` หากเซิร์ฟเวอร์ส่งการตอบสนองข้อผิดพลาดหรือมีข้อผิดพลาดในระหว่างการร้องขอ เราจะตั้งค่าสถานะคำขอเป็นวัตถุ { errorMessage: string } พร้อมข้อความแสดงข้อผิดพลาด

การตอบสนองของเซิร์ฟเวอร์ประกอบด้วยจำนวนบทความทั้งหมดและตัวบทความเอง เราจัดเก็บไว้ในคุณสมบัติ articlesCount และ articles

หลังจากที่เราสร้างส่วนประกอบรายการบทความ ร้านค้าของพวกเขาจะมีสถานะเริ่มต้น:

export const initialArticleListState: ArticleListState = {
  selectedPage: 0,
  pageSize: 3,

  httpRequestState: ‘EMPTY’,

  articles: [],
  articlesCount: 0
}

ร้านค้า

ฉันขยาย ArticleListComponentStore จาก ComponentStore:

@Injectable()
export class ArticleListComponentStore extends ComponentStore<ArticleListState> {
  readonly selectedPage$: Observable<number> = /* ... */;
  readonly pageSize$: Observable<number> = /* ... */;
  readonly httpRequestState$: Observable<HttpRequestState> = /* ... */;
  readonly articles$: Observable<DeepReadonly<Articles>> = /* ... */;
  readonly articlesCount$: Observable<number> = /* ... */;

  readonly totalPages$: Observable<number> = /* ... */;

  readonly pagination$: Observable<{ selectedPage: number, totalPages: number }> = /* ... */;

  readonly articlesService = inject(ArticlesService);

  constructor(
  ) {
    super(initialArticleListState);
  }

  setPaginationSettings = this.updater(
    (state, s: RouteParamsPaginatonState) => /* ... */);

  readonly loadArticles = this.effect<void>(/* ... */);

  setRequestStateLoading = this.updater(
    (state) => /* ... */);

  setRequestStateSuccess = this.updater(
    (state, params: ArticlesResponseType) => /* ... */);

  setRequestStateError = this.updater(
    (state, error: string): => /* ... */);

  setSelectedPage = this.updater(
    (state, selectedPage: number) => /* ... */);
}

ฉันสร้าง `ArticleListSignalStore` ด้วยฟังก์ชัน `signalStore()` ยอมรับคุณสมบัติต่างๆ ของร้านค้าตามลำดับ ฉันจะอธิบายรายละเอียดเพิ่มเติมเหล่านี้:

export const ArticleListSignalStore = signalStore(
  { debugId: ‘ArticleListSignalStore’ },
  withState<ArticleListState>(initialArticleListState),
  withComputed(({ articlesCount, pageSize }) => ({ /* ... */ })),
  withComputed(({ selectedPage, totalPages }) => ({ /* ... */ })),
  withUpdaters(({ update }) => ({
    setPaginationSettings: (s: RouteParamsPaginatonState) =>  /* ... */,
    setRequestStateLoading: () => /* ... */ ,
    setRequestStateSuccess: => /* ... */ ,
    setRequestStateError: (error: string) => /* ... */ ,
    setSelectedPage: (selectedPage: number) =>  /* ... */,
  withEffects(
    ( {
      selectedPage, pageSize,
      setRequestStateLoading, setRequestStateSuccess, setRequestStateError
      },
    ) => {
      const articlesService = inject(ArticlesService)
      // ...
    }
  )
);

ตัวเลือก

ArticleListComponentStore เก็บสถานะไว้ในหัวเรื่อง store$ หัวข้อนี้จะปล่อยค่าทุกครั้งที่มีการเปลี่ยนแปลงสถานะ หากต้องการสังเกตการเปลี่ยนแปลงคุณสมบัติของรัฐทีละรายการ เราจะสร้างตัวเลือกแยกต่างหากสำหรับคุณสมบัติแต่ละรายการเหล่านี้:

readonly selectedPage$: Observable<number> = 
    this.select(state => state.selectedPage);
  readonly pageSize$: Observable<number> = 
    this.select(state => state.pageSize);
  readonly httpRequestState$: Observable<HttpRequestState> = 
    this.select(state => state.httpRequestState);
  readonly articles$: Observable<DeepReadonly<Articles>> = 
    this.select(state => state.articles);
  readonly articlesCount$: Observable<number> = 
    this.select(state => state.articlesCount);

SignalStore จะสร้าง signal แยกต่างหากโดยอัตโนมัติสำหรับคุณสมบัติรูททั้งหมดของสถานะ เราเรียกสิ่งเหล่านี้ว่าสถานะบางส่วน เราสามารถเข้าถึงสถานะบางส่วนเหล่านี้ได้โดย:

  • ArticleListSignalStore.selectedPage()
  • ArticleListSignalStore.pageSize()
  • ArticleListSignalStore.httpRequestState()
  • ArticleListSignalStore.articles()และ
  • ArticleListSignalStore.articlesCount()

ฉันสร้างตัวเลือกรวมเพิ่มเติมใน ArticleListComponentStore เพื่อคำนวณจำนวนหน้า:

readonly totalPages$: Observable<number> = this.select(
    this.articlesCount$, this.pageSize$,
    (articlesCount, pageSize) => Math.ceil(articlesCount / pageSize));

หากต้องการทำเช่นเดียวกันใน ArticleListSignalStore ฉันใช้ฟังก์ชัน withComputed() ฉันใส่สัญญาณ articlesCount และ pageSize เป็นพารามิเตอร์ให้กับฟังก์ชัน และคำนวณจำนวนหน้าทั้งหมด:

withComputed(({ articlesCount, pageSize }) => ({
    totalPages: computed(() => Math.ceil(articlesCount() / pageSize())),
  })),

นอกจากนี้เรายังจำเป็นต้องเพิ่มตัวเลือก "รูปแบบมุมมอง" ให้กับองค์ประกอบการแบ่งหน้า นี่คือรหัสสำหรับตัวเลือกใน ArticleListComponentStore:

readonly pagination$: Observable<{ selectedPage: number, totalPages: number }> = this.select(
    this.selectedPage$,
    this.totalPages$,
    (selectedPage, totalPages) => ({ selectedPage, totalPages })
  );

และนี่คือตัวเลือกเดียวกันใน ArticleListSignalStore เช่นกัน:

withComputed(({ selectedPage, totalPages }) => ({
    pagination: computed(() => ({ selectedPage, totalPages })),
  })),

ผู้อัปเดต

ภายใน updaters ของ ComponentStore เราจะสร้างออบเจ็กต์สถานะที่ไม่เปลี่ยนรูปแบบใหม่ด้วยค่าที่อัปเดตแล้วส่งคืนเสมอ ออบเจ็กต์สถานะที่ส่งคืนมีคุณสมบัติทั้งหมดจากสถานะ ทั้งที่อัปเดตและไม่ได้แก้ไข

ตัวอย่างเช่น นี่คือวิธีที่เราจัดการกับการตอบสนองของเซิร์ฟเวอร์:

setRequestStateSuccess = this.updater((state, params: ArticlesResponseType): ArticleListState => {
    return {
      ...state,
      httpRequestState: ‘FETCHED’,
      articles: params.articles,
      articlesCount: params.articlesCount
    }
  });

พารามิเตอร์ params มีค่า articles และ articlesCount จากการตอบสนองของเซิร์ฟเวอร์:

export type ArticlesResponseType = {
  articles: Articles,
  articlesCount: number
}

ใน ArticleListSignalStore เราสร้าง updaters ด้วยฟังก์ชัน withUpdaters() ใน updaters เหล่านี้ เราสร้างออบเจ็กต์ที่ไม่เปลี่ยนรูปใหม่จากคุณสมบัติที่อัปเดตเท่านั้น ดังนั้นจึงไม่มี …state ที่นี่ SignalStore อัปเดตสถานะบางส่วนด้วยการใช้ค่าคุณสมบัติที่ส่งคืนเหล่านี้:

withUpdaters(({ update }) => ({
    setPaginationSettings: (s: RouteParamsPaginatonState) => update(() => ({
    // ...
    setRequestStateSuccess: (params: ArticlesResponseType) => update(() => ({
      httpRequestState: ‘FETCHED’,
      articles: params.articles,
      articlesCount: params.articlesCount
    }))
    // ...
  }))

ผลกระทบ

ร้านค้ามี effect เดียวที่ดึงรายการบทความจากเซิร์ฟเวอร์ นี่คือ effect จาก ArticleListComponentStore:

readonly loadArticles = this.effect<void>((trigger$: Observable<void>) => {
    return trigger$.pipe(
      withLatestFrom(this.selectedPage$, this.pageSize$),
      tap(() => this.setRequestStateLoading()),
      switchMap(([, selectedPage, pageSize]) => {
        return this.articlesService.getArticles({
          limit: pageSize,
          offset: selectedPage * pageSize
        }).pipe(
          tapResponse(
            (response) => {
              this.setRequestStateSuccess(response);
            },
            (errorResponse: HttpErrorResponse) => {
              this.setRequestStateError(‘Request error’);
            }
          ),
        );
      }),
    );
  });

ใน SignalStore เราใช้เอฟเฟกต์กับฟังก์ชัน withEffects() SignalStores รองรับเอฟเฟกต์สองประเภท: เอฟเฟกต์แบบ RxJs และเอฟเฟกต์แบบ Promise เอฟเฟกต์แบบ RxJs ดูคล้ายกับเอฟเฟกต์ที่เราใช้ใน ComponentStore มาก:

withEffects(
    ( {
      selectedPage, pageSize,
      setRequestStateLoading, setRequestStateSuccess, setRequestStateError
      },
    ) => {
      const articlesService = inject(ArticlesService)
      return {
        loadArticles: rxEffect<void>(
          pipe(
            tap(() => setRequestStateLoading()),
            switchMap(() => articlesService.getArticles({
              limit: pageSize(),
              offset: selectedPage() * pageSize()
            })),
            tapResponse(
              (response) => {
                setRequestStateSuccess(response);
              },
              (errorResponse: HttpErrorResponse) => {
                setRequestStateError(‘Request error’);
              }
            )
          )
        )
      }
    }
  )

เอฟเฟกต์ที่ใช้ Promise จะมีประโยชน์เมื่อ Promise มีฟังก์ชันการทำงานที่เพียงพอ และเราไม่ต้องการพลังของ RxJ ในกรณีที่ดึงข้อมูลจากเซิร์ฟเวอร์ มันมีข้อเสียเปรียบ: ไม่รองรับตรรกะการยกเลิก:

withEffects(
    ( {
      selectedPage, pageSize,
      setRequestStateLoading, setRequestStateSuccess, setRequestStateError
      },
    ) => {
      const articlesService = inject(ArticlesService)
      return {
        async loadArticles() {
          setRequestStateLoading();
          try {
            const response = await lastValueFrom(articlesService.getArticles({
              limit: pageSize(),
              offset: selectedPage() * pageSize()
            }));
            setRequestStateSuccess(response);
          }
          catch(e) {
            setRequestStateError(‘Request error’);
          }
        }
      }
    }
  )

สรุป

โดยสรุป ความแตกต่างที่สำคัญระหว่าง ComponentStore และ SignalStore คือ:

  • ComponentStore มีสถานะอยู่ในหัวเรื่อง state$ SignalStore มีสัญญาณแยกต่างหากสำหรับคุณสมบัติรูททั้งหมดของรัฐ (สถานะบางส่วน)
  • ใน SignalStore เราไม่จำเป็นต้องมีตัวเลือกเพื่อเข้าถึงคุณสมบัติระดับรูทของรัฐ โดยจะเก็บข้อมูลเหล่านี้ไว้ในสัญญาณแยกกัน ดังนั้นจึงสามารถเข้าถึงได้โดยตรง
  • ทั้ง ComponentStore และ SignalStore รองรับเอฟเฟกต์แบบ RxJs และ SignalStore ยังรองรับเอฟเฟกต์แบบ Promise อีกด้วย

แม้ว่าการใช้งาน SignalStore ในปัจจุบันจะเป็นเพียงต้นแบบ และอาจมีการเปลี่ยนแปลง API ในอนาคต แต่ฉันก็สนุกกับการทำงานกับมันมาก API มีความยืดหยุ่นและเข้าใจง่าย เนื่องจากเป็นไปตามแนวคิดพื้นฐานของ ComponentStore แต่เป็นแนวทางขั้นสูงกว่า

เพื่อให้ง่ายต่อการแก้ไขข้อบกพร่องการเปลี่ยนแปลงสถานะ updaters และ effects ฉันจึงแพตช์โค้ด SignalStore ดั้งเดิมด้วยโค้ดดีบักบางส่วนจากโปรเจ็กต์ ngx-ngrx-component-store-debug-tools ของฉัน

คำถามหลักที่ฉันมีตอนนี้คือจะสร้าง ComponentStores อย่างไรในลักษณะที่เมื่อมีการเผยแพร่ SignalStore ที่พร้อมสำหรับการใช้งานจริง จะช่วยให้กระบวนการย้ายข้อมูลสามารถจัดการได้ง่าย

ในส่วนถัดไปของชุดบทความ ฉันจะกำหนดหลักเกณฑ์บางประการที่จะช่วยให้เราบรรลุเป้าหมายนี้ นอกจากนี้ ฉันจะตรวจสอบสถานการณ์ที่ซับซ้อนและเปรียบเทียบสองวิธีนี้ เช่น วิธีการทำงานของการยกเลิกคำขอ HTTP ใน SignalStore และ ComponentStore

ขอบคุณสำหรับการอ่าน ฉันหวังว่าคุณจะพบว่าบทความของฉันมีประโยชน์ โปรดแจ้งให้เราทราบหากคุณมีข้อเสนอแนะ!

👨‍💻เกี่ยวกับผู้เขียน

ฉันชื่อ "Gergely Szerovay" ฉันทำงานเป็นหัวหน้าบทการพัฒนาส่วนหน้า การสอน (และการเรียนรู้) เชิงมุมเป็นหนึ่งในความสนใจของฉัน ฉันบริโภคเนื้อหาที่เกี่ยวข้องกับ Angular ทุกวัน ไม่ว่าจะเป็นบทความ พอดแคสต์ การพูดคุยในการประชุม และอื่นๆ อีกมากมาย

ฉันสร้างจดหมายข่าว Angular Addict ขึ้นมาเพื่อที่ฉันจะได้ส่งแหล่งข้อมูลที่ดีที่สุดที่ฉันเจอในแต่ละเดือนไปให้คุณ ไม่ว่าคุณจะเป็น Angular Addict ที่มีประสบการณ์หรือเป็นมือใหม่ ฉันก็ช่วยคุณได้

ถัดจากจดหมายข่าว ฉันยังมีสิ่งพิมพ์ชื่อ — คุณเดาได้เลย — Angular Addicts เป็นการรวบรวมแหล่งข้อมูลที่ฉันพบว่ามีข้อมูลและน่าสนใจที่สุด แจ้งให้เราทราบหากคุณต้องการที่จะรวมเป็นนักเขียน

มาเรียนรู้ Angular ด้วยกัน! สมัครสมาชิกที่นี่ 🔥

ติดตามฉันที่ "Medium", "Twitter" หรือ "LinkedIn" เพื่อเรียนรู้เพิ่มเติมเกี่ยวกับ Angular!