# สร้างแอปธนาคาร ตอนที่ 4: แนวคิดการจัดการสถานะ ## แบบทดสอบก่อนเรียน [แบบทดสอบก่อนเรียน](https://ff-quizzes.netlify.app/web/quiz/47) ### บทนำ เมื่อแอปพลิเคชันเว็บเติบโตขึ้น การติดตามการไหลของข้อมูลทั้งหมดกลายเป็นเรื่องท้าทาย โค้ดใดที่ได้รับข้อมูล หน้าใดที่ใช้ข้อมูลนั้น ต้องอัปเดตเมื่อใดและที่ไหน...ง่ายมากที่จะจบลงด้วยโค้ดที่ยุ่งเหยิงและยากต่อการดูแลรักษา โดยเฉพาะอย่างยิ่งเมื่อคุณต้องแชร์ข้อมูลระหว่างหน้าต่าง ๆ ของแอป เช่น ข้อมูลผู้ใช้ แนวคิดของ *การจัดการสถานะ* มีอยู่ในโปรแกรมทุกประเภทมาโดยตลอด แต่เมื่อแอปเว็บมีความซับซ้อนมากขึ้นเรื่อย ๆ การจัดการสถานะจึงกลายเป็นจุดสำคัญที่ต้องพิจารณาในระหว่างการพัฒนา ในส่วนสุดท้ายนี้ เราจะมาทบทวนแอปที่เราได้สร้างขึ้นเพื่อคิดใหม่เกี่ยวกับวิธีการจัดการสถานะ โดยสนับสนุนการรีเฟรชเบราว์เซอร์ในทุกจุด และทำให้ข้อมูลคงอยู่ในเซสชันของผู้ใช้ ### ความต้องการเบื้องต้น คุณต้องทำส่วน [การดึงข้อมูล](../3-data/README.md) ของแอปเว็บให้เสร็จสมบูรณ์สำหรับบทเรียนนี้ นอกจากนี้คุณยังต้องติดตั้ง [Node.js](https://nodejs.org) และ [เรียกใช้เซิร์ฟเวอร์ API](../api/README.md) ในเครื่องเพื่อจัดการข้อมูลบัญชี คุณสามารถทดสอบว่าเซิร์ฟเวอร์ทำงานได้อย่างถูกต้องโดยการรันคำสั่งนี้ในเทอร์มินัล: ```sh curl http://localhost:5000/api # -> should return "Bank API v1.0.0" as a result ``` --- ## ทบทวนการจัดการสถานะ ใน [บทเรียนก่อนหน้า](../3-data/README.md) เราได้แนะนำแนวคิดพื้นฐานของสถานะในแอปของเราด้วยตัวแปร `account` แบบ global ซึ่งมีข้อมูลธนาคารของผู้ใช้ที่เข้าสู่ระบบอยู่ในปัจจุบัน อย่างไรก็ตาม การใช้งานปัจจุบันของเรามีข้อบกพร่อง ลองรีเฟรชหน้าเมื่อคุณอยู่ในแดชบอร์ด เกิดอะไรขึ้น? มีปัญหา 3 ข้อในโค้ดปัจจุบัน: - สถานะไม่ได้ถูกเก็บไว้ เนื่องจากการรีเฟรชเบราว์เซอร์จะพาคุณกลับไปที่หน้าเข้าสู่ระบบ - มีฟังก์ชันหลายตัวที่แก้ไขสถานะ เมื่อแอปเติบโตขึ้น อาจทำให้ยากต่อการติดตามการเปลี่ยนแปลงและง่ายต่อการลืมอัปเดต - สถานะไม่ได้ถูกล้าง ดังนั้นเมื่อคุณคลิก *ออกจากระบบ* ข้อมูลบัญชียังคงอยู่แม้ว่าคุณจะอยู่ในหน้าเข้าสู่ระบบ เราสามารถอัปเดตโค้ดของเราเพื่อแก้ไขปัญหาเหล่านี้ทีละข้อ แต่จะสร้างการทำซ้ำโค้ดมากขึ้นและทำให้แอปซับซ้อนและยากต่อการดูแลรักษา หรือเราสามารถหยุดชั่วคราวและคิดกลยุทธ์ใหม่ > ปัญหาอะไรที่เรากำลังพยายามแก้ไขจริง ๆ? [การจัดการสถานะ](https://en.wikipedia.org/wiki/State_management) คือการหาวิธีที่ดีในการแก้ไขปัญหาเฉพาะสองข้อนี้: - จะทำให้การไหลของข้อมูลในแอปเข้าใจง่ายได้อย่างไร? - จะทำให้ข้อมูลสถานะสอดคล้องกับส่วนติดต่อผู้ใช้เสมอ (และในทางกลับกัน) ได้อย่างไร? เมื่อคุณจัดการกับสิ่งเหล่านี้แล้ว ปัญหาอื่น ๆ ที่คุณอาจมีอาจได้รับการแก้ไขแล้วหรือกลายเป็นเรื่องง่ายขึ้นในการแก้ไข มีวิธีการมากมายในการแก้ไขปัญหาเหล่านี้ แต่เราจะใช้วิธีแก้ไขทั่วไปที่ประกอบด้วย **การรวมศูนย์ข้อมูลและวิธีการเปลี่ยนแปลงข้อมูล** การไหลของข้อมูลจะเป็นดังนี้: ![แผนภาพแสดงการไหลของข้อมูลระหว่าง HTML, การกระทำของผู้ใช้ และสถานะ](../../../../translated_images/data-flow.fa2354e0908fecc89b488010dedf4871418a992edffa17e73441d257add18da4.th.png) > เราจะไม่ครอบคลุมส่วนที่ข้อมูลกระตุ้นการอัปเดตมุมมองโดยอัตโนมัติ เนื่องจากเกี่ยวข้องกับแนวคิดขั้นสูงของ [Reactive Programming](https://en.wikipedia.org/wiki/Reactive_programming) ซึ่งเป็นหัวข้อที่ดีสำหรับการศึกษาต่อหากคุณต้องการเจาะลึก ✅ มีไลบรารีมากมายที่มีวิธีการต่าง ๆ ในการจัดการสถานะ [Redux](https://redux.js.org) เป็นตัวเลือกที่ได้รับความนิยม ลองดูแนวคิดและรูปแบบที่ใช้ เนื่องจากมักเป็นวิธีที่ดีในการเรียนรู้ปัญหาที่อาจเกิดขึ้นในแอปเว็บขนาดใหญ่และวิธีแก้ไข ### งาน เราจะเริ่มต้นด้วยการปรับปรุงโค้ดเล็กน้อย แทนที่การประกาศ `account`: ```js let account = null; ``` ด้วย: ```js let state = { account: null }; ``` แนวคิดคือการ *รวมศูนย์* ข้อมูลแอปทั้งหมดในวัตถุสถานะเดียว เรามีเพียง `account` ในสถานะตอนนี้ ดังนั้นมันจึงไม่เปลี่ยนแปลงมากนัก แต่สร้างเส้นทางสำหรับการพัฒนาในอนาคต เรายังต้องอัปเดตฟังก์ชันที่ใช้มัน ในฟังก์ชัน `register()` และ `login()` แทนที่ `account = ...` ด้วย `state.account = ...`; ที่ด้านบนของฟังก์ชัน `updateDashboard()` เพิ่มบรรทัดนี้: ```js const account = state.account; ``` การปรับปรุงโค้ดนี้เองไม่ได้ทำให้เกิดการปรับปรุงมากนัก แต่แนวคิดคือการวางรากฐานสำหรับการเปลี่ยนแปลงต่อไป ## ติดตามการเปลี่ยนแปลงข้อมูล ตอนนี้เราได้วางวัตถุ `state` เพื่อเก็บข้อมูลของเราแล้ว ขั้นตอนต่อไปคือการรวมศูนย์การอัปเดต เป้าหมายคือทำให้ง่ายต่อการติดตามการเปลี่ยนแปลงใด ๆ และเมื่อเกิดขึ้น เพื่อหลีกเลี่ยงการเปลี่ยนแปลงที่เกิดขึ้นกับวัตถุ `state` การพิจารณาให้มันเป็น [*immutable*](https://en.wikipedia.org/wiki/Immutable_object) หรือไม่สามารถแก้ไขได้เลยก็เป็นแนวปฏิบัติที่ดี นอกจากนี้ยังหมายความว่าคุณต้องสร้างวัตถุสถานะใหม่หากคุณต้องการเปลี่ยนแปลงสิ่งใดในนั้น ด้วยวิธีนี้ คุณสร้างการป้องกันเกี่ยวกับ [side effects](https://en.wikipedia.org/wiki/Side_effect_(computer_science)) ที่อาจไม่ต้องการ และเปิดโอกาสสำหรับคุณสมบัติใหม่ในแอปของคุณ เช่น การทำ undo/redo ในขณะเดียวกันก็ทำให้ง่ายต่อการดีบัก ตัวอย่างเช่น คุณสามารถบันทึกการเปลี่ยนแปลงทุกครั้งที่เกิดขึ้นกับสถานะและเก็บประวัติการเปลี่ยนแปลงเพื่อทำความเข้าใจแหล่งที่มาของบั๊ก ใน JavaScript คุณสามารถใช้ [`Object.freeze()`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze) เพื่อสร้างเวอร์ชันที่ไม่สามารถแก้ไขได้ของวัตถุ หากคุณพยายามเปลี่ยนแปลงวัตถุที่ไม่สามารถแก้ไขได้ จะเกิดข้อยกเว้นขึ้น ✅ คุณรู้ความแตกต่างระหว่างวัตถุ *shallow* และ *deep* immutable หรือไม่? คุณสามารถอ่านเกี่ยวกับมัน [ที่นี่](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze#What_is_shallow_freeze) ### งาน มาสร้างฟังก์ชัน `updateState()` ใหม่: ```js function updateState(property, newData) { state = Object.freeze({ ...state, [property]: newData }); } ``` ในฟังก์ชันนี้ เรากำลังสร้างวัตถุสถานะใหม่และคัดลอกข้อมูลจากสถานะก่อนหน้าโดยใช้ [*spread (`...`) operator*](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/Spread_syntax#Spread_in_object_literals) จากนั้นเราจะเขียนทับคุณสมบัติหนึ่งของวัตถุสถานะด้วยข้อมูลใหม่โดยใช้ [bracket notation](https://developer.mozilla.org/docs/Web/JavaScript/Guide/Working_with_Objects#Objects_and_properties) `[property]` สำหรับการกำหนดค่า สุดท้ายเราล็อกวัตถุเพื่อป้องกันการแก้ไขโดยใช้ `Object.freeze()` ตอนนี้เรามีเพียงคุณสมบัติ `account` ที่เก็บไว้ในสถานะ แต่ด้วยวิธีนี้คุณสามารถเพิ่มคุณสมบัติได้มากเท่าที่คุณต้องการในสถานะ เราจะอัปเดตการเริ่มต้น `state` เพื่อให้แน่ใจว่าสถานะเริ่มต้นถูกล็อกด้วย: ```js let state = Object.freeze({ account: null }); ``` หลังจากนั้น อัปเดตฟังก์ชัน `register` โดยแทนที่การกำหนดค่า `state.account = result;` ด้วย: ```js updateState('account', result); ``` ทำเช่นเดียวกันกับฟังก์ชัน `login` โดยแทนที่ `state.account = data;` ด้วย: ```js updateState('account', data); ``` เราจะใช้โอกาสนี้แก้ไขปัญหาข้อมูลบัญชีที่ไม่ได้ถูกล้างเมื่อผู้ใช้คลิก *ออกจากระบบ* สร้างฟังก์ชันใหม่ชื่อ `logout()`: ```js function logout() { updateState('account', null); navigate('/login'); } ``` ใน `updateDashboard()` แทนที่การเปลี่ยนเส้นทาง `return navigate('/login');` ด้วย `return logout()`; ลองลงทะเบียนบัญชีใหม่ ออกจากระบบ และเข้าสู่ระบบอีกครั้งเพื่อตรวจสอบว่าทุกอย่างยังทำงานได้อย่างถูกต้อง > เคล็ดลับ: คุณสามารถดูการเปลี่ยนแปลงสถานะทั้งหมดได้โดยเพิ่ม `console.log(state)` ที่ด้านล่างของ `updateState()` และเปิดคอนโซลในเครื่องมือพัฒนาของเบราว์เซอร์ของคุณ ## ทำให้สถานะคงอยู่ แอปเว็บส่วนใหญ่ต้องการเก็บข้อมูลเพื่อให้ทำงานได้อย่างถูกต้อง ข้อมูลสำคัญทั้งหมดมักจะถูกเก็บไว้ในฐานข้อมูลและเข้าถึงผ่านเซิร์ฟเวอร์ API เช่นเดียวกับข้อมูลบัญชีผู้ใช้ในกรณีของเรา แต่บางครั้งก็เป็นเรื่องน่าสนใจที่จะเก็บข้อมูลบางส่วนไว้ในแอปที่ทำงานในเบราว์เซอร์ของคุณ เพื่อประสบการณ์ผู้ใช้ที่ดีขึ้นหรือเพื่อปรับปรุงประสิทธิภาพการโหลด เมื่อคุณต้องการเก็บข้อมูลในเบราว์เซอร์ มีคำถามสำคัญบางข้อที่คุณควรถามตัวเอง: - *ข้อมูลนี้เป็นข้อมูลที่ละเอียดอ่อนหรือไม่?* คุณควรหลีกเลี่ยงการเก็บข้อมูลที่ละเอียดอ่อนในฝั่งไคลเ [แบบทดสอบหลังการบรรยาย](https://ff-quizzes.netlify.app/web/quiz/48) ## งานที่ได้รับมอบหมาย [พัฒนา "กล่องโต้ตอบเพิ่มธุรกรรม"](assignment.md) นี่คือตัวอย่างผลลัพธ์หลังจากทำงานที่ได้รับมอบหมายเสร็จ: ![ภาพหน้าจอแสดงตัวอย่าง "กล่องโต้ตอบเพิ่มธุรกรรม"](../../../../translated_images/dialog.93bba104afeb79f12f65ebf8f521c5d64e179c40b791c49c242cf15f7e7fab15.th.png) --- **ข้อจำกัดความรับผิดชอบ**: เอกสารนี้ได้รับการแปลโดยใช้บริการแปลภาษา AI [Co-op Translator](https://github.com/Azure/co-op-translator) แม้ว่าเราจะพยายามให้การแปลมีความถูกต้อง แต่โปรดทราบว่าการแปลโดยอัตโนมัติอาจมีข้อผิดพลาดหรือความไม่ถูกต้อง เอกสารต้นฉบับในภาษาดั้งเดิมควรถือเป็นแหล่งข้อมูลที่เชื่อถือได้ สำหรับข้อมูลที่สำคัญ ขอแนะนำให้ใช้บริการแปลภาษามนุษย์ที่มีความเชี่ยวชาญ เราไม่รับผิดชอบต่อความเข้าใจผิดหรือการตีความผิดที่เกิดจากการใช้การแปลนี้