在 React Native 中使用 Redux、RxJS 和 Redux-Observable 構建反應式應用程序

已發表: 2022-03-11

在日益豐富且功能強大的 Web 和移動應用程序的生態系統中,需要管理的狀態越來越多,例如當前用戶、加載的項目列表、加載狀態、錯誤等等。 Redux 是通過將狀態保存在全局對像中來解決此問題的一種方法。

Redux 的限制之一是它不支持開箱即用的異步行為。 一個解決方案是redux-observable ,它基於 RxJS,一個強大的 JavaScript 響應式編程庫。 RxJS 是 ReactiveX 的一個實現,ReactiveX 是一種源自微軟的反應式編程 API。 ReactiveX 結合了反應範式、函數式編程、觀察者模式和迭代器模式的一些最強大的特性。

在本教程中,我們將了解 Redux 及其與 React 的用法。 我們還將探索使用 RxJS 的響應式編程,以及它如何讓繁瑣而復雜的異步工作變得非常簡單。

最後,我們將學習 redux-observable,這是一個利用 RxJS 進行異步工作的庫,然後將使用 Redux 和 redux-observable 在 React Native 中構建一個應用程序。

還原

正如它在 GitHub 上描述的那樣,Redux 是“JavaScript 應用程序的可預測狀態容器”。 它為您的 JavaScript 應用程序提供全局狀態,使狀態和操作遠離 React 組件。

在沒有 Redux 的典型 React 應用程序中,我們必須通過屬性或props將數據從根節點傳遞給子節點。 這種數據流對於小型應用程序是可管理的,但隨著應用程序的增長會變得非常複雜。 Redux 允許我們擁有彼此獨立的組件,因此我們可以將其用作單一事實來源。

Redux 可以使用react-redux在 React 中使用,它為 React 組件提供綁定以從 Redux 讀取數據並調度操作以更新 Redux 狀態。

還原

Redux 可以描述為三個簡單的原則:

1. 單一真理來源

整個應用程序的狀態存儲在單個對像中。 Redux 中的這個對象由 store 持有。 任何 Redux 應用程序中都應該有一個商店。

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

要在 React 組件中從 Redux 讀取數據,我們使用react-reduxconnect函數。 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接收全局 Redux 狀態作為其第一個參數,並返回一個對象,該對象將與其父組件傳遞給<UserTile />的 props 合併。

2. 狀態是只讀的

Redux 狀態對於 React 組件是只讀的,改變狀態的唯一方法是發出一個動作。 動作是一個簡單的對象,表示改變狀態的意圖。 每個動作對像都必須有一個type字段,並且值必須是一個字符串。 除此之外,動作的內容完全取決於您,但大多數應用程序都遵循通量標準動作格式,這將動作的結構限制為只有四個鍵:

  1. type操作的任何字符串標識符。 每個動作都必須有一個獨特的動作。
  2. payload任何操作的可選數據。 它可以是任何時間並包含有關操作的信息。
  3. error如果操作表示錯誤,則任何可選的布爾屬性都設置為 true。 這類似於被拒絕的Promise. string 操作的Promise. string標識符。 每個動作都必須有一個獨特的動作。 按照慣例,當errortrue時, payload應該是一個錯誤對象。
  4. meta Meta 可以是任何類型的值。 它適用於不屬於有效負載的任何額外信息。

以下是兩個動作示例:

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

3. 用純函數改變狀態

全局 Redux 狀態是使用稱為 reducer 的純函數更改的。 reducer 接受前一個狀態和動作並返回下一個狀態。 reducer 創建一個新的狀態對象,而不是改變現有的狀態對象。 根據應用程序的大小,一個 Redux 商店可以有一個 reducer 或多個 reducer。

 /* 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的概念,它是觀察者可以訂閱的數據流,並且這個觀察者隨著時間的推移傳遞數據。

observable 的觀察者是一個具有三個功能的對象: nexterrorcomplete 。 所有這些功能都是可選的。

 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 ,並傳入一個接收訂閱者(也稱為觀察者)的函數。 訂閱者有三種方法: nexterrorcomplete 。 訂閱者可以根據需要多次調用 next ,並最終completeerror 。 在調用completeerror之後,observable 不會將任何值推送到流中。

 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) )

上面的示例將在 1000 毫秒後打印Value is hi

每次手動創建 observable 會變得冗長乏味。 因此,RxJS 有很多函數可以創建 observable。 一些最常用的是offromajax

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接受一個字符串 URL 或創建一個發出 HTTP 請求的 observable。 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) // {...}

還有更多方法可以製作可觀察的(您可以在此處查看完整列表)。

運營商

Operators 是 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採用單個參數並從流中刪除對給定函數返回 false 的值:

 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運算符採用一個函數,將流中的每個項目映射到另一個流中,並將這些流的所有值展平:

 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 } ] 

平面圖

合併

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-Observable

Redux-Observable

按照設計,Redux 中的所有操作都是同步的。 Redux-observable 是 Redux 的一個中間件,它使用 observable 流來執行異步工作,然後在 Redux 中用異步工作的結果調度另一個動作。

Redux-observable 是基於Epics的思想。 史詩是一個函數,它接受一個動作流,也可以選擇一個狀態流並返回一個動作流。

函數 (action$: Observable , state$: StateObservable ): 可觀察的;

按照慣例,每個作為流的變量(_aka _observable )都以$結尾。 在我們可以使用 redux-observable 之前,我們必須將它作為中間件添加到我們的 store 中。 由於史詩是可觀察的流,並且退出此流的每個動作都通過管道返回到流中,因此返回相同的動作將導致無限循環。

 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 的 reducer。 正是這些管道頂部的過濾器決定了什麼進入,什麼被阻塞。

讓我們看看乒乓史詩是如何運作的。 它需要一個 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);

_Important:Epics 就像 RxJS 中的任何其他可觀察流一樣。 它們最終可能處於完整或錯誤狀態。 在此狀態之後,史詩和您的應用程序將停止工作。 因此,您必須抓住蒸汽中的每一個潛在錯誤。 您可以為此使用__catchError__運算符。 更多信息: redux-observable 中的錯誤處理

反應式待辦事項應用程序

添加一些 UI 後,一個(最小的)演示應用程序看起來像這樣:

反應式待辦事項應用程序
這個應用程序的源代碼可以在 Github 上找到。 在 Expo 上試用該項目或在 Expo 應用程序中掃描上面的二維碼。

React、Redux 和 RxJS 教程總結

我們了解了什麼是響應式應用程序。 我們還學習了 Redux、RxJS 和 redux-observable,甚至使用 React Native 在 Expo 中創建了一個響應式 Todo 應用程序。 對於 React 和 React Native 開發人員來說,當前的趨勢提供了一些非常強大的狀態管理選項。

再一次,這個應用程序的源代碼在 GitHub 上。 歡迎在下面的評論中分享您對反應式應用程序狀態管理的看法。