การสร้างแอปเชิงโต้ตอบด้วย Redux, RxJS และ Redux-Observable ใน React Native

เผยแพร่แล้ว: 2022-03-11

ในระบบนิเวศที่กำลังเติบโตของเว็บและแอพมือถือที่สมบูรณ์และทรงพลัง มีสถานะที่ต้องจัดการมากขึ้นเรื่อยๆ เช่น ผู้ใช้ปัจจุบัน รายการที่โหลด สถานะการโหลด ข้อผิดพลาด และอื่นๆ อีกมากมาย Redux เป็นวิธีแก้ปัญหาหนึ่งโดยการรักษาสถานะไว้ในวัตถุส่วนกลาง

ข้อจำกัดอย่างหนึ่งของ Redux คือไม่รองรับการทำงานแบบอะซิงโครนัสตั้งแต่แกะกล่อง ทางออกหนึ่งสำหรับสิ่งนี้คือ redux-observable ซึ่งอิงตาม RxJS ซึ่งเป็นไลบรารีที่ทรงพลังสำหรับการเขียนโปรแกรมเชิงโต้ตอบใน JavaScript RxJS เป็นการนำ ReactiveX ซึ่งเป็น API สำหรับการเขียนโปรแกรมเชิงโต้ตอบซึ่งมีต้นกำเนิดมาจาก Microsoft ReactiveX รวมคุณลักษณะที่ทรงพลังที่สุดของกระบวนทัศน์ปฏิกิริยา การเขียนโปรแกรมเชิงฟังก์ชัน รูปแบบผู้สังเกต และรูปแบบการวนซ้ำ

ในบทช่วยสอนนี้ เราจะเรียนรู้เกี่ยวกับ Redux และการใช้งานด้วย React นอกจากนี้เรายังจะสำรวจการเขียนโปรแกรมเชิงโต้ตอบโดยใช้ RxJS และวิธีที่มันจะทำให้งานอะซิงโครนัสที่น่าเบื่อและซับซ้อนนั้นง่ายมาก

สุดท้าย เราจะเรียนรู้ redux-observable ซึ่งเป็นไลบรารีที่ใช้ประโยชน์จาก RxJS ในการทำงานแบบอะซิงโครนัส จากนั้นจะสร้างแอปพลิเคชันใน React Native โดยใช้ Redux และ redux-observable

Redux

ตามที่อธิบายตัวเองบน GitHub Redux เป็น "คอนเทนเนอร์สถานะที่คาดเดาได้สำหรับแอป JavaScript" โดยให้แอป JavaScript ของคุณมีสถานะสากล โดยทำให้สถานะและการดำเนินการอยู่ห่างจากส่วนประกอบ React

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

สามารถใช้ Redux ใน React ได้โดยใช้ react-redux ซึ่งจัดเตรียมการเชื่อมโยงสำหรับส่วนประกอบ React เพื่ออ่านข้อมูลจาก Redux และส่งการดำเนินการเพื่ออัปเดตสถานะ Redux

Redux

Redux สามารถอธิบายได้เป็นหลักการง่ายๆ สามประการ:

1. แหล่งเดียวแห่งความจริง

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

 » console.log(store.getState()) « { user: {...}, todos: {...} }

ในการอ่านข้อมูลจาก Redux ในองค์ประกอบ React ของคุณ เราใช้ฟังก์ชั่นการ connect จาก react-redux connect รับสี่อาร์กิวเมนต์ ซึ่งทั้งหมดเป็นทางเลือก สำหรับตอนนี้ เราจะเน้นที่อันแรกที่เรียกว่า mapStateToProps

 /* UserTile.js */ import { connect } from 'react-redux'; class UserTile extends React.Component { render() { return <p>{ this.props.user.name }</p> } } function mapStateToProps(state) { return { user: state.user } } export default connect(mapStateToProps)(UserTile)

ในตัวอย่างข้างต้น mapStateToProps ได้รับสถานะ Global Redux เป็นอาร์กิวเมนต์แรกและส่งคืนอ็อบเจ็กต์ที่จะรวมเข้ากับอุปกรณ์ประกอบที่ส่งผ่านไปยัง <UserTile /> โดยองค์ประกอบหลัก

2. สถานะเป็นแบบอ่านอย่างเดียว

สถานะ Redux เป็นแบบอ่านอย่างเดียวสำหรับส่วนประกอบ React และวิธีเดียวในการเปลี่ยนสถานะคือการปล่อยการ กระทำ การกระทำเป็นวัตถุธรรมดาที่แสดงถึงความตั้งใจที่จะเปลี่ยนสถานะ ทุกอ็อบเจ็กต์แอคชันต้องมีฟิลด์ type และค่าต้องเป็นสตริง นอกจากนั้น เนื้อหาของการกระทำนั้นขึ้นอยู่กับคุณโดยสิ้นเชิง แต่แอพส่วนใหญ่ใช้รูปแบบการกระทำมาตรฐานแบบฟลักซ์ ซึ่งจำกัดโครงสร้างของการกระทำไว้เพียงสี่ปุ่มเท่านั้น:

  1. type ตัวระบุสตริงใด ๆ สำหรับการดำเนินการ ทุกการกระทำต้องมีการกระทำที่ไม่ซ้ำกัน
  2. payload เสริมสำหรับการดำเนินการใดๆ สามารถเป็นได้ตลอดเวลาและมีข้อมูลเกี่ยวกับการดำเนินการ
  3. error คุณสมบัติบูลีนที่เป็นทางเลือกใด ๆ ที่ตั้งค่าเป็นจริงหากการดำเนินการแสดงถึงข้อผิดพลาด นี่คล้ายกับ Promise. string ที่ถูกปฏิเสธ ตัวระบุ Promise. string สำหรับการดำเนินการ ทุกการกระทำต้องมีการกระทำที่ไม่ซ้ำกัน ตามธรรมเนียม เมื่อ error เป็น true เพย์ payload ควรเป็นอ็อบเจ็กต์ข้อผิดพลาด
  4. meta Meta สามารถเป็นค่าประเภทใดก็ได้ มีไว้สำหรับข้อมูลเพิ่มเติมที่ไม่ได้เป็นส่วนหนึ่งของเพย์โหลด

ต่อไปนี้คือตัวอย่างการดำเนินการสองตัวอย่าง:

 store.dispatch({ type: 'GET_USER', payload: '21', }); store.dispatch({ type: 'GET_USER_SUCCESS', payload: { user: { id: '21', name: 'Foo' } } });

3. สถานะเปลี่ยนไปด้วย Pure Functions

สถานะ Redux ทั่วโลกมีการเปลี่ยนแปลงโดยใช้ฟังก์ชันบริสุทธิ์ที่เรียกว่ารีดิวเซอร์ ตัวลดใช้สถานะก่อนหน้าและการดำเนินการและส่งกลับสถานะถัดไป ตัวลดจะสร้างวัตถุสถานะใหม่แทนการกลายพันธุ์ที่มีอยู่ ร้านค้า Redux สามารถมีตัวลดเดี่ยวหรือตัวลดหลายตัวทั้งนี้ขึ้นอยู่กับขนาดแอพ

 /* store.js */ import { combineReducers, createStore } from 'redux' function user(state = {}, action) { switch (action.type) { case 'GET_USER_SUCCESS': return action.payload.user default: return state } } function todos(state = [], action) { switch (action.type) { case 'ADD_TODO_SUCCESS': return [ ...state, { id: uuid(), // a random uuid generator function text: action.text, completed: false } ] case 'COMPLETE_TODO_SUCCESS': return state.map(todo => { if (todo.id === action.id) { return { ...todo, completed: true } } return todo }) default: return state } } const rootReducer = combineReducers({ user, todos }) const store = createStore(rootReducer)

คล้ายกับการอ่านจากสถานะ เราสามารถใช้ฟังก์ชัน connect เพื่อส่งการดำเนินการ

 /* UserProfile.js */ class Profile extends React.Component { handleSave(user) { this.props.updateUser(user); } } function mapDispatchToProps(dispatch) { return ({ updateUser: (user) => dispatch({ type: 'GET_USER_SUCCESS', user, }), }) } export default connect(mapStateToProps, mapDispatchToProps)(Profile);

RxJS

RxJS

การเขียนโปรแกรมเชิงโต้ตอบ

การเขียนโปรแกรมเชิงโต้ตอบเป็นกระบวนทัศน์การเขียนโปรแกรมที่เปิดเผยซึ่งเกี่ยวข้องกับการไหลของข้อมูลใน " สตรีม " และด้วยการขยายพันธุ์และการเปลี่ยนแปลง RxJS ไลบรารี่สำหรับการเขียนโปรแกรมเชิงโต้ตอบใน JavaScript มีแนวคิดเรื่อง observables ซึ่งเป็นสตรีมข้อมูลที่ผู้สังเกตการณ์สามารถ สมัครรับข้อมูล ได้ และผู้สังเกตการณ์รายนี้จะส่งข้อมูลเมื่อเวลาผ่านไป

ผู้สังเกตการณ์ที่สังเกตได้คือวัตถุที่มีสามหน้าที่: next error และ complete ฟังก์ชันทั้งหมดนี้เป็นทางเลือก

 observable.subscribe({ next: value => console.log(`Value is ${value}`), error: err => console.log(err), complete: () => console.log(`Completed`), })

ฟังก์ชัน .subscribe สามารถมีได้สามฟังก์ชันแทนที่จะเป็นอ็อบเจ็กต์

 observable.subscribe( value => console.log(`Value is ${value}`), err => console.log(err), () => console.log(`Completed`) )

เราสามารถสร้าง observable ใหม่ได้โดยการสร้างอ็อบเจ็กต์ของ observable ส่งผ่านฟังก์ชันที่รับสมาชิก aka ผู้สังเกตการณ์ สมาชิกมีสามวิธี: next error และ complete สมาชิกสามารถเรียกค่าถัดไปได้มากเท่าที่ต้องการและ complete หรือ error ในที่สุด หลังจากเรียก complete หรือ error ค่าที่สังเกตได้จะไม่ส่งค่าใด ๆ ลงไปที่สตรีม

 import { Observable } from 'rxjs' const observable$ = new Observable(function subscribe(subscriber) { const intervalId = setInterval(() => { subscriber.next('hi'); subscriber.complete() clearInterval(intervalId); }, 1000); }); observable$.subscribe( value => console.log(`Value is ${value}`), err => console.log(err) )

ตัวอย่างด้านบนจะพิมพ์ Value is hi หลังจาก 1,000 มิลลิวินาที

การสร้างสิ่งที่สังเกตได้ด้วยตนเองทุกครั้งอาจกลายเป็นเรื่องละเอียดและน่าเบื่อ ดังนั้น RxJS จึงมีฟังก์ชันมากมายในการสร้างสิ่งที่สังเกตได้ ตัวที่ใช้บ่อยที่สุดคือ of , from และ ajax

ของ

of ใช้ลำดับของค่าและแปลงเป็นสตรีม:

 import { of } from 'rxjs' of(1, 2, 3, 'Hello', 'World').subscribe(value => console.log(value)) // 1 2 3 Hello World

จาก

from การแปลงเกือบทุกอย่างเป็นกระแสของค่า:

 import { from } from 'rxjs' from([1, 2, 3]).subscribe(console.log) // 1 2 3 from(new Promise.resolve('Hello World')).subscribe(console.log) // 'Hello World' from(fibonacciGenerator).subscribe(console.log) // 1 1 2 3 5 8 13 21 ...

ajax

ajax ใช้ URL สตริงหรือสร้างสิ่งที่สังเกตได้ซึ่งส่งคำขอ HTTP ajax มีฟังก์ชัน ajax.getJSON ซึ่งส่งคืนเฉพาะวัตถุตอบกลับที่ซ้อนกันจากการเรียก AJAX โดยไม่มีคุณสมบัติอื่นใดที่ส่งคืนโดย ajax() :

 import { ajax } from 'rxjs/ajax' ajax('https://jsonplaceholder.typicode.com/todos/1').subscribe(console.log) // {request, response: {userId, id, title, completed}, responseType, status} ajax.getJSON('https://jsonplaceholder.typicode.com/todos/1').subscribe(console.log) // {userId, id, title, completed} ajax({ url, method, headers, body }).subscribe(console.log) // {...}

มีหลายวิธีที่จะทำให้สังเกตได้ (คุณสามารถดูรายการทั้งหมดได้ที่นี่)

ผู้ประกอบการ

โอเปอเรเตอร์คือขุมพลังที่แท้จริงของ RxJS ซึ่งมีโอเปอเรเตอร์สำหรับเกือบทุกอย่างที่คุณต้องการ ตั้งแต่ RxJS 6 ตัวดำเนินการไม่ใช่เมธอดบนอ็อบเจ็กต์ที่สังเกตได้ แต่เป็นฟังก์ชันล้วนๆ ที่ใช้กับสิ่งที่สังเกตได้โดยใช้เมธอด . .pipe

แผนที่

map ใช้ฟังก์ชันอาร์กิวเมนต์เดียวและใช้การฉายภาพกับแต่ละองค์ประกอบในสตรีม:

 import { of } from 'rxjs' import { map } from 'rxjs/operators' of(1, 2, 3, 4, 5).pipe( map(i=> i * 2) ).subscribe(console.log) // 2, 4, 6, 8, 10 

แผนที่

กรอง

filter กรองรับอาร์กิวเมนต์เดียวและลบค่าออกจากสตรีมที่คืนค่าเท็จสำหรับฟังก์ชันที่กำหนด:

 import { of } from 'rxjs' import { map, filter } from 'rxjs/operators' of(1, 2, 3, 4, 5).pipe( map(i => i * i), filter(i => i % 2 === 0) ).subscribe(console.log) // 4, 16 

กรอง

แผนที่แบน

ตัวดำเนินการ flatMap ใช้ฟังก์ชันที่แมปทุกรายการใน Steam ไปยังสตรีมอื่น และทำให้ค่าทั้งหมดของสตรีมเหล่านี้แบนลง:

 import { of } from 'rxjs' import { ajax } from 'rxjs/ajax' import { flatMap } from 'rxjs/operators' of(1, 2, 3).pipe( flatMap(page => ajax.toJSON(`https://example.com/blog?size=2&page=${page}`)), ).subscribe(console.log) // [ { blog 1 }, { blog 2 }, { blog 3 }, { blog 4 }, { blog 5 }, { blog 6 } ] 

FlatMap

ผสาน

การ merge รวมรายการจากสองสตรีมตามลำดับที่มาถึง:

 import { interval, merge } from 'rxjs' import { pipe, take, mapTo } from 'rxjs/operators' merge( interval(150).pipe(take(5), mapTo('A')), interval(250).pipe(take(5), mapTo('B')) ).subscribe(console.log) // ABAABAABBB 

ผสาน

รายชื่อโอเปอเรเตอร์ทั้งหมดมีอยู่ที่นี่

Redux-สังเกตได้

Redux-สังเกตได้

จากการออกแบบ การดำเนินการทั้งหมดใน Redux เป็นแบบซิงโครนัส Redux-observable เป็นมิดเดิลแวร์สำหรับ Redux ที่ใช้สตรีมที่สังเกตได้เพื่อทำงานแบบอะซิงโครนัส แล้วส่งการดำเนินการอื่นใน Redux ด้วยผลลัพธ์ของการทำงานแบบอะซิงโครนัสนั้น

Redux-observable มีพื้นฐานมาจากแนวคิดของ Epics มหากาพย์คือหน้าที่ที่ใช้กระแสของการกระทำ และทางเลือกคือกระแสของรัฐและส่งกลับกระแสของการกระทำ

ฟังก์ชั่น (action$: สังเกตได้ , state$: StateObservable ): สังเกตได้ ;

ตามธรรมเนียม ทุกตัวแปรที่เป็นสตรีม (_aka _observable ) ลงท้ายด้วย $ ก่อนที่เราจะสามารถใช้ redux-observable ได้ เราต้องเพิ่มเป็นมิดเดิลแวร์ในร้านของเราเสียก่อน เนื่องจากมหากาพย์เป็นสายธารของสิ่งที่สังเกตได้ และทุกการกระทำที่ออกจาก Steam นี้จะถูกส่งกลับไปยังสตรีม การส่งคืนการกระทำเดิมจะส่งผลให้เกิดการวนซ้ำไม่รู้จบ

 const epic = action$ => action$.pipe( filter(action => action.type === 'FOO'), mapTo({ type: 'BAR' }) // not changing the type of action returned // will also result in an infinite loop ) // or import { ofType } from 'redux-observable' const epic = action$ => action$.pipe( ofType('FOO'), mapTo({ type: BAZ' }) )

คิดว่าสถาปัตยกรรมแบบรีแอกทีฟนี้เหมือนกับระบบของไพพ์ที่เอาต์พุตของไพพ์ทุกอันป้อนกลับเข้าไปในทุกไพพ์ รวมทั้งตัวมันเอง และรวมถึงรีดิวเซอร์ของ Redux ด้วย ตัวกรองที่อยู่ด้านบนของท่อเหล่านี้จะกำหนดสิ่งที่เข้าไปและสิ่งที่ถูกบล็อก

เรามาดูกันว่ามหากาพย์ปิงปองจะทำงานอย่างไร ต้องใช้ ping ส่งไปยังเซิร์ฟเวอร์ และหลังจากคำขอเสร็จสิ้น จะส่ง pong กลับไปที่แอป

 const pingEpic = action$ => action$.pipe( ofType('PING'), flatMap(action => ajax('https://example.com/pinger')), mapTo({ type: 'PONG' }) ) Now, we are going to update our original todo store by adding epics and retrieving users. import { combineReducers, createStore } from 'redux' import { ofType, combineEpics, createEpicMiddleware } from 'redux-observable'; import { map, flatMap } from 'rxjs/operators' import { ajax } from 'rxjs/ajax' // ... /* user and todos reducers defined as above */ const rootReducer = combineReducers({ user, todos }) const epicMiddleware = createEpicMiddleware(); const userEpic = action$ => action$.pipe( ofType('GET_USER'), flatMap(() => ajax.getJSON('https://foo.bar.com/get-user')), map(user => ({ type: 'GET_USER_SUCCESS', payload: user })) ) const addTodoEpic = action$ => action$.pipe( ofType('ADD_TODO'), flatMap(action => ajax({ url: 'https://foo.bar.com/add-todo', method: 'POST', body: { text: action.payload } })), map(data => data.response), map(todo => ({ type: 'ADD_TODO_SUCCESS', payload: todo })) ) const completeTodoEpic = action$ => action$.pipe( ofType('COMPLETE_TODO'), flatMap(action => ajax({ url: 'https://foo.bar.com/complete-todo', method: 'POST', body: { id: action.payload } })), map(data => data.response), map(todo => ({ type: 'COMPLEE_TODO_SUCCESS', payload: todo })) ) const rootEpic = combineEpics(userEpic, addTodoEpic, completeTodoEpic) const store = createStore(rootReducer, applyMiddleware(epicMiddleware)) epicMiddleware.run(rootEpic);

_ข้อสำคัญ: Epics เหมือนกับสตรีมที่สังเกตได้อื่นๆ ใน RxJS พวกเขาสามารถจบลงในสถานะสมบูรณ์หรือข้อผิดพลาด หลังจากสถานะนี้ มหากาพย์และแอปของคุณจะหยุดทำงาน ดังนั้นคุณต้องจับทุกข้อผิดพลาดที่อาจเกิดขึ้นใน Steam คุณสามารถใช้ตัวดำเนินการ __catchError__ สำหรับสิ่งนี้ ข้อมูลเพิ่มเติม: การจัดการข้อผิดพลาดในไฟล์ redux-observable

แอป Reactive Todo

เมื่อเพิ่ม UI บางส่วน แอปสาธิต (ขั้นต่ำ) จะมีลักษณะดังนี้:

แอป Reactive Todo
ซอร์สโค้ดสำหรับแอพนี้มีอยู่ใน Github ลองใช้โปรเจ็กต์ในงาน Expo หรือสแกนโค้ด QR ด้านบนในแอป Expo

สรุปบทช่วยสอน React, Redux และ RxJS

เราได้เรียนรู้ว่าแอปรีแอคทีฟคืออะไร นอกจากนี้เรายังได้เรียนรู้เกี่ยวกับ Redux, RxJS และ redux-observable และสร้างแอป Todo แบบรีแอกทีฟใน Expo ด้วย React Native สำหรับนักพัฒนา React และ React Native แนวโน้มปัจจุบันเสนอตัวเลือกการจัดการสถานะที่ทรงพลังมาก

ซอร์สโค้ดสำหรับแอปนี้อยู่บน GitHub อีกครั้ง อย่าลังเลที่จะแบ่งปันความคิดของคุณเกี่ยวกับการจัดการสถานะสำหรับแอปปฏิกิริยาในความคิดเห็นด้านล่าง