Angular + NgRx: ยากจริงดิ๊
พอ 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
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