Angular + NgRx: ยากจริงดิ๊

Chutipong Jarernsawat
Sirisoft
Published in
7 min readJan 12, 2023

พอ develop ไปนาน ๆ ทุกคนเคยสงสัยไหมว่า เอ๊ะ! ทำไมข้อมูลต่าง ๆ ที่ใช้ในเว็บไซต์ของเราถึงจัดการยากมีตั้งหลาย Services เยอะมากเลย เราจะจัดการข้อมูลต่าง ๆ ยังไงดีน๊าา วันนี้ผมเลยจะมาช่วยทุกคนในเรื่องนี้เองครับ โดยจะใช้สิ่งที่เรียกว่า NgRx ร่วมกับ Angular กันครับผม

NgRx คืออะไร?

NgRx หรือ State management สำหรับใน Angular แล้วเราจะเห็นได้ว่ามีการแชร์ข้อมูลกันระหว่างหลาย ๆ components เพื่อใช้งาน ซึ่งเมื่อเราพัฒนาไปนาน ๆ เข้าเว็บไซต์ก็จะขยายไปเรื่อย ๆ ยากต่อ develop และ maintanance services ต่าง ๆ
ซึ่ง NgRx จะเข้าช่วยในการจัดการข้อมูลที่เราต้องการที่ใช้งานทั้งหมดให้ง่ายขึ้น
โดย NgRx จะประกอบไปด้วย

  • Action: ฟังก์ชันที่เป็นจะไป trigger reducers เพื่อทำการบันทึกข้อมูลไปไว้ที่ store ของ NgRx
  • Reducer: ฟังก์ชันที่ถูกใช้งานเมื่อ state ของข้อมูลมีการเปลี่ยนแปลง
  • Store: model ของข้อมูลที่ใช้ในการเก็บข้อมูล
  • Selector: นำข้อมูลที่เก็บไว้ใน store มาใช้งานที่ components ต่าง ๆ
  • Effects: ฟังก์ชันที่ติดต่อกับฐานข้อมูลหรือ call API ตรง ๆ เพื่อนำข้อมูลมาใช้งานตาม Action ของ NgRx
https://1.bp.blogspot.com/-5C8e4cFJbOc/YO6lGjqUMAI/AAAAAAAArwc/r4oP--oJFFQ5PR2B8vNIqIsfiFiKmqO9ACLcBGAsYHQ/s16000/dd%2B%25282%2529.jpg

NgRx ทำงานยังไง?

การทำงานของ NgRx เริ่มจาก Angular เรียกใช้งานข้อมูลผ่านฟังก์ชันของ Action ซึ่ง Action จะไป trigger Effect เพื่อนำข้อมูลจาก API มาใช้งาน หลังจากนั้นก็ไปเปลี่ยนค่าของ state ของข้อมูลผ่าน Reducer เพื่อทำการเปลี่ยนแปลง state ของข้อมูลแล้วจัดเก็บไว้ที่ store โดย Angular จะสามารถใช้ข้อมูลต่าง ๆ ผ่าน Selector อีกทีนึง โดยที่ Selector จะนำข้อมูลที่ต้องการใช้งานจาก store

Create Angular App

เริ่มต้นจากเรามาสร้าง Angular App กัน โดยผ่าน command:

หลังจากนั้นสิ่งที่ช่วยให้เราจัดการ UI ได้ง่ายขึ้น Boostrap นั่นเอง:

หลังจากนั้นไปที่ไฟล์ angular.json แล้วตั้งค่า Boostrap ตามภาพต่อเลยครับ

ทดสอบว่าเราติดตั้ง Boostrap เสร็จหรือยังโดยสร้าง navbar ที่

  • app.component.html
  • app.component.scss

ลองรัน Angular ของเรากันเลยผ่าน command:

หลังจากนั้นเข้าไปที่ URL: http://localhost:4200

Setup JSON Server

JSON Server คือ การทำ API server ขนาดย่อม ๆ ที่เครื่อง localhost ของเรานั่นเอง โดยเราสามารถติดตั้งผ่าน command:

จากนั้นสร้างไฟล์ db.json ที่ src/assets/db.json

จากนั้นรัน server จิ๋วของเรากันเลย:

ทดสอบว่าเราทำถูกต้องไหมผ่าน URL: http://localhost:3000/cars

สร้าง Interface ของข้อมูลรถ (Cars) เพื่อใช้ในตอน response data จาก API และเป็น type ที่จัดเก็บไว้ที่ store โดยสร้างไฟล์ที่ shared/interfaces/cars.ts

  • shared/interfaces/cars.ts

สร้าง Services สำหรับ CRUD ร่วมกับ JSON Server โดยใช้ httpClient ของ Angular ที่ src/app/shared/services/cars.service.ts:

ฟังก์ชันต่าง ๆ ภายในไฟล์ src/app/shared/services/cars.service.ts

สร้าง Angular Module/Components

เราจะสร้าง Module/Componentsโดยตัวอย่างของข้อมูลที่ผมจะใช้เป็นเกี่ยวกับรถ (Cars) กันนะครับ เริ่มจากสร้าง module กันเลย:

ต่อด้วยสร้าง components หน้าแรกของ Cars:

Config routing ของเว็บภายใน Angular กับ Module เพื่อลดการทำงานแบบ Lazy Loading เริ่มต้นจาก app-routing.module.ts:

และ config routing ของ Cars Module จาก modules/cars/cars.module.ts:

ติดตั้ง NgRx Package

Upgrade Angular ให้เป็น Version 14 ก่อนนะครับไม่งั้นอาจจะติดตั้ง package ของ NgRx ไม่ได้ ผมมี command มาให้ละครับตามด้านล่างนี้โล้ด
(**** ถ้าเป็น Angular Version 14 อยู่แล้วก็ไม่ต้องทำขั้นตอนนี้นะครับ ****)

Install the ‘@ngrx/store’ package:

Install the ‘@ngrx/effects’ package:

Install the ‘@ngrx/store-devtools’ package:

สร้าง CRUD ร่วมกับ NgRx

สร้าง Reducer ของข้อมูลรถ เพื่อใช้ในการเปลี่ยนแปลง state ของข้อมูล โดยสร้างไฟล์ที่ shared/stores/cars/cars.reducer.ts

โดยรายละเอียดของ code ในไฟล์มีดังนี้:

  • คำสั่ง initialState คือคำสั่งที่เอากำหนดค่าข้อมูลเริ่มต้น โดยจาก code เราจะกำหนดค่าเริ่มต้นให้กับข้อมูลรถ (Cars) เป็นค่า array เปล่า
  • การใช้งาน “createReducer” ที่มี input parameter เป็น initialState คือ กำหนด state เริ่มต้นให้กับ store ซึ่งค่าที่ state จะเป็น array ว่างครับ

ต่อมาสร้าง Selector เพื่อกำหนดชื่อข้อมูลที่ต้องการใน store มาใช้งานที่ components ต่าง ๆ ที่ shared/stores/cars/cars.selector.ts

Selector คือ เราจะใช้ข้อมูลที่อยู่ store ชื่อว่าอะไรแล้วจัดเก็บข้อมูลที่ประกอบไปด้วยอะไรบ้าง ในที่นี้ผมทำการสร้าง Interface ของ Cars ขึ้นแล้ว งั้นผมขอทำการสร้างชื่อตัวแปรของ selector นี้ชื่อ “myCars” ไปเล้ย

Import Reducer ของข้อมูลรถ (Cars) เพื่อให้ module สามารถใช้งาน store ที่ชื่อว่า “myCars” ของเราได้ ที่ modules/cars/cars.module.ts

ส่วนนี้คือสร้าง Action เพื่อไป trigger กับ Effect หรือ Reducer ให้อัปเดตข้อมูลที่อยู่ภายใน store โดยจะไฟล์เก็บไว้ที่ shared/stores/cars/cars.action.ts

ถัดมาเป็นรายละเอียด code ที่อยู่ในไฟล์ shared/stores/cars/cars.action.ts

  • invokeCarsAPI คือ การสร้าง Action ขึ้นมาให้สามารถ invoke กับ Effect เพื่อทำการ fetch ข้อมูลจาก API มาใช้งาน โดย ‘[Cars API] Invoke Cars Fetch API’ คือชื่อของ state ที่บ่งบอกว่าตอนนี้ข้อมูลอยู่ใน state ไหนแล้ว
  • carsFetchAPISuccess คือ การสร้างอีก Action เหมือนกันแต่หลังจาก fetch ข้อมูลสำเร็จ Action นี้จะทำการบันทึกข้อมูลรถ (Cars) และจัดเก็บไว้ที่ store

เกือบจะเสร็จแล้วนะครับ ส่วนนี้คือ การสร้าง Effect ให้สามารถ invoke กับ API เพื่อนำข้อมูลต่าง ๆ ใช้งานใน app โดยจะสร้างไฟล์ไว้ที่ shared/stores/cars/cars.effect.ts

โดยรายละเอียดของ code ที่เราจะเขียนกันที่ไฟล์ shared/stores/cars/cars.effect.ts

  • Injectable คือ การ Inject Action Serviceให้มาจาก NgRx store
  • creatEffect คือ การสร้าง Effect จาก NgRx store ที่ได้ โดยชื่อ Effect ที่ถูกสร้างนะมีชื่อว่า “loadAllCars” เพื่อทำการ fetch ข้อมูลรถทั้งหมดออกมาจาก store เพื่อนำมาใช้งาน
  • ofType คือ การเปลี่ยนแปลง state ของข้อมูลใน NgRx จาก code ชื่อ state คือ “invokeCarsAPI” ซึ่ง input parameter เพื่อเปลี่ยนแปลง state ภายใน NgRx
  • withLatestFrom คือ ฟังก์ชันของ RxJs ที่ช่วยในการ get ข้อมูลล่าสุดจาก observable ซึ่ง this.store.pipe(select(selectCars)) หมายความว่า ตรวจสอบข้อมูลภายใน store ก่อน หากมีข้อมูลอยู่ใน store อยู่แล้วก็จะไม่ call API แต่ถ้าหากยังไม่มีข้อมูลอยู่ที่ store จะทำการ call API เพื่อนำข้อมูลมาใช้ภายในระบบ
  • mergeMap คือ ฟังก์ชันของ RxJs เพื่อใช้ในการ Map Object ซึ่งฟังก์ชันนี้จะประกอบไปด้วย 2 parameters โดย parameters ตัวแรกเราจะตั้งค่าเป็น undefined เพราะว่าเราจะไม่ใช้งานมันครับ ส่วนอีก parameter นึงเป็นค่าที่ได้จาก withLatestFrom
  • if ใน mergeMap คือ ถ้าหากมีข้อมูลอยู่แล้วใน store จะ return EMPTY ส่วน else ในหมายความว่าไม่มีข้อมูลอยู่ใน store เลยต้องทำการ fetch ข้อมูลจาก API เพื่อนำมาจัดเก็บไว้ที่ store

ต่อมาเป็นเพิ่มฟังก์ชันการทำงานของ Reducer เล็กน้อย ๆ นะครับ เพื่อเป็นการบอกว่า เรา fetch ข้อมูลเสร็จแล้วหรือไม่ เราทำการเปลี่ยน state แล้วจะเอาข้อมูลไปเก็บไว้ที่ store นะครับ โดยทำการเพิ่มที่ไฟล์ shared/stores/cars/cars.reducer.ts

อย่างสุดท้ายครับเป็นการทำ app state หรือ state ข้อมูลกลางของทั้ง app นะครับ เริ่มที่สร้างไฟล์ shared/stores/app.state.ts

โดยที่ app.state.ts เสมือนว่าเป็น interface บอกว่าทั้ง app ของเราจะเก็บโครงสร้างของ state ข้อมูลที่ได้จาก API เป็นอย่างไรบ้าง ก็ตามผมเลยครับมี apiStatus และ apiResponseMessage ว่า “Succees” หรือ “Fail”

ต่อมาจะเป็น Action ของ app ทั้งหมดนะครับ โดยไฟล์ของเราก็อยู่ที่ shared/stores/app.action.ts

ซึ่ง Action ของ app เราจะเอาไว้ทำ invoke สถานะการ call API เวลา response data จาก API ครับ โดยจะมีรายละเอียดของ code ตามนี้ครับ

สู้ ๆ นะ อันสุดท้ายของ app แล้วครับ ไปกันต่อที่สร้าง Reducer ของ app เลยครับ โดยสร้างไฟล์ที่ shared/stores/app.reducer.ts

โดย Reducer ของ app ที่เรากำลังสร้าง จะถูกใช้งานเพื่อเปลี่ยนแปลงค่า state ของ response data จาก API ก็เป็นตาม code

เกือบจะจบแล้วครับ ส่วนนี้เป็นส่วนของ Selector ของ app ครับที่เอาไว้ initial appState ของ app store ครับ โดยไฟล์จะถูกสร้างที่ shared/stores/app.selector.ts

โดยมีรายละเอียดในการ initial ของ Selector ภายใน app ตาม code ได้เลยครับ

สุดท้ายครับเราจะต้องกลับไปที่ module ของ App และ Cars เพื่อทำการ import และ config store ทุกอย่างเลยนะครับที่ไฟล์

  • app.module.ts
  • modules/cars/cars.module.ts

โอเครครับ! ทีนี้เราก็เตรียมวัตถุดิบเสร็จสิ้นแล้วนะครับ เอ้ยไม่ใช่ครับ เตรียม NgRx store ของเราเสร็จแล้ว เรากลับไปต่อที่ components กันเลย ในไฟล์ที่ cars/cars-table/cars-table.component.ts

  • carList = this.store.pipe(select(selectCars)) หมายความว่า เราจะทำการเอาข้อมูลที่จัดเก็บไว้ที่ store มาใช้งาน
  • this.store.dispatch(invokeCarsAPI()) คือ การ invoke Effect เพื่อทำการ fetch ข้อมูลจาก API เพื่อนำมาใช้งานครับ

ทีนี้เรามีองค์ประกอบกันครบถ้วนแล้วนะครับ แต่เอ๊ะเราลืมทำหน้าจอไปนี่นา รออะไรกันอยู่ตามมาเลยที่ไฟล์ cars/cars-table/cars-table.html

ทีนี้ก็รวมร่างงงงงง ไปดูผลงานเราเลยที่ http://localhost:4200

เก่งมากครับทุกคนเราจะได้หน้าตาของข้อมูลจาก NgRx กันแล้ว ทีนี้เรารู้ได้ไงล่ะว่า เราทำ NgRx สำเร็จไหม? ทุกคนสามารถโหลด Extension ใน Web browser มาได้เลยนะครับ ซึ่งจะมีชื่อว่า “Redux DevTools” (ปล. ผมใช้ Chrome นะครับ)

ทีนี้ทุกคนก็จะสามารถเห็น state ต่าง ๆ ของ NgRx ที่เราได้ลำบากทำมันหลายขั้นตอนได้ที่ “Redux DevTools” เลยครับตามภาพข้างล่าง

ท้อหรือยังครับ อันนี้แค่ fetch all ข้อมูลกลับมานะครับ ยังเหลือ Insert, Update, Delete อีกนะครับ อย่าเพิ่งไปไหนนะครับ เรามาสู้ไปด้วยกันครับ T^T

ต่อมาเป็นการ Insert นะครับ ทำเหมือนเดิมแบบเดียวกับ fetch ข้อมูลเลยครับ แต่ทีนี้อาจจะบางอย่างที่อาจจะเขียนต่างกันเล็กน้อย

เรามาเริ่มที่การเพิ่ม component ไว้สำหรับทำ insert ข้อมูลรถกันเลยครับ โดยไฟล์นี้จะอยู่ที่ modules/cars/cars-add-form

หลังจากนั้น import component และทำการ route หน้าที่ modules/cars/cars.module

ถัดมาเป็นการเพิ่ม store สำหรับการ insert ข้อมูลรถครับ เริ่มจาก Action กันเลยครับ โดยเพิ่มที่ไฟล์ shared/stores/cars/cars.action.ts

  • รูปแบบก็เหมือนกับตอนที่ Action ของการ fetch ข้อมูลรถเลยแต่มีเขียนแตกต่างกันนิดหน่อยครับ เพราะหลังจากที่เพิ่มข้อมูลเสร็จสิ้นระบบจะนำข้อมูลที่เพิ่มเข้าไปใหม่ไว้ store ส่วนฟังก์ชัน createAction ก็ทำงานเหมือนเดิมเลยครับสร้าง Action พร้อมกับชื่อของ state ที่เราต้องการให้แสดงผลที่ Redux DevTools ครับ

ถัดมานะครับเป็นการเพิ่ม Effect ของการ insert ข้อมูลรถ โดยเราจะเพิ่มไฟล์เดิมเลยครับที่ shared/stores/cars/cars.effect.ts

  • switchMap คือ เราจะทำการนำค่าของ instance ของ Action ของการ insert แล้วตั้งค่าให้กับ payload ตอนที่ส่งค่าไปให้กับ API
  • this.appStore.dispatch เป็นการ initial ค่าของ store ที่เราต้องการเก็บค่าของ state ไว้ก่อน call API ว่า success หรือ fail ไหม
  • หลังจากนั้นจะเป็นการ call API ผ่านทาง this.carsService.insert() เพื่อทำการเพิ่มข้อมูลรถ แล้วนำข้อมูลที่ได้ผ่านทาง response ของ API มาเปลี่ยนแปลง state ของ app ให้เป็น success และเปลี่ยนแปลง state ข้อมูลรถของ store เพื่อเปลี่ยนแปลงค่าของรถที่ถูกเพิ่มและจัดเก็บไว้ที่ store

อย่างสุดท้ายครับเป็นการสร้าง Reducer ของการ insert ข้อมูลรถ จำไฟล์ Reducer เดิมได้ไหมครับที่เราทำไว้ที่ shared/stores/cars/cars.reducer.ts เราจะเพิ่มมันที่นี่แหละ

  • โดยเราจะเพิ่ม Reducer ฟังก์ชันใหม่เข้ามาเพื่อทำการเพิ่มค่าข้อมูลใหม่เข้าไปที่ store ของ NgRx โดยชื่อ state ของฟังก์ชันคือ saveNewCarAPISuccess

เสร็จแล้วกับการทำ Insert ร่วมกับ store ของ NgRx ครับไปต่อแบบไม่หยุดยั้ง โดยจะเริ่มต่อกันที่ไฟล์ components ก่อนเลยครับ

  • modules/cars/cars-add-form/cars-add-form.component.ts
  • modules/cars/cars-add-form/cars-add-form.component.html

ทีนี้เราจะเข้าถึงหน้านี้ยังไงเหรอครับ ผมก็เพิ่มปุ่ม insert ที่หน้า modules/cars/cars-table/cars-table.components.html

เสร็จแล้วก็ลองเล่นเพิ่มกันเลยครับที่ URL เดิมเลยครับ http://localhost:4200

เหนื่อยกันยังครับ คนเขียนก็ตื่นเต้นเลยครับมาอดทนอีกนิดครับเกือบจะครบ CRUD แล้วไปต่อกันโล้ดที่การ Update กับ NgRx

เริ่มเหมือนเดิมเลยครับสร้าง components กันก่อนเลยครับ โดยเราจะสร้าง component ไว้ที่ modules/cars/cars-edit-form

ต่อมาเป็นการทำ route ของหน้า Update ข้อมูลกันนะครับ เดี๋ยวทำไปพร้อม ๆ กันเลยนะครับ ไปที่ไฟล์ modules/cars/cars.module.ts

เหมือนเดิมครับเราจะการทำ store เหมือนการ Insert เลยครับเริ่มที่ Action ของไฟล์ shared/stores/cars/cars.action.ts

ถัดมาเป็นก็คือ Effect ของการ Update ข้อมูลของ store ก็เขียน code คล้ายกับการ Insert ข้อมูลเลยครับเปลี่ยนแค่ฟังก์ชันของ Service ที่ call API เท่านั้นครับ โดยจะเพิ่มที่ไฟล์ shared/stores/cars/cars.effect.ts

เกือบสุดท้ายครับของ Store ครับนั่นคือ Reducer เหมือนเดิมเลยครับ ก็แก้ไขไฟล์ shared/stores/cars/cars.reducer.ts

มาถึงอย่างสุดท้ายแล้วนะครับไปต่อที่ Selector สำหรับการ Update กันครับ ที่ไฟล์ shared/stores/cars/cars.selector.ts

  • createSelector คือ การสร้าง Selector Custom ขึ้นมาโดยในที่นี้ผมสร้าง selectCarById เพื่อทำการ fetch ข้อมูลรถโดยใช้ id จาก store

ทีนี้ก็เสร็จสิ้นไปกับการ Update ข้อมูลภายใน storeด้วย NgRx แล้วครับเราจะไปกันที่ component ของการ Update เลยนะครับ

  • modules/cars/cars-edit-form.components.ts
  • modules/cars/cars-edit-form.components.html

ทีนี้ทำเหมือนกับตอนที่เรา Insert เลยครับที่เราไปเพิ่มปุ่ม Update ที่หน้า modules/cars/cars-table/cars-table.components.html เลยครับ

ลองเล่น Update กับ Store กันเลยครับที่ URL: http://localhost:4200

ต่อไปเป็น CRUD สุดท้ายแล้วนะครับทุกคนนนนน นั่นก็คือ Delete ครับเราไปด้วยกันนะครับฮึบ เริ่มจากสร้าง components ก็เลยครับ โอ๊ะไม่ใช่ครับ ในส่วนนี้เราแค่เพิ่ม element html ก็พอครับแต่ผมขอไปทำ store ของการ Delete ก่อนนะครับ

เริ่มที่เพิ่ม Action ของการ Delete ของ store เลยครับที่ไฟล์เดิมที่ไฉไลกว่าเดิม shared/stores/cars/cars-action.ts

ต่อด้วยการเพิ่ม Effect ของการ Delete ข้อมูลรถเลยครับที่ไฟล์ Effect เดิมนะ นั่นก็คือ shared/stores/cars/cars.effect.ts

แล้วสุดท้ายครับเหมือนเดิมเลยนั่นคือไฟล์ Reducer ครับ ที่อยู่ใน shared/stores/cars/cars.reducer.ts

เสร็จแล้วกับการทำ Delete ข้อมูลรถภายใน store ครับดูง่ายใช่ไหมล่ะ ฮ่าๆ แต่ยังไม่จบครับเราเหลือไปทำการเพิ่มปุ่ม Delete ที่หน้า modules/cars/cars-table

  • modules/cars/cars-table.component.ts
  • modules/cars/cars-table.component.html

เท่านี้เราก็ทำสำเร็จไปทั้ง CRUD แล้วนะครับ เย้ๆๆ ไปเล่นเว็บเราในส่วนของการ Delete กันเลยครับที่ URL: http://localhost:4200

เรียบร้อยไปแล้วนะครับสำหรับทำ Angular App ในการทำ State Management ข้อมูลโดยการใช้ NgRx เข้ามาช่วยครับ

สรุปการใช้งาน NgRx

สำหรับการทำใช้งานและการทำ State Management นั้นดีไหม งั้นขอตอบเลยนะครับว่าดีมากครับ ถ้า App ของเรามีขนาดใหญ่ การทำ State Management นั้นจะช่วยเราให้ code และ maintain ข้อมูลต่าง ๆ ใน App ง่ายมาก ๆ แต่หาก App ของเรามีขนาดเล็กล่ะทำยังไงดีทำไปแล้วคุ้มไหม ในมุมมองของผมอาจจะยังไม่คุ้มเท่าไหร่ครับ เนื่องจากอาจจะเสียเวลาไปกับการทำอะไรที่ยุ่งยากไม่ว่าจะเป็นการสร้าง Action, Reducer, Selector และ Effect ทำให้เวลาในพัฒนาอาจจะเสียไปกับการทำ State Management อยู่ก็ได้ ผมจึงคิดว่าถ้า App มีขนาดใหญ่ โอเครเลยครับในการนำ State Management เข้ามาช่วย แต่ถ้าหากยังเป็น App เล็ก ๆ ที่ไม่ได้มีความยุ่งยากของการ maintain ข้อมูลต่าง ๆ อะไรมากนัก อาจจะไม่คุ้มที่จะทำ State Management ครับ

เพื่อนๆสามารถติดตามข่าวสารของพวกเราได้ที่
Facebook: Sirisoft
Instargram: Sirisoft_official
TikTok: Sirisoft
Youtube: Sirisoft Official

ช่องทางสอบถามข้อมูลและสมัครงานได้ที่
Website: Sirisoft
Line official : @sirisoft

--

--