概况
例子源代码:https://github.com/reactjs/redux/tree/master/examples/async
运行之后的效果:
可以看到,基本上可以分为上下两个部分。上面部分是一个下拉框,从中可以选择想要显示项目,下面部分在没有完全得到数据之前,显示“Loading…”,得到数据之后,显示对应的内容,以及获取内容的时间,和一个用于刷新的按钮。
如果想要显示最初的“Loading…”效果,它对应的HTML可以写成这样:
<div data-reactroot="">
<span>
<h1>reactjs</h1>
<select>
<option value="reactjs">reactjs</option>
<option value="frontend">frontend</option>
</select>
</span>
<h2>Loading...</h2>
</div>
而获得结果之后的HTML可以是这样:
<div data-reactroot="">
<span>
<h1>reactjs</h1>
<select>
<option value="reactjs">reactjs</option>
<option value="frontend">frontend</option>
</select>
</span>
<p>
<span>
Last updated at 下午3:23:38
</span>
<button>Refresh</button>
</p>
<div>
<ul>
<li>Beginner's Thread / Easy Questions (February 2018)</li>
<li>React's new Context API</li>
<li>Build a Progressive Web App (PWA) using React</li>
<li>Developing Games with React, Redux, and SVG - Part 1</li>
<li>Infinite scroll techniques in React</li>
<li>docx file generation and download Help</li>
<li>How do you use mock data?</li>
<li>How to model the behavior of Redux apps using statecharts</li>
</ul>
</div>
</div>
Components
Picker
可以把上面这一部分独立出来,可以组成一个component:
const Picker = () => (
<span>
<h1>reactjs</h1>
<select>
<option value="reactjs">reactjs</option>
<option value="frontend">frontend</option>
</select>
</span>
)
如果将< h1 >中和< select > 中的 reactjs 和 frontend 都转化为参数,可以写成:
// value = 'reactjs', options = ['reactjs', 'frontend']
const Picker = ({ value, options }) => (
<span>
<h1>{value}</h1>
<select value={value}>
{options.map(option =>
<option value={option} key={option}>
{option}
</option>)
}
</select>
</span>
)
如果希望 < select > 发生变化的时候可以有所反应,可以传入一个function:
// value = 'reactjs', options = ['reactjs', 'frontend']
const Picker = ({ value, onchange, options }) => (
<span>
<h1>{value}</h1>
<select onchange={e => onchange(e.target.value)}
value={value}>
{options.map(option =>
<option value={option} key={option}>
{option}
</option>)
}
</select>
</span>
)
Posts
网页的下半部分,可能是简单的字符串,也可能是一个列表,将这个列表做一个component:
const Posts = ({posts}) => (
<ul>
{posts.map((post, i) =>
<li key={i}>{post.title}</li>
)}
</ul>
)
首先,我们知道这是一个异步获取数据的例子,而在Redux里面,我能做的就是定义 Action 和对应的 Reducer。有几件事情是必须的:
1. 开始发起request
2. request 过程之中
3. 接收 request 的结果
这里,我们用fetch
来发起网络请求,那么就用isFetching
来表示是否正在进行fetch
动作。用isEmpty
来表示是否已经获得数据。那么列表内容如何显示,即是显示“Loading…”,还是显示列表内容,可以如下表示:
{ isEmpty
? (isFetching ? <h2>Loading...</h2> : <h2>Empty.</h2>)
: <div>
<Posts posts={posts} />
</div>
}
当不是正在fetch
的时候,即isFetching
为false
的时候,希望可以用一个按钮来进行刷新:
{!isFetching &&
<button onClick={this.handleRefreshClick}>
Refresh
</button>
}
另外,我想记录下最近一次 update 的时间:
{lastUpdated &&
<span>
Last updated at {new Date(lastUpdated).toLocaleTimeString()}.
{' '}
</span>
}
Render
把 Picker
和 Posts
组合起来渲染我们想要的内容:
<div>
<Picker value={selectedSubreddit}
onChange={this.handleChange}
options={[ 'reactjs', 'frontend' ]} />
<p>
{lastUpdated &&
<span>
Last updated at {new Date(lastUpdated).toLocaleTimeString()}.
{' '}
</span>
}
{!isFetching &&
<button onClick={this.handleRefreshClick}>
Refresh
</button>
}
</p>
{isEmpty
? (isFetching ? <h2>Loading...</h2> : <h2>Empty.</h2>)
: <div style={{ opacity: isFetching ? 0.5 : 1 }}>
<Posts posts={posts} />
</div>
}
</div>
显然,我们需要从外部传入一些属性才能做 render:
selectedSubreddit: PropTypes.string.isRequired,
posts: PropTypes.array.isRequired,
isFetching: PropTypes.bool.isRequired,
lastUpdated: PropTypes.number
而isEmpty
可以通过计算得到:
const isEmpty = posts.length === 0
所以,render 写成:
render() {
const { selectedSubreddit, posts, isFetching, lastUpdated } = this.props
const isEmpty = posts.length === 0
return (
<div>
<Picker value={selectedSubreddit}
onChange={this.handleChange}
options={[ 'reactjs', 'frontend' ]} />
<p>
{lastUpdated &&
<span>
Last updated at {new Date(lastUpdated).toLocaleTimeString()}.
{' '}
</span>
}
{!isFetching &&
<button onClick={this.handleRefreshClick}>
Refresh
</button>
}
</p>
{isEmpty
? (isFetching ? <h2>Loading...</h2> : <h2>Empty.</h2>)
: <div style={{ opacity: isFetching ? 0.5 : 1 }}>
<Posts posts={posts} />
</div>
}
</div>
)
}
Actions
这里还有this.handleRefreshClick
和this.handleChange
没有任何解释。而redux里面,他们所要做的就是dispatch
某个命令,生成某种type
的action
。
handleRefreshClick
// selectedSubreddit 为当前选中的记录
// Actions
export const invalidateSubreddit = subreddit => ({
type: INVALIDATE_SUBREDDIT,
subreddit
})
const shouldFetchPosts = (state, subreddit) => {
const posts = state.postsBySubreddit[subreddit]
if (!posts) {
return true
}
if (posts.isFetching) {
return false
}
return posts.didInvalidate
}
export const requestPosts = subreddit => ({
type: REQUEST_POSTS,
subreddit
})
export const receivePosts = (subreddit, json) => ({
type: RECEIVE_POSTS,
subreddit,
posts: json.data.children.map(child => child.data),
receivedAt: Date.now()
})
const fetchPosts = subreddit => dispatch => {
dispatch(requestPosts(subreddit))
return fetch(`https://www.reddit.com/r/${subreddit}.json`)
.then(response => response.json())
.then(json => dispatch(receivePosts(subreddit, json)))
}
export const fetchPostsIfNeeded = subreddit => (dispatch, getState) => {
if (shouldFetchPosts(getState(), subreddit)) {
return dispatch(fetchPosts(subreddit))
}
}
handleRefreshClick = e => {
e.preventDefault()
const { dispatch, selectedSubreddit } = this.props
dispatch(invalidateSubreddit(selectedSubreddit))
dispatch(fetchPostsIfNeeded(selectedSubreddit))
}
handleChange
export const selectSubreddit = subreddit => ({
type: SELECT_SUBREDDIT,
subreddit
})
handleChange = nextSubreddit => {
this.props.dispatch(selectSubreddit(nextSubreddit))
}
componentDidMount & componentWillReceiveProps
网页打开的时候,希望自动开始 request 数据,在 React 中,需要componentDidMount
和 componentWillReceiveProps
。
componentDidMount() {
const { dispatch, selectedSubreddit } = this.props
dispatch(fetchPostsIfNeeded(selectedSubreddit))
}
componentWillReceiveProps(nextProps) {
if (nextProps.selectedSubreddit !== this.props.selectedSubreddit) {
const { dispatch, selectedSubreddit } = nextProps
dispatch(fetchPostsIfNeeded(selectedSubreddit))
}
}
APP
先抛开Reducer,假设已经实现好了,App 可以写成这样。
class App extends Component {
static propTypes = {
selectedSubreddit: PropTypes.string.isRequired,
posts: PropTypes.array.isRequired,
isFetching: PropTypes.bool.isRequired,
lastUpdated: PropTypes.number,
dispatch: PropTypes.func.isRequired
}
componentDidMount() {
const { dispatch, selectedSubreddit } = this.props
dispatch(fetchPostsIfNeeded(selectedSubreddit))
}
componentWillReceiveProps(nextProps) {
if (nextProps.selectedSubreddit !== this.props.selectedSubreddit) {
const { dispatch, selectedSubreddit } = nextProps
dispatch(fetchPostsIfNeeded(selectedSubreddit))
}
}
handleChange = nextSubreddit => {
this.props.dispatch(selectSubreddit(nextSubreddit))
}
handleRefreshClick = e => {
e.preventDefault()
const { dispatch, selectedSubreddit } = this.props
dispatch(invalidateSubreddit(selectedSubreddit))
dispatch(fetchPostsIfNeeded(selectedSubreddit))
}
render() {
const { selectedSubreddit, posts, isFetching, lastUpdated } = this.props
const isEmpty = posts.length === 0
return (
<div>
<Picker value={selectedSubreddit}
onChange={this.handleChange}
options={[ 'reactjs', 'frontend' ]} />
<p>
{lastUpdated &&
<span>
Last updated at {new Date(lastUpdated).toLocaleTimeString()}.
{' '}
</span>
}
{!isFetching &&
<button onClick={this.handleRefreshClick}>
Refresh
</button>
}
</p>
{isEmpty
? (isFetching ? <h2>Loading...</h2> : <h2>Empty.</h2>)
: <div style={{ opacity: isFetching ? 0.5 : 1 }}>
<Posts posts={posts} />
</div>
}
</div>
)
}
}
从上面可以看到,我想从 Redux 中获得的 property,因为 dispatch 是 Redux 自带的, 应该是这样的形式:
{
selectedSubreddit,
posts,
isFetching,
lastUpdated
}
于是,需要下面的形式:
const mapStateToProps = state => {
// 经过计算。。。过程略
return {
selectedSubreddit,
posts,
isFetching,
lastUpdated
}
}
export default connect(mapStateToProps)(App)
Reducer
下拉框更新
下拉框发生改变时,发一个 action:
handleChange = nextSubreddit => {
this.props.dispatch(selectSubreddit(nextSubreddit))
}
action 记作:
export const selectSubreddit = subreddit => ({
type: SELECT_SUBREDDIT,
subreddit
})
对应处理的 reducer:
const selectedSubreddit = (state = 'reactjs', action) => {
switch (action.type) {
case SELECT_SUBREDDIT:
return action.subreddit
default:
return state
}
}
列表更新
在componentDidMount
和componentWillReceiveProps
中,都要发 request 去获取数据,那么发个 action。发个怎样的 action 呢?
首先,在有需要的时候发action,action 记作fetchPostsIfNeeded
。
如果真有需要,那么就发起请求,action 记作fetchPosts
。
那么,如何fetchPosts
?
首先,说,我要发起请求了,action 记作 requestPosts
。
然后,获取数据,真的发起网络请求,fetch
。
最后,说,接收到数据了,receivePosts
。
展开成代码:
export const requestPosts = subreddit => ({
type: REQUEST_POSTS,
subreddit
})
export const receivePosts = (subreddit, json) => ({
type: RECEIVE_POSTS,
subreddit,
posts: json.data.children.map(child => child.data),
receivedAt: Date.now()
})
const fetchPosts = subreddit => dispatch => {
dispatch(requestPosts(subreddit))
return fetch(`https://www.reddit.com/r/${subreddit}.json`)
.then(response => response.json())
.then(json => dispatch(receivePosts(subreddit, json)))
}
const shouldFetchPosts = (state, subreddit) => {
const posts = state.postsBySubreddit[subreddit]
if (!posts) {
return true
}
if (posts.isFetching) {
return false
}
return posts.didInvalidate
}
export const fetchPostsIfNeeded = subreddit => (dispatch, getState) => {
if (shouldFetchPosts(getState(), subreddit)) {
return dispatch(fetchPosts(subreddit))
}
}
对应的Reducer,发起请求的时候,将isFetching
设置为true
,这样就可以显示 Loading
了,接受到数据之后,isFetching
设置为false,显示记录,更新时间和按钮。
const posts = (state = {
isFetching: false,
didInvalidate: false,
items: []
}, action) => {
switch (action.type) {
case REQUEST_POSTS:
return {
...state,
isFetching: true,
}
case RECEIVE_POSTS:
return {
...state,
isFetching: false,
items: action.posts,
lastUpdated: action.receivedAt
}
default:
return state
}
}
const postsBySubreddit = (state = { }, action) => {
switch (action.type) {
case RECEIVE_POSTS:
case REQUEST_POSTS:
return {
...state,
[action.subreddit]: posts(state[action.subreddit], action)
}
default:
return state
}
}
将这两者结合起来,可以得到:
const rootReducer = combineReducers({
postsBySubreddit,
selectedSubreddit
})
export default rootReducer
这样,App 的 mapStateToProps
可以写成:
const mapStateToProps = state => {
const { selectedSubreddit, postsBySubreddit } = state
const {
isFetching,
lastUpdated,
items: posts
} = postsBySubreddit[selectedSubreddit] || {
isFetching: true,
items: []
}
return {
selectedSubreddit,
posts,
isFetching,
lastUpdated
}
}
总结
异步调用可在 action 内部完成,action 可以继续 dispatch action。