从 react 组件中指派 actions
我们已经知道如何从 Redux Store 获取数据到 UI 中,现在来看看如何从 UI 中提交数据用于 actions。
思考这个问题的最佳场景是投票界面上的投票按钮。之前在写相关界面时,我们假设 Voting
组件接受一个回调函数 props。
当用户点击某个按钮时组件将会调用这个回调函数。但我们目前并没有实现这个回调函数,除了在测试代码中。
当用户投票后应该做什么?投票结果应该发送给服务端,这部分我们稍后再说,客户端也需要执行一些逻辑:
组件的 hasVoted
值应该被设置,这样用户才不会反复对同一对选项投票。
这是我们要创建的第二个客户端 Redux Action,我们称之为 VOTE
:
//test/reducer_spec.js
it('handles VOTE by setting hasVoted', () => {
const state = fromJS({
vote: {
pair: ['Trainspotting', '28 Days Later'],
tally: {Trainspotting: 1}
}
});
const action = {type: 'VOTE', entry: 'Trainspotting'};
const nextState = reducer(state, action);
expect(nextState).to.equal(fromJS({
vote: {
pair: ['Trainspotting', '28 Days Later'],
tally: {Trainspotting: 1}
},
hasVoted: 'Trainspotting'
}));
});
为了更严谨,我们应该考虑一种情况:不管什么原因,当 VOTE
action 传递了一个不存在的选项时我们的应用该怎么做:
//test/reducer_spec.js
it('does not set hasVoted for VOTE on invalid entry', () => {
const state = fromJS({
vote: {
pair: ['Trainspotting', '28 Days Later'],
tally: {Trainspotting: 1}
}
});
const action = {type: 'VOTE', entry: 'Sunshine'};
const nextState = reducer(state, action);
expect(nextState).to.equal(fromJS({
vote: {
pair: ['Trainspotting', '28 Days Later'],
tally: {Trainspotting: 1}
}
}));
});
下面来看看我们的 reducer 如何实现的:
//src/reducer.js
import {Map} from 'immutable';
function setState(state, newState) {
return state.merge(newState);
}
function vote(state, entry) {
const currentPair = state.getIn(['vote', 'pair']);
if (currentPair && currentPair.includes(entry)) {
return state.set('hasVoted', entry);
} else {
return state;
}
}
export default function(state = Map(), action) {
switch (action.type) {
case 'SET_STATE':
return setState(state, action.state);
case 'VOTE':
return vote(state, action.entry);
}
return state;
}
hasVoted
并不会一直保存在状态数据中,每当开始一轮新的投票时,我们应该在 SET_STATE
action 的处理逻辑中
检查是否用户是否已经投票,如果还没,我们应该删除掉 hasVoted
:
//test/reducer_spec.js
it('removes hasVoted on SET_STATE if pair changes', () => {
const initialState = fromJS({
vote: {
pair: ['Trainspotting', '28 Days Later'],
tally: {Trainspotting: 1}
},
hasVoted: 'Trainspotting'
});
const action = {
type: 'SET_STATE',
state: {
vote: {
pair: ['Sunshine', 'Slumdog Millionaire']
}
}
};
const nextState = reducer(initialState, action);
expect(nextState).to.equal(fromJS({
vote: {
pair: ['Sunshine', 'Slumdog Millionaire']
}
}));
});
根据需要,我们新增一个 resetVote
函数来处理 SET_STATE
动作:
//src/reducer.js
import {List, Map} from 'immutable';
function setState(state, newState) {
return state.merge(newState);
}
function vote(state, entry) {
const currentPair = state.getIn(['vote', 'pair']);
if (currentPair && currentPair.includes(entry)) {
return state.set('hasVoted', entry);
} else {
return state;
}
}
function resetVote(state) {
const hasVoted = state.get('hasVoted');
const currentPair = state.getIn(['vote', 'pair'], List());
if (hasVoted && !currentPair.includes(hasVoted)) {
return state.remove('hasVoted');
} else {
return state;
}
}
export default function(state = Map(), action) {
switch (action.type) {
case 'SET_STATE':
return resetVote(setState(state, action.state));
case 'VOTE':
return vote(state, action.entry);
}
return state;
}
我们还需要在修改一下连接逻辑:
//src/components/Voting.jsx
function mapStateToProps(state) {
return {
pair: state.getIn(['vote', 'pair']),
hasVoted: state.get('hasVoted'),
winner: state.get('winner')
};
}
现在我们依然需要为 Voting
提供一个 vote
回调函数,用来为 Sotre 指派我们新增的 action。我们依然要尽力保证Voting
组件的纯粹性,不应该依赖任何 actions 或 Redux。这些工作都应该在 react-redux 的 connect
中处理。
除了连接输入参数属性,react-redux 还可以用来连接 output actions。开始之前,我们先来介绍一下另一个 Redux 的
核心概念:Action creators。
如我们之前看到的,Redux actions 通常就是一个简单的对象,它包含一个固有的 type
属性和其它内容。我们之前都是直接
利用 js 对象字面量来直接声明所需的 actions。其实可以使用一个 factory 函数来更好的生成 actions,如下:
function vote(entry) {
return {type: 'VOTE', entry};
}
这类函数就被称为 action creators。它们就是个纯函数,用来返回 action 对象,别的没啥好介绍得了。但是你也可以
在其中实现一些内部逻辑,而避免将每次生成 action 都重复编写它们。使用 action creators 可以更好的表达所有需要分发
的 actions。
让我们新建一个用来声明客户端所需 action 的 action creators 文件:
//src/action_creators.js
export function setState(state) {
return {
type: 'SET_STATE',
state
};
}
export function vote(entry) {
return {
type: 'VOTE',
entry
};
}
我们当然也可以为 action creators 编写测试代码,但由于我们的代码逻辑太简单了,我就不再写测试了。
现在我们可以在 index.jsx
中使用我们刚新增的 setState
action creator 了:
//src/index.jsx
import React from 'react';
import Router, {Route, DefaultRoute} from 'react-router';
import {createStore} from 'redux';
import {Provider} from 'react-redux';
import io from 'socket.io-client';
import reducer from './reducer';
import {setState} from './action_creators';
import App from './components/App';
import {VotingContainer} from './components/Voting';
import {ResultsContainer} from './components/Results';
const store = createStore(reducer);
const socket = io(`${location.protocol}//${location.hostname}:8090`);
socket.on('state', state =>
store.dispatch(setState(state))
);
const routes = <Route handler={App}>
<Route path="/results" handler={ResultsContainer} />
<DefaultRoute handler={VotingContainer} />
</Route>;
Router.run(routes, (Root) => {
React.render(
<Provider store={store}>
{() => <Root />}
</Provider>,
document.getElementById('app')
);
});
使用 action creators 还有一个非常优雅的特点:在我们的场景里,我们有一个需要 vote
回调函数 props 的Vote
组件,我们同时拥有一个 vote
的 action creator。它们的名字和函数签名完全一致(都接受一个用来表示
选中项的参数)。现在我们只需要将 action creators 作为 react-redux 的 connect
函数的第二个参数,即可完成
自动关联:
//src/components/Voting.jsx
import React from 'react/addons';
import {connect} from 'react-redux';
import Winner from './Winner';
import Vote from './Vote';
import * as actionCreators from '../action_creators';
export const Voting = React.createClass({
mixins: [React.addons.PureRenderMixin],
render: function() {
return <div>
{this.props.winner ?
<Winner ref="winner" winner={this.props.winner} /> :
<Vote {...this.props} />}
</div>;
}
});
function mapStateToProps(state) {
return {
pair: state.getIn(['vote', 'pair']),
hasVoted: state.get('hasVoted'),
winner: state.get('winner')
};
}
export const VotingContainer = connect(
mapStateToProps,
actionCreators
)(Voting);
这么配置后,我们的 Voting
组件的 vote
参数属性将会与 vote
aciton creator 关联起来。这样当点击
某个投票按钮后,会导致触发 VOTE
动作。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论