返回介绍

从 react 组件中指派 actions

发布于 2025-02-17 12:51:32 字数 7299 浏览 0 评论 0 收藏 0

我们已经知道如何从 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文