Redux App - BookList
通过创建一个书籍列表app, 学习Redux的一些基础概念以及主要API的运用, 下面是该app的图示:
简单介绍
首次渲染的app页面:
左边是书籍列表, 右边首次渲染的文字为提示词(选择某本书显示该本书相关信息), 功能如提示词所示, 即点击某本书后在右侧会显示该书的详细信息, 以下是图例:
从视图层, 数据层分析
从上图可以看出, 从数据方面看, 该app中包含两种类型的数据, 一是左边栏中一系列的书籍(hard data), 二是当前选中的书籍(meta level properties).
从视图方面看, 该app的视图包含3部分的内容, List View是数据列表, List Item是列表中的单本书, Detail View是当前选中书籍的详细信息.
在创建app时, 我们要将app的视图与数据分开, redux负责数据层的内容, 存储state; react负责视图层的内容, 将state转化为呈现在页面上的view.
组织结构
Redux脚手架: Redux Boilerplate
文件结构
1 | book_list/ |
从文件结构中可以看到, 应用最重要的几个部分为reducers, actions, components和container. 下面对这几个部分进行详细说明.
reducer和action
reducer
reducer是返回应用state的函数, 因为大多数redux应用包含多个state, 因此包含多个不同的reducer.
在书籍应用(book_list)中, 有两类数据, 因此应包含两个reducer, 一个负责创建全部书籍列表, 一个负责创建当前选中的书籍.
如上图所示, 创建BooksReducer
以及ActiveBook
, reducer返回state的值, state的key(即图中的books和activeBook)可以为任何值, 与reducer无关, 与reducer相关的只有state的值. BooksReducer
返回所有书籍列表数据, 具体内容见下:
reducer_books.js
1
2
3
4
5
6
7
8export default function() {
return [
{ title: 'Javascript: The Good Parts', pages: 101 },
{ title: 'Harry Potter', pages: 39 },
{ title: 'The Dark Tower', pages: 85 },
{ title: 'Eloquent Ruby', pages: 1 }
];
}
ActiveBook
的内容会根据用户操作时刻变化, reducer接受redux分配的action后根据action类型返回与之相关的state.
reducer_active_book.js
1
2
3
4
5
6
7
8export default function(state = null, action) {
switch(action.type) {
case 'BOOK_SELECTED':
return action.payload;
}
return state;
}
最后再使用redux库的combineReducers方法将所有reducer整合起来. 把reducer对象传入combineReducers后, redux根据传入的所有reducer创建整个app的状态(state).
reducers/index.js
1
2
3
4
5
6
7
8
9
10import { combineReducers } from 'redux';
import BooksReducer from './reducer_books';
import ActiveBook from './reducer_active_book';
const rootReducer = combineReducers({
books: BooksReducer,
activeBook: ActiveBook
});
export default rootReducer;
action
上面提到, ActiveBook
这个reducer返回的state值会根据action.type变化, 此app比较简单, 只需要一个action来处理用户操作.
actions/index.js
1
2
3
4
5
6export function selectBook(book) {
return {
type: 'BOOK_SELECTED',
payload: book
};
}
selectBook()
是action creator, 相关的state发生改变后会调用action creator, action creator返回action, action是一个JavaScript对象, 在这个对象中必须声明type属性, type描述用户操作的具体内容, 此时是BOOK_SELECTED
, 除了type属性, 还有一个payload属性, payload属性的值为与该action相关的数据值, 因为这个action选择了某一本书, 那么所需要的相关数据就是那本书的详细信息, payload的值就是所选取的书的信息. type和payload属性中, type必须声明, 但payload并非必需.
container和component
在组织组件时, 一般将组件分为container和component, 因此组织文件结构时, 也创建两个文件夹, 分别为containers和components.
容器组件(container)
container是利用redux store中数据的组件, 也被叫做smart component. BookList和BookDetail都是根据state的变化而变化, 因此这两个组件应为container.
book-detail.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26import React, { Component } from 'react';
import { connect } from 'react-redux';
class BookDetail extends Component {
render() {
if (!this.props.book) {
return <div>Select a book to get started.</div>;
}
return (
<div>
<h3>Details for:</h3>
<div>Title: {this.props.book.title}</div>
<div>Pages: {this.props.book.pages}</div>
</div>
);
}
}
function mapStateToProps(state) {
return {
book: state.activeBook
};
}
export default connect(mapStateToProps)(BookDetail);
从以上代码中可以看到, book-detail从react-redux库中引入了connect方法, 因为在react和redux搭配使用时, react无法直接获取redux所负责的应用state, 因此需要react-redux的帮助使两个库能更好得配合使用. mapStateToProps将来自redux的state转换为当前component的props, connect将component提升为container.
关于connect:
只要应用的state产生变化(用户事件, ajax请求…):
- container会立即rerender
- 组件的props也会立即变化
book-list.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38import React, { Component } from 'react';
import { connect } from 'react-redux';
import { selectBook } from '../actions/index';
import { bindActionCreators } from 'redux';
class BookList extends Component {
renderList() {
return this.props.books.map((book) => {
return (
<li
key={book.title}
onClick={() => this.props.selectBook(book)}
className="list-group-item">
{book.title}
</li>
);
});
}
render() {
return (
<ul className="list-group col-sm-4">
{this.renderList()}
</ul>
)
}
}
function mapStateToProps(state) {
return {
books: state.books
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({ selectBook: selectBook }, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(BookList);
BookList组件中, 用户点击动作不只需要调用action creator, 还要确保action分发到所有reducer中, 使用redux库中的bindActionCreator方法和dispatch方法达到此目的.
展示组件(component)
component是没有与redux store连结的组件, 也被叫做dumb component, 只负责呈现View, 不处理数据. 此例中是只负责渲染书籍列表和选中书籍的详细信息到页面中.
app.js
1 | import React from 'react'; |
wrapup
src/index.js
1
2
3
4
5
6
7
8
9
10
11
12
13import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import App from './components/app';
import reducers from './reducers';
ReactDOM.render(
<Provider store={createStore(reducers)}>
<App />
</Provider>
, document.querySelector('.container'));
从react-redux引入了Provider, 将store作为属性传入Provider后, Provider接受来自store的更新并传递这些更新.
下面是该应用的完整逻辑介绍:
Tips
React与Redux的state
只使用react构建应用时, 也有一个state, 是component state(组件层面), 要注意与redux的application state(应用层面)进行区分, 在redux应用中, 两个类型的state都可以使用, 两者是完全分离, 互不影响的, 在app中使用redux state一般是将state转化为props再使用, 利用this.props.something
, 而component state是this.state.somthing
.
Redux存储数据的方法与Angular, Backbone, Flux的区别
Redux将所有的数据只存储在一个object中, 这个object就是store.
许多其他库或框架将数据分为不同的集合存储, Backbone是存储在不同的collection中, flux则是存储在不同的store中.
参考