React-Redux小应用(一)-React_Redux_Appointment

前端之家收集整理的这篇文章主要介绍了React-Redux小应用(一)-React_Redux_Appointment前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

React-Redux-Appointment

先来一波硬广:我的博客欢迎观光:传送门
这个小应用使用Create React App创建,演示地址:https://liliang-cn.github.io/react_redux_appointment,repo地址:https://github.com/liliang-cn/react_redux_appointment

这是之前的React_appointment的Redux版,之前的演示,改写自Lynda的课程Building a Web Interface with React.js

文件结构

最终的文件目录如下:

  1. react_redux_appointment/
  2. README.md
  3. node_modules/
  4. package.json
  5. public/
  6. index.html
  7. favicon.ico
  8. src/
  9. actions/
  10. index.js
  11. components/
  12. AddForm.js
  13. AptList.js
  14. Search.js
  15. Sort.js
  16. constants/
  17. index.js
  18. containers/
  19. AddForm.js
  20. App.js
  21. reducers/
  22. apts.js
  23. formExpanded.js
  24. index.js
  25. openDialog.js
  26. orderBy.js
  27. orderDir.js
  28. query.js
  29. index.css
  30. index.js

用到的模块

  1. {
  2. "name": "react_redux_appointment","version": "0.1.0","private": true,"homepage": "https://liliang-cn.github.io/react_redux_appointment","devDependencies": {
  3. "react-scripts": "0.8.4"
  4. },"dependencies": {
  5. "axios": "^0.15.3","gh-pages": "^0.12.0","lodash": "^4.17.2","material-ui": "^0.16.5","moment": "^2.17.1","react": "^15.4.1","react-dom": "^15.4.1","react-redux": "^5.0.1","react-tap-event-plugin": "^2.0.1","redux": "^3.6.0"
  6. },"scripts": {
  7. "start": "react-scripts start","build": "react-scripts build","deploy": "yarn build && gh-pages -d build","test": "react-scripts test --env=jsdom","eject": "react-scripts eject"
  8. }
  9. }

所有的state

小应用一共有六个状态,其中的formExpanded和openDialog是界面组件的状态,
剩下的四个分别是apts(代表所有的预约)、orderBy(根据什么来排列预约列表,根据姓名还是根据日期)、
orderDir(排列列表的方向,是增序还是降序)、query(搜索的关键字)。

所有的Action

在应用中可能产生的actions有七种:

  • addApt,即新建预约

  • deleteApt, 即删除预约

  • toggleDialog, 即显示、隐藏警告框

  • toggleFormExpanded,显示/隐藏表单

  • query,即查询

  • changeOrderBy,即改变排序的关键字

  • changeOrderDir,即改变排序方向

定义七个常量来代表这些action的类型:

constants/index.js:

  1. export const ADD_APT = 'ADD_APT';
  2.  
  3. export const DELETE_APT = 'DELETE_APT';
  4.  
  5. export const TOGGLE_DIALOG = 'TOGGLE_DIALOG';
  6.  
  7. export const TOGGLE_FORM_EXPANDED = 'TOGGLE_FORM_EXPANDED';
  8.  
  9. export const QUERY = 'QUERY';
  10.  
  11. export const CHANGE_ORDER_BY = 'CHANGE_ORDER_BY';
  12.  
  13. export const CHANGE_ORDER_DIR = 'CHANGE_ORDER_DIR';

actions/index.js:

  1. import {
  2. ADD_APT,DELETE_APT,TOGGLE_DIALOG,TOGGLE_FORM_EXPANDED,QUERY,CHANGE_ORDER_BY,CHANGE_ORDER_DIR
  3. } from '../constants';
  4.  
  5. export const addApt = (apt) => ({
  6. type: ADD_APT,apt
  7. });
  8.  
  9. export const deleteApt = (id) => ({
  10. type: DELETE_APT,id
  11. });
  12.  
  13. export const toggleDialog = () => ({
  14. type: TOGGLE_DIALOG
  15. });
  16.  
  17. export const toggleFormExpanded = () => ({
  18. type: TOGGLE_FORM_EXPANDED
  19. });
  20.  
  21. export const query = (query) => ({
  22. type: QUERY,query
  23. });
  24.  
  25. export const changeOrderBy = (orderBy) => ({
  26. type: CHANGE_ORDER_BY,orderBy
  27. });
  28.  
  29. export const changeOrderDir = (orderDir) => ({
  30. type: CHANGE_ORDER_DIR,orderDir
  31. });

UI组件

样式

使用Material-UI需要引入Roboto字体:

src/index.css

  1. @import url('https://fonts.googleapis.com/css?family=Roboto:300,400,500');
  2. body {
  3. margin: 0;
  4. padding: 0;
  5. font-family: Roboto,sans-serif;
  6. }

表单组件

components/addForm.js:

  1. import React from 'react';
  2.  
  3. import {Card,CardHeader,CardText} from 'material-ui/Card';
  4. import TextField from 'material-ui/TextField';
  5. import DatePicker from 'material-ui/DatePicker';
  6. import TimePicker from 'material-ui/TimePicker';
  7. import RaisedButton from 'material-ui/RaisedButton';
  8. import Paper from 'material-ui/Paper';
  9. import Divider from 'material-ui/Divider';
  10. import Dialog from 'material-ui/Dialog';
  11. import FlatButton from 'material-ui/FlatButton';
  12.  
  13. import moment from 'moment';
  14.  
  15. const paperStyle = {
  16. width: 340,margin: '0 auto 20px',textAlign: 'center'
  17. };
  18.  
  19. const buttonStyle = {
  20. margin: 12
  21. };
  22.  
  23. // open,toggleDialog是两个布尔值,handleAdd,formExpanded,toggleFormExpanded是三个回调函数,来自于../containers/AddForm.js中的容器从store中获取并传递下来的
  24. const AddForm = ({handleAdd,open,toggleDialog,formExpanded,toggleFormExpanded}) => {
  25. let guestName,date,time,note;
  26. // 点击Add时会先首先检查是否所有的值都有输入,如果输入合法则发起ADD_APT的action然后发起切换表单显示的action,如果输入有误则发起TOGGLE_DIALOG的action
  27. const onAdd = () => {
  28. guestName && date && time && note
  29. ?
  30. handleAdd({guestName,note}) && toggleFormExpanded()
  31. :
  32. toggleDialog()
  33. };
  34.  
  35. // 这两个函数用来获取输入的日期和时间
  36. const handleDateChange = (event,aptDate) => {
  37. date = moment(aptDate).format('YYYY-MM-DD')
  38. };
  39.  
  40. const handleTimeChange = (event,aptTime) => {
  41. time = moment(aptTime).format('hh:mm')
  42. };
  43.  
  44. const actions = [
  45. <FlatButton
  46. label="OK"
  47. primary={true}
  48. onTouchTap={toggleDialog}
  49. />
  50. ];
  51.  
  52. return (
  53. <Paper style={paperStyle} zDepth={2}>
  54. // Card组件的expanded的值是一个布尔值,来自于父组件传下来的formExpanded,即应用的状态formExpanded,用来确定是否显示表单
  55. <Card style={{textAlign: 'left'}} expanded={formExpanded} onExpandChange={toggleFormExpanded}>
  56. <CardHeader
  57. title="New Appointment"
  58. showExpandableButton={true}
  59. />
  60. <CardText expandable={true}>
  61. <TextField
  62. floatingLabelText="Guest's Name"
  63. underlineShow={false}
  64. onChange={e => guestName = e.target.value.trim()}
  65. />
  66. <Divider />
  67. <DatePicker
  68. hintText="Date"
  69. underlineShow={false}
  70. onChange={handleDateChange}
  71. />
  72. <Divider />
  73. <TimePicker
  74. hintText="Time"
  75. okLabel="OK"
  76. cancelLabel="Cancel"
  77. underlineShow={false}
  78. onChange={handleTimeChange}
  79. />
  80. <Divider />
  81. <TextField
  82. floatingLabelText="Note"
  83. underlineShow={false}
  84. onChange={e => note = e.target.value.trim()}
  85. />
  86. <Divider />
  87. <RaisedButton label="Add" primary={true} style={buttonStyle} onClick={onAdd}/>
  88. <RaisedButton label="Cancel" secondary={true} style={buttonStyle} onClick={toggleFormExpanded}/>
  89. </CardText>
  90. // Dialog组件的open的值也是一个布尔值,来自于父组件传下来的open,即应用的状态openDialog,用来验证表单
  91. <Dialog
  92. title="Caution"
  93. actions={actions}
  94. modal={false}
  95. open={open}
  96. onRequestClose={toggleDialog}
  97. >
  98. All fileds are required!
  99. </Dialog>
  100. </Card>
  101. </Paper>
  102. );
  103. };
  104.  
  105. export default AddForm;

搜索表单

components/Search.js:

  1. import React from 'react';
  2. import TextField from 'material-ui/TextField';
  3.  
  4. const Search = ({handleSearch}) => {
  5. return (
  6. <div>
  7. <TextField
  8. hintText="Search"
  9. onChange={
  10. e => handleSearch(e.target.value)
  11. }
  12. />
  13. </div>
  14. );
  15. };
  16.  
  17. export default Search;

排列选择

components/Sort.js:

  1. import React from 'react';
  2.  
  3. import SelectField from 'material-ui/SelectField';
  4. import MenuItem from 'material-ui/MenuItem'
  5.  
  6. const Sort = ({
  7. orderBy,orderDir,handleOrderByChange,handleOrderDirChange
  8. }) => {
  9. return (
  10. <div>
  11. <SelectField
  12. floatingLabelText="Order By"
  13. value={orderBy}
  14. style={{textAlign: 'left'}}
  15. onChange={(event,index,value) => {handleOrderByChange(value)}}
  16. >
  17. <MenuItem value='guestName' primaryText="Guest's name" />
  18. <MenuItem value='date' primaryText="Date" />
  19. </SelectField>
  20.  
  21. <SelectField
  22. floatingLabelText="Order Direction"
  23. value={orderDir}
  24. style={{textAlign: 'left'}}
  25. onChange={(event,value) => {handleOrderDirChange(value)}}
  26. >
  27. <MenuItem value='asc' primaryText="Ascending" />
  28. <MenuItem value='desc' primaryText="Descending" />
  29. </SelectField>
  30. </div>
  31. );
  32. };
  33.  
  34. export default Sort;

预约列表

这个组件的作用就是显示预约列表,接受父组件传来的apts数组和handleDelete函数,在点击RaisedButton的时候将apt.id传入handleDelete并执行。

components/AptList.js:

  1. import React from 'react';
  2. import {List,ListItem} from 'material-ui/List';
  3. import {Card,CardActions,CardTitle,CardText} from 'material-ui/Card';
  4. import RaisedButton from 'material-ui/RaisedButton';
  5.  
  6. const buttonStyle = {
  7. width: '60%',margin: '12px 20%',};
  8.  
  9. const AptList = ({apts,handleDelete}) => {
  10. return (
  11. <div>
  12. <h2>Appointments List</h2>
  13. <List>
  14. // 这里的i也可以直接用apt.id
  15. {apts.map((apt,i) => (
  16. <ListItem key={i}>
  17. <Card style={{textAlign: 'left'}}>
  18. <CardHeader
  19. title={apt.date}
  20. subtitle={apt.time}
  21. actAsExpander={true}
  22. showExpandableButton={true}
  23. />
  24. <CardTitle title={apt.guestName}/>
  25. <CardText expandable={true}>
  26. {apt.note}
  27. <CardActions>
  28. <RaisedButton
  29. style={buttonStyle}
  30. label="Delete"
  31. secondary={true}
  32. onClick={() => handleDelete(apt.id)}
  33. />
  34. </CardActions>
  35. </CardText>
  36. </Card>
  37. </ListItem>
  38. ))}
  39. </List>
  40. </div>
  41. );
  42. };
  43.  
  44. export default AptList;

处理不同的actions

处理表单的显示和隐藏

reducers/formExpanded.js:

  1. import { TOGGLE_FORM_EXPANDED } from '../constants';
  2.  
  3. // formExpanded默认为false,即不显示,当发起类型为TOGGLE_FORM_EXPANDED的action的时候,将状态切换为true或者false
  4. const formExpanded = (state=false,action) => {
  5. switch (action.type) {
  6. case TOGGLE_FORM_EXPANDED:
  7. return !state;
  8. default:
  9. return state;
  10. }
  11. };
  12.  
  13. export default formExpanded;

表单验证错误提示对话框

reducers/openDialog.js:

  1. import { TOGGLE_DIALOG } from '../constants';
  2.  
  3. // 这个action是由其他action引发的
  4. const openDialog = (state=false,action) => {
  5. switch (action.type) {
  6. case TOGGLE_DIALOG:
  7. return !state;
  8. default:
  9. return state;
  10. }
  11. };
  12.  
  13. export default openDialog;

处理新建预约和删除预约

reducers/apts.js:

  1. import { ADD_APT,DELETE_APT } from '../constants';
  2.  
  3. // 用唯一的id来标识不同的预约,也可以直接用时间戳new Date()
  4. let id = 0;
  5.  
  6. // 根据传入的数组和id来执行删除操作
  7. const apts = (state=[],action) => {
  8. const handleDelete = (arr,id) => {
  9. for(let i=0; i<arr.length; i++) {
  10. if (arr[i].id === id) {
  11. return [
  12. ...arr.slice(0,i),...arr.slice(i+1)
  13. ]
  14. }
  15. }
  16. };
  17.  
  18. switch (action.type) {
  19. // 根据action传入的数据apt再加上id来生成一个新的预约
  20. case ADD_APT:
  21. return [
  22. ...state,Object.assign({},action.apt,{
  23. id: ++id
  24. })
  25. ]
  26. case DELETE_APT:
  27. return handleDelete(state,action.id);
  28. default:
  29. return state;
  30. }
  31. };
  32.  
  33. export default apts;

查询和排列方式

这三个函数的作用就是根据action传入的数据,更新state里的对应值,在这里并不会真正的去处理预约的列表。

reducers/orderBy.js:

  1. import { CHANGE_ORDER_BY } from '../constants';
  2.  
  3. const orderBy = (state=null,action) => {
  4. switch (action.type) {
  5. case CHANGE_ORDER_BY:
  6. return action.orderBy
  7. default:
  8. return state;
  9. }
  10. };
  11.  
  12. export default orderBy;

reducers/orderDir.js:

  1. import { CHANGE_ORDER_DIR } from '../constants';
  2.  
  3. const orderDir = (state=null,action) => {
  4. switch (action.type) {
  5. case CHANGE_ORDER_DIR:
  6. return action.orderDir
  7. default:
  8. return state;
  9. }
  10. };
  11.  
  12. export default orderDir;

reducers/query.js:

  1. import { QUERY } from '../constants';
  2.  
  3. const query = (state=null,action) => {
  4. switch (action.type) {
  5. case QUERY:
  6. return action.query;
  7. default:
  8. return state;
  9. }
  10. }
  11.  
  12. export default query;

合成reducers

reducers/index.js:

  1. import { combineReducers } from 'redux';
  2.  
  3. import apts from './apts';
  4. import openDialog from './openDialog';
  5. import formExpanded from './formExpanded';
  6. import query from './query';
  7. import orderBy from './orderBy';
  8. import orderDir from './orderDir';
  9.  
  10. // redux提供的combineReducers函数用来将处理不同部分的state的函数合成一个
  11. // 每当action进来的时候会经过每一个reducer函数,但是由于action类型(type)的不同
  12. // 只有符合(switch语句的判断)的reducer才会处理,其他的只是将state原封不动返回
  13.  
  14. const reducers = combineReducers({
  15. apts,openDialog,query,orderBy,orderDir
  16. });
  17.  
  18. export default reducers;

容器组件

containers/AddForm.js:

  1. import { connect } from 'react-redux';
  2.  
  3. import { addApt,toggleFormExpanded } from '../actions';
  4.  
  5. import AddForm from '../components/AddForm';
  6.  
  7. // AddForm组件可通过props来获取两个state:open和formExpanded
  8. const mapStateToProps = (state) => ({
  9. open: state.openDialog,formExpanded: state.formExpanded
  10. });
  11.  
  12. // 使得AddForm组件可以通过props得到三个回调函数调用即可相当于发起action
  13. const mapDispatchToProps = ({
  14. toggleFormExpanded,handleAdd: newApt => addApt(newApt)
  15. });
  16.  
  17. // 使用react-redux提供的connect函数,可以将一个组件提升为容器组件,容器组件可直接获取到state、可以直接使用dispatch。
  18. // 这个connect函数接受两个函数作为参数,这两个作为参数的函数的返回值都是对象,按约定他们分别命名为mapStateToProps,mapDispatchToProps
  19. // mapStateToProps确定了在这个组件中可以获得哪些state,这里的话只用到了两个UI相关的state:open和formExpanded,这些state都可通过组件的props来获取
  20. // mapDispatchToProps本来应该是返回对象的函数,这里比较简单,直接写成一个对象,确定了哪些action是这个组件可以发起的,也是通过组件的props来获取
  21. // connect函数的返回值是一个函数,接受一个组件作为参数。
  22.  
  23. export default connect(mapStateToProps,mapDispatchToProps)(AddForm);

containers/App.js:

  1. import React from 'react';
  2. import { connect } from 'react-redux';
  3.  
  4. import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'
  5. import injectTapEventPlugin from 'react-tap-event-plugin';
  6.  
  7. injectTapEventPlugin();
  8. import AppBar from 'material-ui/AppBar';
  9. import Paper from 'material-ui/Paper';
  10.  
  11. import AddForm from '../containers/AddForm';
  12. import Search from '../components/Search';
  13. import Sort from '../components/Sort';
  14. import AptList from '../components/AptList';
  15.  
  16. import { deleteApt,changeOrderBy,changeOrderDir } from '../actions';
  17.  
  18. const paperStyle = {
  19. minHeight: 600,width: 360,margin: '20px auto',textAlign: 'center'
  20. };
  21.  
  22. const App = ({
  23. apts,dispatch,handleSearch,handleDelete,handleOrderDirChange
  24. }) => (
  25. <MuiThemeProvider>
  26. <div>
  27. <AppBar
  28. title="React Redux Appointment"
  29. showMenuIconButton={false}
  30. />
  31. <Paper style={paperStyle} zDepth={5}>
  32. <AddForm />
  33. <Search handleSearch={handleSearch}/>
  34. <Sort
  35. orderBy={orderBy}
  36. orderDir={orderDir}
  37. handleOrderByChange={handleOrderByChange}
  38. handleOrderDirChange={handleOrderDirChange}
  39. />
  40. <AptList
  41. apts={apts}
  42. handleDelete={handleDelete}
  43. />
  44. </Paper>
  45. </div>
  46. </MuiThemeProvider>
  47. );
  48.  
  49.  
  50. // 处理搜索和排序,返回处理后数组
  51. const handledApts = (apts,orderDir) => {
  52. const filterArr = (arr,query) => {
  53. return arr.filter(item => (
  54. item.guestName.toLowerCase().indexOf(query) !== -1 ||
  55. item.date.indexOf(query) !== -1 ||
  56. item.time.indexOf(query) !== -1 ||
  57. item.note.toLowerCase().indexOf(query) !== -1)
  58. );
  59. };
  60.  
  61. const sortArr = (arr,orderDir) => {
  62. if (orderBy && orderDir) {
  63. return arr.sort((apt1,apt2) => {
  64. const value1 = apt1[orderBy].toString().toLowerCase();
  65. const value2 = apt2[orderBy].toString().toLowerCase();
  66. if (value1 < value2) {
  67. return orderDir === 'asc' ? -1 : 1;
  68. } else if (value1 > value2) {
  69. return orderDir === 'asc' ? 1 : -1;
  70. } else {
  71. return 0;
  72. }
  73. })
  74. } else {
  75. return arr;
  76. }
  77. };
  78.  
  79. if (!query) {
  80. return sortArr(apts,orderDir);
  81. } else {
  82. return sortArr(filterArr(apts,query),orderDir);
  83. }
  84. };
  85.  
  86.  
  87. // App组件可通过props来获取到四个state:query,apts
  88. // 这里是真正处理搜索和排序的地方,并不是直接将state中的apts返回,而是调用handleApts,返回处理的数组
  89. const mapStateToProps = (state) => ({
  90. query: state.query,orderBy: state.orderBy,orderDir: state.orderDir,apts: handledApts(state.apts,state.query,state.orderBy,state.orderDir),});
  91.  
  92. // App组件可通过props来获取到四个函数,也就是发起四个action:handleSearch,handleDelete,handleOrderByChange,handleOrderDirChange
  93. const mapDispatchToProps = ({
  94. handleSearch: searchText => query(searchText),handleDelete: id => deleteApt(id),handleOrderByChange: orderBy => changeOrderBy(orderBy),handleOrderDirChange: orderDir => changeOrderDir(orderDir)
  95. });
  96.  
  97. export default connect(mapStateToProps,mapDispatchToProps)(App);

入口文件

src/index.js:

  1. import React from 'react';
  2. import ReactDOM from 'react-dom';
  3.  
  4. import { createStore } from 'redux';
  5. import { Provider } from 'react-redux';
  6.  
  7. import App from './containers/App';
  8. import './index.css';
  9.  
  10. import reducers from './reducers';
  11.  
  12. // 使用createStore表示应用的store,传入的第一个参数是reducers,第二个参数是Redux的调试工具
  13. const store = createStore(reducers,window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());
  14.  
  15. // 使用react-redux提供的Provider组件,使App组件及子组件可以得到store的相关的东西,如store.getState(),store.dispatch()等。
  16. ReactDOM.render(
  17. <Provider store={store}>
  18. <App />
  19. </Provider>,document.getElementById('root')
  20. );

结尾

React提供的是通过state来控制控制UI和单向数据流动,
Redux提供的是单一数据源和只能通过action和reducer来处理state的更新。

以其中的点击按钮显示新建预约表单的过程来捋一捋React、React-Redux的逻辑(灵感来源于自Cory House大神):

  • 用户:点击按钮

  • React:哈喽,action生成函数toggleFormExpanded,有人点击了展开新建预约的表单。

  • Action:收到,谢谢React,我马上发布一个action也就是{type:TOGGLE_FORM_EXPANDED}告诉reducers来更新state。

  • Reducer:谢谢Action,我收到你的传过来要执行的action了,我会根据你传递进来的{type:TOGGLE_FORM_EXPANDED},先复制一份当前的state,然后把state中的formExpanded的值更新为true,然后把新的state给Store。

  • Store:嗯,Reducer你干得漂亮,我收到了新的state,我会通知所有与我连接的组件,确保他们会收到新state。

  • React-Redux:啊,感谢Store传来的新数据,我现在就看看React界面是否需要需要发生变化,啊,需要把新建预约的表单显示出来啊,那界面还是要更新一下的,交给你了,React。

  • React:好的,有新的数据由store通过props传递下来的数据了,我会马上根据这个数据把新建预约的表单显示出来。

  • 用户:看到了新建预约的表单。

如果觉得还不错,来个star吧。(笑脸)

猜你在找的React相关文章