Redux:例子分析 Async

本文通过分析Redux官方的async例子,详细讲解如何处理异步操作,包括Picker组件、Posts组件的实现,以及在React中如何使用Redux进行数据请求和状态管理。重点介绍了Actions和Reducers在异步请求过程中的作用,如handleRefreshClick、handleChange等关键函数,以及如何在componentDidMount和componentWillReceiveProps中触发请求。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

概况

例子源代码:https://github.com/reactjs/redux/tree/master/examples/async
运行之后的效果:
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的时候,即isFetchingfalse的时候,希望可以用一个按钮来进行刷新:

{!isFetching &&
    <button onClick={this.handleRefreshClick}>
        Refresh
    </button>
}

另外,我想记录下最近一次 update 的时间:

{lastUpdated &&
    <span>
        Last updated at {new Date(lastUpdated).toLocaleTimeString()}.
        {' '}
    </span>
}

Render

PickerPosts 组合起来渲染我们想要的内容:

<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.handleRefreshClickthis.handleChange没有任何解释。而redux里面,他们所要做的就是dispatch某个命令,生成某种typeaction

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 中,需要componentDidMountcomponentWillReceiveProps

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

列表更新

componentDidMountcomponentWillReceiveProps中,都要发 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。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值