Redux for react native 指南(持續更新)
如果要看理論的童鞋點選這裡redux中文文件 或者redux官方文件 ,本文不會太刻意去介紹大篇幅的理論,本文不做框架之間的對比,只給想學 redux
的童鞋提供實質的、高效的、易理解的學習參考資源,分享自己在學習過程中的得到(文後有彩蛋 :smile:)。
老規矩先上圖:

react native
版。
為什麼我要寫這個demo
有的童鞋可能會有疑問 問:官方不是 Todos
demo嗎?為什麼還要寫這個demo? 答:官方的demo都是 react
的,而並非 react native
的。我也找過很多關於介紹redux的文章,但我發現找到的資料要麼太基礎、要麼介紹不全面、提供的demo下載無法使用等等各種問題,迫使我有了自己動手造輪的衝動,而且這個demo並非只是介紹關於redux的基礎的東西,有空我還沒陸續更新在使用 redux
過程中的得到,希望大家鼓勵支援。
demo採的程式碼規範
通常一個大專案到後期是需要很多開發者參與的,如果每個開發者都使用自己的一套程式碼規範做事情,這樣帶來的後果就是:後期的程式碼管理工作帶來非常大的麻煩,浪費更多的時間去重構,而且也會讓新人看程式碼時理解花更多的時間,還容易把別人帶溝裡去,所以一個大型專案最初構建架構的時候就必須要遵守一些規範。 那麼我們怎麼能敲出清爽而又優雅的程式碼呢?又如何檢查我們程式碼質量合格呢? 我在這裡極力推薦遵守 ofollow,noindex">airbnb/javascript 的規範和使用 eslint 來檢查自己程式碼的程式碼質量(是否遵守了規範),因為它們已經得到了很多公司和開發者的認可。(這裡過多的介紹 airbnb
eslint
,本文只提供思路,想了解更多自行搜尋) 在沒有使用程式碼規範前我們可能用各自的風格寫了很多年的程式碼了,突然要適應這套規範可能非常不適應,沒關係,多敲多練習,時間長了就習慣了,誰還沒有一個過程,過程是痛苦的,但痛苦過後會給你帶來質的昇華,自己慢慢領悟體會。 如果還是堅持我行我素,那我也沒辦法的,我只能說好的東西是會被世界所接受,差的東西最終是要被淘汰的,所以做為一個合格的程式員(特別是前端程式設計師)要擁抱變化,因為它會使你變得更加的優秀,得到大眾的認可。除非你不願意讓自己變得更優秀。(我回看幾年前我寫的程式碼,只能用一坨一坨的來形容 ,不知道你們是否也有這種感腳呢?:grin:)
redux能幫我們做什麼
兩張圖示意:


redux特性
-
單一資料來源: 整個應用的 state被儲存在一棵 object tree 中,並且這個 object tree 只存在於唯一一個 store中。
-
State 是隻讀的:唯一改變 state 的方法就是觸發 action,action 是一個用於描述已發生事件的普通物件。
-
使用純函式來執行修改:為了描述 action 如何改變 state tree ,你需要編寫 reducers。
-
預見性:所有的使用者的行為都是你提前定義好的。
-
統一管理state:所有的狀態都在一個store中分配管理。
哪些開發者和專案適合用redux
這裡只針對 react native
開發而言:
-
初級:剛接觸
react native
我非常不建議去使用,因為你還不知道怎麼用它,建議先達到中級。 -
中級:使用
react native
做出一個以上已經上架的不復雜
的應用redux
,也可以不使用,因為使用它並不能讓你在前期快速的迭代開發,在這樣的專案下使用redux
就好比大炮打蚊子
,副作用很大。但是可以先了解起來,並發現它的優點。這類相對簡單的應用:當用戶觸發一個動作(程式需要setState({xxx:xxx})
)的時候應用程式狀態流程是這樣的: -
高階:使用
react native
做出一個以上已經上架的複雜
的應用(涉及到即時通訊、介面佈局比較複雜,元件巢狀太多層次等),而這類複雜應用:當用戶觸發一個動作(程式需要setState({xxx:xxx})
)的時候應用程式狀態流程是這樣的:
這種狀態帶來的後果,兩方面分析:
- 效能:祖父子元件之間多餘的狀態傳遞,導致寶貴的記憶體資源浪費,同時介面渲染的速度也會變慢,自然使用者體驗就變差了。
- 狀態管理:當程式不斷的迭代,介面佈局越來越複雜,必然就會產生許多的
state
狀態,那你是如何有效的管理這些狀態?是什麼原因導致UI多次渲染?是哪一步操作導致的UI元件的變化?在沒有使用redux
前你可能已經發現可以使用生命週期函式中的shouldComponentUpdate
來減少子元件中沒必要的渲染,但終究解決不了狀態管理複雜的難題。 當你使用redux
後,複雜的應用程式狀態流程是這樣的:redux
呢?這要感謝 @justjavac 文章提供的動圖支援。
redux for react native 工作邏輯圖
感謝@黑森林工作室作者提供的清晰的邏輯圖

redux工程結構分析
我對官方的demo小部分位置做了些改造具體看程式碼分析:

分工明細
-
js/actions
此資料夾下放內容做的事情是:定義使用者行為。 -
js/reducers
此資料夾下放內容做的事情是:響應使用者行為,返回改變後的狀態,併發送到store
。 -
js/components
此資料夾下放內容做的事情是:自定義的元件。 -
js/containers
此資料夾下放內容做的事情是:把components
資料夾中涉及到狀態變化的元件進行第二次封裝。 -
App.js
入口檔案(store在這裡),為什麼我要把store定義在這裡? 因為它是唯一的,而且必須使用react-redux
提供的Provider
元件包裹入口的其他元件才能使redux
中的store
生效。 -
global.js
存放全域性定義的變數、常量、方法等。
需要注意的事
- 一個工程中
redux
的store
是唯一的,不能在多個store
。 - 保持
reducer
純淨非常重要。永遠不要在reducer
裡做這些操作:
- 修改傳入引數;
- 執行有副作用的操作,如
API
請求和路由跳轉; - 呼叫非純函式,如
Date.now()
或Math.random()
;
- 使用物件展開運算子
...
代替Object.assign()
才是最好的解決方案。 - 元件名首字母要大寫,也就是說
components
和containers
資料夾下的檔案首字母都要大寫。 - 應該儘量減少傳遞到
action
中的資料(能傳單個數據就不傳物件,能傳物件就不傳陣列)
//good function todoApp(state = initialState, action) { switch (action.type) { case SET_VISIBILITY_FILTER: return Object.assign({}, state, { visibilityFilter: action.filter }) default: return state } } 複製程式碼
//best function todoApp(state = initialState, action) { switch (action.type) { case SET_VISIBILITY_FILTER: return { ...state, visibilityFilter: action.filter } default: return state } } 複製程式碼
#程式碼詳解 js/actions/types.js
//新增列表資料 export const ADD_TODO = 'ADD_TODO'; //篩選 export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER'; //文字新增/取消中劃線 export const TOGGLE_TODO = 'TOGGLE_TODO'; 複製程式碼
釋:
action定義
為什麼我要把使用者的 action
(行為)定義單獨抽出來寫一個 type.js
?
- 方便狀態管理。
- 複用性。
js/actions/index.js
import{ ADD_TODO, SET_VISIBILITY_FILTER, TOGGLE_TODO, } from './types' let nextTodoId = 0; export const addTodo = text => ({ type: ADD_TODO, id: nextTodoId++, text }); export const setVisibilityFilter = (filter) => ({ type: SET_VISIBILITY_FILTER, filter }); export const toggleTodo = id => ({ type: TOGGLE_TODO, id }); 複製程式碼
釋:
Action 建立函式
Action 建立函式 就是生成 action 的方法。“action” 和 “action 建立函式” 這兩個概念很容易混在一起,使用時最好注意區分。
在 Redux 中的 action 建立函式只是簡單的返回一個 action:
js/reducers/todos.js
import{ ADD_TODO, TOGGLE_TODO, } from '../actions/types' const todos = (state = [], action) => { let {id, text, type} = action; switch (type) { case ADD_TODO: return [ ...state, { id: id, text: text, completed: false } ]; case TOGGLE_TODO: return state.map(todo => (todo.id === id) ? {...todo, completed: !todo.completed} : todo); default: return state; } }; exportdefaulttodos; 複製程式碼
js/reducers/visibilityFilter.js
import { SET_VISIBILITY_FILTER } from '../actions/types' import { visibilityFilters } from '../global' const { SHOW_ALL } = visibilityFilters; const visibilityFilter = (state = SHOW_ALL, action) => { let {type, filter} = action; switch (type){ case SET_VISIBILITY_FILTER: return filter; default: return state } }; export defaultvisibilityFilter; 複製程式碼
釋:
reducer 就是一個純函式,接收舊的 state 和 action,返回新的 state(上面兩個檔案可以看著兩個reducer)。
注意:
-
Redux
首次執行時,state
為undefined
,此時需要設定返回應用的初始state
。 - 每個
reducer
只負責管理全域性state
中它負責的一部分。每個reducer
的state
引數都不同,分別對應它管理的那部分state
資料。
js/reducers/index.js
import { combineReducers } from 'redux' import todos from './todos' import visibilityFilter from './visibilityFilter' export default combineReducers({ todos, visibilityFilter }) 複製程式碼
釋:
combineReducers()
所做的只是生成一個函式,這個函式來呼叫你的一系列 reducer,每個 reducer 根據它們的 key 來篩選出 state 中的一部分資料並處理,然後這個生成的函式再將所有 reducer 的結果合併成一個大的物件。
表面上看上去 combineReducers()
的作用就是把多個 reducer
合成一個的 reducer
。
js/components/Todo.js
import React, { Component } from 'react' import { Text, TouchableOpacity } from 'react-native' import PropTypes from 'prop-types' export default class Todo extends Component { static propTypes = { onClick: PropTypes.func.isRequired, completed: PropTypes.bool.isRequired, text: PropTypes.string.isRequired }; render(){ let { onClick, completed, text } = this.props; return ( <TouchableOpacity style={{ flexDirection: 'row', flex: 1, height: 50, alignItems: 'center', justifyContent: 'center', backgroundColor: '#cccccc', marginTop: 10 }} onPress={onClick}> <Text style={{ textDecorationLine: completed ? 'line-through' : 'none'}}>{text}</Text> </TouchableOpacity> ); } } 複製程式碼
js/components/TodoList.js
import React, { Component } from 'react' import PropTypes from 'prop-types' import { FlatList } from 'react-native' import Todo from './Todo' export default class TodoList extends Component { static propTypes = { todos: PropTypes.arrayOf( PropTypes.shape({ id: PropTypes.number.isRequired, completed: PropTypes.bool.isRequired, text: PropTypes.string.isRequired }).isRequired ).isRequired, toggleTodo: PropTypes.func.isRequired }; _renderItem = (data) => { let dataItem = data.item; let { id } = dataItem; let { toggleTodo } = this.props; return ( <Todo {...dataItem} onClick={() => toggleTodo(id)} /> ) }; render() { let { todos } = this.props; return ( <FlatList data={todos} keyExtractor={(item)=>item.id.toString()} renderItem={this._renderItem} /> ) } } 複製程式碼
js/components/Link.js.js
import React, { Component } from 'react' import PropTypes from 'prop-types' import { TouchableOpacity, Text } from 'react-native' export default class Link extends Component { static propTypes = { active: PropTypes.bool.isRequired, filter: PropTypes.string.isRequired, onClick: PropTypes.func.isRequired }; render() { let { active,filter, onClick } = this.props; return ( <TouchableOpacity style={{ marginLeft: 4, height: 40, flex:1, borderWidth: 1, borderColor: '#ccc', alignItems: 'center', justifyContent:'center' }} onPress={onClick} > <Text style={{fontSize: 10, color: active ? 'black' : '#cccccc'}}>{filter}</Text> </TouchableOpacity> ); } } 複製程式碼
js/components/Filters.js
import React, { Component } from 'react' import { View, } from 'react-native' import FilterLink from '../containers/FilterLink' import { visibilityFilters } from '../global' const { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE} = visibilityFilters; export defaultclass Filters extends Component { render(){ return( <View style={{ flexDirection: 'row', marginTop: 20}}> <FilterLink filter={SHOW_ALL} /> <FilterLink filter={SHOW_COMPLETED} /> <FilterLink filter={SHOW_ACTIVE} /> </View> ) } } 複製程式碼

釋:
以上四個檔案是自定義的四個展示元件,這些元件只定義外觀並不關心資料來源和如何改變。傳入什麼就渲染什麼。如果你把程式碼從 Redux 遷移到別的架構,這些元件可以不做任何改動直接使用。它們並不依賴於 Redux。
js/containers/AddTodo.js
import React, { Component } from 'react' import { View, TextInput, Button, } from 'react-native' import { connect } from 'react-redux' import { addTodo } from '../actions' class AddTodo extends Component { constructor(props){ super(props); this.inputValue = ''; } render(){ let { dispatch } = this.props; return ( <View style={{flexDirection: 'row'}}> <TextInput style={{flex:1, borderWidth: 1, borderColor: '#cccccc', textAlign: 'center'}} onChangeText={text => this.inputValue = text} /> <Button title="Add Todo" onPress={() => dispatch(addTodo(this.inputValue))}/> </View> ) } } export default connect()(AddTodo) 複製程式碼
js/containers/FilterLink.js
import { connect } from 'react-redux' import { setVisibilityFilter } from '../actions' import Link from '../components/Link' const mapStateToProps = (state, ownProps) => ({ active: ownProps.filter === state.visibilityFilter, filterText: ownProps.filter }); const mapDispatchToProps = (dispatch, ownProps) => ({ onClick: () => dispatch(setVisibilityFilter(ownProps.filter)) }); export default connect( mapStateToProps, mapDispatchToProps, )(Link) 複製程式碼
js/containers/VisibleTodoList.js
import { connect } from 'react-redux' import { toggleTodo } from '../actions' import TodoList from '../components/TodoList' import { visibilityFilters } from '../global' const { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } = visibilityFilters; const getVisibleTodos = (todos, filter) => { switch (filter) { case SHOW_COMPLETED: return todos.filter(t => t.completed); case SHOW_ACTIVE: return todos.filter(t => !t.completed); case SHOW_ALL: return todos; default: throw new Error('Unknown filter: ' + filter) } }; const mapStateToProps = state => ({ todos: getVisibleTodos(state.todos, state.visibilityFilter) }); const mapDispatchToProps = dispatch => ({ toggleTodo: id => dispatch(toggleTodo(id)) }); export default connect( mapStateToProps, mapDispatchToProps, )(TodoList) 複製程式碼
釋:
以上三個是容器元件,作用是把展示元件連線到 Redux。
有時很難分清到底該使用容器元件還是展示元件。如這個小的元件:
-
AddTodo.js
含有“Add”按鈕 和 輸入框
技術上講可以把它分成兩個元件,但一開始就這麼做有點早。在一些非常小的元件裡混用容器和展示是可以的。當業務變複雜後,如何拆分就很明顯了。所以現在就使用混合型的吧。
上面出現了使用 react-redux
的 connect()
方法來把展示元件和容器元件關聯在一起,這個方法做了效能優化來避免很多不必要的重複渲染。(這樣你就不必為了效能而手動實現 React 效能優化建議中的 shouldComponentUpdate
方法。)
使用 connect()
前,需要先定義 mapStateToProps
這個函式來指定如何把當前 Redux store state
對映到展示元件的 props
中。例如, VisibleTodoList
需要計算傳到 TodoList
中的 todos
,所以定義了根據 state.visibilityFilter
來過濾 state.todos
的方法,並在 mapStateToProps
中使用。
除了讀取 state
,容器元件還能分發 action
。類似的方式,可以定義 mapDispatchToProps()
方法接收 dispatch()
方法並返回期望注入到展示元件的 props 中的回撥方法。例如,我們希望 VisibleTodoList
向 TodoList
元件中注入一個叫 onTodoClick
的 props ,還希望 onTodoClick
能分發 TOGGLE_TODO
這個 action
。 最後,使用 connect()
建立 VisibleTodoList
,並傳入這兩個函式。
js/components/Group.js
import React, { Component } from 'react' import { View } from 'react-native' import AddTodo from '../containers/AddTodo' import Filters from '../components/Filters' import VisibleTodoList from '../containers/VisibleTodoList' export default class Group extends Component { render() { return ( <View style={{paddingHorizontal: 20, paddingVertical: 44}}> <AddTodo/> <Filters/> <VisibleTodoList/> </View> ); } } 複製程式碼
釋:
Group.js 是把所有的關聯後的元件串起來,形成一個完整的介面。
App.js
import React, { Component } from 'react' import { createStore } from 'redux' import { Provider } from 'react-redux' import Group from './js/components/Group' import rootReducer from './js/reducers' export default class App extends Component { render() { const store = createStore(rootReducer); return ( <Provider store={store}> <Group /> </Provider> ); } } 複製程式碼
釋:
入口檔案傳入 Store
- 建立
store
傳入reducers
。 - 使用
Provider
元件包裹Group
元件,store
作為屬性傳入Provider
。
進行到這一步,程式碼分析完畢。本次寫作到此結束。我相信大家如果仔細看完的話,多多少少會有些收穫吧,如果demo看不太懂,那就跟著程式碼分析的思路多敲幾遍程式碼,也就理解了,有空我會繼續更新未完成的內容。
彩蛋
附上 github demo ReduxForReactNativeDemo 好使的話,別忘了給出寶貴的:heart::star:️,沒有比這個更能鼓舞人心的了 :joy:。最後歡迎:clap:指出錯誤或者釋出自己的見解探討,共勉。
待更新內容
- Middleware的使用。
- 配合react-navigation使用。
- ......