教程說(shuō)明
如何初始化一個(gè)redux Store
如何使用action+reducer來(lái)管理state
如何在react-native里更新ui
這個(gè)例子可能不是很具體,但是對(duì)于理解用法比較好(目前看到的例子都是counter)
PACKAGE.JSON
一些版本的東西(因?yàn)閞eact-redux暫時(shí)用3.x,原因看 this React Native issue)
PACKAGE.JSON
一些版本的東西(因?yàn)閞eact-redux暫時(shí)用3.x,原因看 [this React Native issue](https://github.com/facebook/react-native/issues/2985))
{
"name": "TodoList",
"version": "0.0.1",
"private": true,
"scripts": {
"start": "node_modules/react-native/packager/packager.sh"
},
"dependencies": {
"normalizr": "^1.4.0",
"react-native": "^0.14.2",
"react-redux": "^3.0.1",
"redux": "^3.0.4",
"redux-thunk": "^1.0.0"
}}
目錄結(jié)構(gòu)
.
├── index.ios.js
├── index.android.js
└── src
├── actions //存放Actions
├── containers //UI && Component
└── reducers //存放Reducers
首先index.io.js改下入口:
/**
* Sample React Native App
* https://github.com/facebook/react-native
*/
'use strict';
import React from 'react-native';
import App from './src/containers/App';
var {
AppRegistry,
} = React;
var TodoList = React.createClass({
render: function() {
return (
<App />
);
}
});
AppRegistry.registerComponent('TodoList', () => TodoList);
把我們的入口設(shè)置成 App.js.
現(xiàn)在來(lái)創(chuàng)建App.js文件,位于./src/containers/App.js :
import React, { Component, View, Text } from 'react-native';
import { Provider } from 'react-redux/native';
import { createStore, applyMiddleware, combineReducers } from 'redux';
import thunk from 'redux-thunk';
import * as reducers from '../reducers';
import BaseApp from './BaseApp';
//apply thunk
const createStoreWithThunk = applyMiddleware(thunk)(createStore);
const reducer = combineReducers(reducers);
const store = createStoreWithThunk(reducer);
export default class App extends Component {
render() {
return (
<Provider store={store}>
{ () => <BaseApp /> }
</Provider>
);
}
}
先來(lái)解釋這個(gè)地方具體做什么事芭挽,這里把reducers收集起來(lái)了,然后打包成一個(gè)叫做Store的東西膛薛,這個(gè)Store就是我們后面用到的所有state合集刽射,具體不清楚军拟,可以選擇直接console.log(Store),發(fā)現(xiàn)Store有這些method:
dispatch: (action)
getState: getState()
replaceReducer: replaceReducer(nextReducer)
subscribe: subscribe(listener)
具體是做什么用誓禁,后面說(shuō)明懈息。
關(guān)于reducers,其實(shí)就是定義了我們整個(gè)state的數(shù)據(jù)結(jié)構(gòu)的一個(gè)東西摹恰。就像我們要做一個(gè)todoList辫继,它最基本的數(shù)據(jù)結(jié)構(gòu)就是:
{
todos: [
{ text: "吃飯" , selected: false },
{ text: "上班" , selected: false },
{ text: "寫(xiě)代碼" , selected: true },
...
]
}
那么我們的App里,reducer就需要返回這個(gè)俗慈,所以簡(jiǎn)單的說(shuō)姑宽,你就理解成:
var reducer = (condition) => {
//根據(jù)條件做了一些羞羞的事情
a();
b();
c();
return {
todos: [
{ text: "吃飯" , selected: false },
{ text: "上班" , selected: false },
{ text: "寫(xiě)代碼" , selected: true },
...
]
}
}
就好了,具體如何做姜盈,看我們后面解釋低千。
所以這里 App.js里面,我們就拿到了后面定義的所有state的讀取的權(quán)力馏颂。這里有行代碼:
const createStoreWithThunk = applyMiddleware(thunk)(createStore);
關(guān)于thunk是什么 說(shuō)簡(jiǎn)單點(diǎn)示血,就是給我們的代碼提供了異步的功能,也就是在promise里還可以同時(shí)做很多操作救拉,比如更新列表难审,彈出提醒等等,詳見(jiàn)后面亿絮。
<Provider store={store}> { () => <BaseApp /> }</Provider>
我們現(xiàn)在通過(guò)Provider把Store遞交給了真正的App入口告喊,也就是開(kāi)始渲染界面的東西: BaseApp.js。
現(xiàn)在來(lái)創(chuàng)建BaseApp.js文件派昧,文件位于 ./src/containers/BaseApp.js:
import React, {
Component,
View,
Text,
Navigator,
TabBarIOS,
} from 'react-native';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux/native';
import * as actions from '../actions';
import List from './List';
@connect(state => ({
state: state
}))
export default class BaseApp extends Component {
constructor(props) {
super(props);
this.initialRoute = {
name: 'List',
component: List,
}
}
configureScene() {
return Navigator.SceneConfigs.VerticalDownSwipeJump;
}
renderScene(route, navigator) {
let Component = route.component;
const { state, dispatch } = this.props;
const action = bindActionCreators(actions, dispatch);
return (
<Component
state={state}
actions={action}
{...route.params}
navigator={navigator} />
);
}
render() {
var _this = this;
return (
<Navigator
initialRoute={_this.initialRoute}
configureScene={_this.configureScene.bind(_this)}
renderScene={_this.renderScene.bind(_this)} />
);
}
}
這是一個(gè)簡(jiǎn)單的 Navigator黔姜,關(guān)于Navigator的用法,請(qǐng)看我上一個(gè)帖子蒂萎。
但是唯一區(qū)別的地方在于:
@connect(state => ({
state: state
}))
這是一個(gè)es6的語(yǔ)法秆吵,叫es7.decorators,具體操作是把上一個(gè)App入口傳入的Store里的state取到五慈,然后作為props在BaseApp里面使用纳寂。
既然說(shuō)到了state,那就先去創(chuàng)建一個(gè)reducer吧泻拦,先定義一下初始的state結(jié)構(gòu):
創(chuàng)建todo.js 文件位于 ./src/reducers/todo.js:
const defaultTodos = [
{text: '寫(xiě)代碼'},
{text: '哄妹紙'},
{text: '做飯洗碗家務(wù)事'},
{text: '等等...'}
];
module.exports = function(state, action) {
state = state || {
type: 'INITIAL_TODOS',
todos: []
}
return {
...state
}
}
這里定義了默認(rèn)的todoList結(jié)構(gòu)毙芜,然后返回這個(gè)函數(shù)給了exports。
為了方便import,我們?cè)谶@個(gè)目錄下再創(chuàng)建一個(gè)index.js
創(chuàng)建index.js 文件位于 ./src/reducers/index.js:
module.exports.todo = require('./todo');
導(dǎo)出為todo就好了争拐,這個(gè)todo就是一個(gè)整個(gè)state數(shù)據(jù)結(jié)構(gòu)里的一部分了腋粥。
這里看到了 type: 'INITIAL_TODOS' ,也就是這個(gè)操作就是初始化todos,那么加載出來(lái)defaultTodos怎么寫(xiě)呢:
修改todo.js 文件位于 ./src/reducers/todo.js:
import React, {
ListView
} from 'react-native';
const defaultTodos = [
{text: '寫(xiě)代碼'},
{text: '哄妹紙'},
{text: '做飯洗碗家務(wù)事'},
{text: '等等...'}
];
module.exports = function(state, action) {
state = state || {
type: 'INITIAL_TODOS',
todos: []
}
switch(action.type) {
case 'LOAD_TODOS': {
var dataSource = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 });
dataSource = dataSource.cloneWithRows(defaultTodos);
return {
...state,
...action,
todos: defaultTodos,
dataSource,
}
}
return {
...state
}
}
這里給action的type開(kāi)始做判斷了隘冲,action是我們的一些具體操作金赦,比如 loadTodos就是加載todoList數(shù)據(jù),這里L(fēng)OAD_TODOS先創(chuàng)建一個(gè)ListView用于后面渲染todoList的內(nèi)容对嚼,
然后把初始數(shù)據(jù)給拷貝給了ListView夹抗,然后用 ...展開(kāi)方法,把state纵竖,action漠烧,todos,dataSource都給返回了靡砌。
現(xiàn)在要?jiǎng)?chuàng)建我們的action了忿磅,這里記住reducer只是單純的負(fù)責(zé)返回?cái)?shù)據(jù)結(jié)構(gòu)塘雳,并不能做抓取數(shù)據(jù)/更新/修改/刪除數(shù)據(jù)的操作,CRUD這些操作都是在action中進(jìn)行。
創(chuàng)建TodoActions.js文件 文件位于 ./src/actions/TodoActions.js:
const LOAD_TODOS = 'LOAD_TODOS';
const SELECT_TODO = 'SELECT_TODO';
const APPEND_TODO = 'APPEND_TODO';
var loadTodos = () => {
return (dispatch) => {
setTimeout(() => {
dispatch({ type: LOAD_TODOS });
}, 1000);
// fetch().then() => dispatch in promise
}
}
var appendTodo = (text, cleanUIState) => {
if(text) {
if(cleanUIState)
cleanUIState();
return {
type: APPEND_TODO,
todo: { text },
}
}
return ;
}
var selectTodo = (selected) => {
return {
type: SELECT_TODO,
selected
}
}
module.exports = {
loadTodos,
appendTodo,
selectTodo,
}
同理為了方便 import ,我們創(chuàng)建index.js文件 文件位于 ./src/actions/index.js:
//exports很多對(duì)象時(shí)候的另一種寫(xiě)法而已
var todo = require('./TodoActions');
var actions = {};
Object.assign(actions, todo);
module.exports = actions;
這里我定義了三個(gè)常亮跟三個(gè)方法彤路,三個(gè)方法分別用于加載todo任務(wù)徒恋,追加todo任務(wù)兵拢,以及完成/撤銷todo任務(wù)介褥,然后再提交給exports,這里千萬(wàn)別忘了module.exports曲聂。
var loadTodos = () => {
return (dispatch) => {
setTimeout(() => {
dispatch({ type: LOAD_TODOS });
}, 1000);
// fetch().then() => dispatch in promise
}
}
這里是做了一個(gè)獲取數(shù)據(jù)的操作霹购,我給它延時(shí)1s操作,就是為了模擬從本地讀取或者從服務(wù)器抓取數(shù)據(jù)朋腋,這些因?yàn)槭钱惒讲僮髌敫恚倩氐角懊婺莻€(gè)thunk的middleware,就是在這里起作用了旭咽。
我們?cè)趓eact-native中就可以直接用 loadTodos() 來(lái)觸發(fā)初始化todoList的操作了贞奋。
這里看最后的 dispatch({ type: LOAD_TODOS });
這個(gè)dispatch就會(huì)把我們的數(shù)據(jù)傳遞到reducer里的 module.exports = function(state, action) 在./src/reducers/todo.js打印那個(gè)console.log(action)就會(huì)顯示我們這里dispatch的
{ type: LOAD_TODOS }
如果我們換成
{ type: LOAD_TODOS, defaultTodos: [{text: '我在dispatch數(shù)據(jù)給reducer'}] }
試試看,你會(huì)看到什么穷绵。轿塔。。
再回到我們的BaseApp里的
renderScene(route, navigator) {
let Component = route.component;
const { state, dispatch } = this.props;
const action = bindActionCreators(actions, dispatch);
return (
<Component
state={state}
actions={action}
{...route.params}
navigator={navigator} />
);
}
state,dispath 都來(lái)自于我們的Store请垛,
然后bindActionCreators把我們定義的所有的actions通過(guò)dispatch的參數(shù)來(lái)關(guān)聯(lián)到reducer的返回,也就是state催训。于是我們action中寫(xiě)的所有method洽议,都可以通過(guò)action.type來(lái)reducer中找到對(duì)應(yīng)的返回的state宗收!
最后都傳遞給我們的組件的Props。所以這里發(fā)生了一個(gè)事情亚兄,就是在所有通過(guò)Navigator導(dǎo)航的Component里混稽,我們都可以操作全局的state。
現(xiàn)在來(lái)完善我們的 reducers.js:
todo 文件位于 ./scr/reducers/todo.js:
import React, {
ListView
} from 'react-native';
const defaultTodos = [
{text: '寫(xiě)代碼'},
{text: '哄妹紙'},
{text: '做飯洗碗家務(wù)事'},
{text: '等等...'}
];
module.exports = function(state, action) {
state = state || {
type: 'INITIAL_TODOS',
todos: []
}
switch(action.type) {
case 'LOAD_TODOS': {
var dataSource = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 });
dataSource = dataSource.cloneWithRows(defaultTodos);
return {
...state,
...action,
todos: defaultTodos,
dataSource,
}
}
case 'APPEND_TODO': {
var todos = [ ...state.todos ];
todos.unshift(action.todo);
dataSource = state.dataSource.cloneWithRows(todos);
return {
...state,
...action,
todos,
dataSource
}
}
case 'SELECT_TODO': {
var selected = action.selected;
var todos = [ ...state.todos ];
var index = todos.indexOf(selected);
if(todos[index].selected) {
todos[index] = { text: todos[index].text }
}else {
todos[index] = { text: todos[index].text, selected: true }
}
dataSource = state.dataSource.cloneWithRows(todos);
return {
...state,
...action,
todos,
dataSource
}
}
}
return {
...state
}
}
會(huì)看到這里都在用 ...展開(kāi),這樣可以創(chuàng)建一個(gè)新的對(duì)象匈勋,而舊的對(duì)象不會(huì)發(fā)生改變礼旅,這個(gè)的目的是為了另一個(gè)功能,具體這里先不解釋了洽洁。
現(xiàn)在已經(jīng)有了Navigator痘系,有個(gè)操作state的三個(gè)action,有了可以返回完整數(shù)據(jù)結(jié)構(gòu)的reducer饿自,現(xiàn)在只需要寫(xiě)一個(gè)List的頁(yè)面來(lái)載入汰翠,添加,完成/撤銷todo任務(wù)就可以了昭雌。
創(chuàng)建文件 List.js 文件位于 ./src/containers/List.js:
import React, {
Component,
View,
ListView,
TextInput,
Text,
Image,
Dimensions,
TouchableOpacity,
ActivityIndicatorIOS,
StyleSheet,
} from 'react-native';
const fullWidth = Dimensions.get('window').width;
const styles = StyleSheet.create({
container: {
flex: 1,
marginTop: 20,
},
todoRow: {
paddingLeft: 10,
paddingRight: 10,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
width: fullWidth,
height: 40,
borderBottomColor: '#EEEEEE',
borderBottomWidth: 1,
},
todoText: {
fontSize: 16,
color: '#666666',
},
todoTextDone: {
fontSize: 16,
color: '999999',
textDecorationColor: '#999999',
textDecorationLine: 'line-through',
textDecorationStyle: 'solid'
},
success: {
color: 'green',
},
pendding: {
color: 'blue',
},
inputText: {
height: 40,
width: (fullWidth-20)*0.8,
borderBottomColor: '#EEEEEE',
borderBottomWidth: 1,
},
button: {
alignItems: 'center',
justifyContent: 'center',
width: (fullWidth - 20)*0.2,
backgroundColor: '#EEEEEE',
padding: 10,
}
});
export default class List extends Component {
constructor(props) {
super(props);
this.state = {
text: null,
placeholder: '寫(xiě)下你將來(lái)要做的事情'
}
}
componentDidMount() {
const { loadTodos } = this.props.actions;
loadTodos();
}
appendTodoList() {
const text = this.state.text;
const { appendTodo } = this.props.actions;
appendTodo(text);
this.setState({ text: null });
}
renderHeader() {
return (
<View style={styles.todoRow}>
<TextInput
value={this.state.text}
placeholder={this.state.placeholder}
onChangeText={(text) => this.setState({ text })}
style={styles.inputText} />
<TouchableOpacity onPress={this.appendTodoList.bind(this)} style={styles.button}>
<Text style={styles.buttonText}>添加</Text>
</TouchableOpacity>
</View>
);
}
renderRow(dataRow) {
const { selectTodo } = this.props.actions;
return (
<View style={styles.todoRow}>
<Text style={ dataRow.selected ? styles.todoTextDone : styles.todoText}>{dataRow.text}</Text>
<TouchableOpacity onPress={() => selectTodo(dataRow)}>
{ dataRow.selected ? <Text style={styles.success}>完成</Text> : <Text style={styles.pendding}>待辦</Text> }
</TouchableOpacity>
</View>
)
}
renderList() {
const { todo } = this.props.state;
return (
<ListView
style={styles.container}
dataSource={todo.dataSource}
renderHeader={this.renderHeader.bind(this)}
renderRow={this.renderRow.bind(this)} />
);
}
renderIndicator() {
return (
<ActivityIndicatorIOS animating={true} color={'#808080'} size={'small'} />
);
}
render() {
const { todo } = this.props.state;
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
{ todo.type != 'INITIAL_TODOS' ? this.renderList() : this.renderIndicator() }
</View>
);
}
}
目前就寫(xiě)這么多了复唤,可能會(huì)有一些錯(cuò)誤的地方,后面可以跟帖補(bǔ)上烛卧。
黑色斜體高亮部分是完整的代碼佛纫,拷貝或者改動(dòng)都可以用的。代碼地址