先来一波硬广:个人博客欢迎观光:传送门
这个小应用使用Create React App建立,演示地址:https://liliang-cn.github.io/react_redux_appointment,repo地址:https://github.com/liliang-cn/react_redux_appointment。javascript
这是以前的React_appointment的Redux版,以前的演示,改写自Lynda的课程Building a Web Interface with React.js。css
最终的文件目录以下:html
react_redux_appointment/ README.md node_modules/ package.json public/ index.html favicon.ico src/ actions/ index.js components/ AddForm.js AptList.js Search.js Sort.js constants/ index.js containers/ AddForm.js App.js reducers/ apts.js formExpanded.js index.js openDialog.js orderBy.js orderDir.js query.js index.css index.js
{ "name": "react_redux_appointment", "version": "0.1.0", "private": true, "homepage": "https://liliang-cn.github.io/react_redux_appointment", "devDependencies": { "react-scripts": "0.8.4" }, "dependencies": { "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" }, "scripts": { "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" } }
小应用一共有六个状态,其中的formExpanded和openDialog是界面组件的状态,
剩下的四个分别是apts(表明全部的预定)、orderBy(根据什么来排列预定列表,根据姓名仍是根据日期)、
orderDir(排列列表的方向,是增序仍是降序)、query(搜索的关键字)。java
在应用中可能产生的actions有七种:node
addApt,即新建预定react
deleteApt, 即删除预定ios
toggleDialog, 即显示、隐藏警告框git
toggleFormExpanded, 显示/隐藏表单github
query,即查询json
changeOrderBy,即改变排序的关键字
changeOrderDir, 即改变排序方向
定义七个常量来表明这些action的类型:
constants/index.js
:
export const ADD_APT = 'ADD_APT'; export const DELETE_APT = 'DELETE_APT'; export const TOGGLE_DIALOG = 'TOGGLE_DIALOG'; export const TOGGLE_FORM_EXPANDED = 'TOGGLE_FORM_EXPANDED'; export const QUERY = 'QUERY'; export const CHANGE_ORDER_BY = 'CHANGE_ORDER_BY'; export const CHANGE_ORDER_DIR = 'CHANGE_ORDER_DIR';
actions/index.js
:
import { ADD_APT, DELETE_APT, TOGGLE_DIALOG, TOGGLE_FORM_EXPANDED, QUERY, CHANGE_ORDER_BY, CHANGE_ORDER_DIR } from '../constants'; export const addApt = (apt) => ({ type: ADD_APT, apt }); export const deleteApt = (id) => ({ type: DELETE_APT, id }); export const toggleDialog = () => ({ type: TOGGLE_DIALOG }); export const toggleFormExpanded = () => ({ type: TOGGLE_FORM_EXPANDED }); export const query = (query) => ({ type: QUERY, query }); export const changeOrderBy = (orderBy) => ({ type: CHANGE_ORDER_BY, orderBy }); export const changeOrderDir = (orderDir) => ({ type: CHANGE_ORDER_DIR, orderDir });
使用Material-UI须要引入Roboto字体:
src/index.css
@import url('https://fonts.googleapis.com/css?family=Roboto:300,400,500'); body { margin: 0; padding: 0; font-family: Roboto, sans-serif; }
components/addForm.js
:
import React from 'react'; import {Card, CardHeader, CardText} from 'material-ui/Card'; import TextField from 'material-ui/TextField'; import DatePicker from 'material-ui/DatePicker'; import TimePicker from 'material-ui/TimePicker'; import RaisedButton from 'material-ui/RaisedButton'; import Paper from 'material-ui/Paper'; import Divider from 'material-ui/Divider'; import Dialog from 'material-ui/Dialog'; import FlatButton from 'material-ui/FlatButton'; import moment from 'moment'; const paperStyle = { width: 340, margin: '0 auto 20px', textAlign: 'center' }; const buttonStyle = { margin: 12 }; // open, toggleDialog是两个布尔值,handleAdd,formExpanded, toggleFormExpanded是三个回调函数,来自于../containers/AddForm.js中的容器从store中获取并传递下来的 const AddForm = ({handleAdd, open, toggleDialog, formExpanded, toggleFormExpanded}) => { let guestName, date, time, note; // 点击Add时会先首先检查是否全部的值都有输入,若是输入合法则发起ADD_APT的action而后发起切换表单显示的action,若是输入有误则发起TOGGLE_DIALOG的action const onAdd = () => { guestName && date && time && note ? handleAdd({guestName, date, time, note}) && toggleFormExpanded() : toggleDialog() }; // 这两个函数用来获取输入的日期和时间 const handleDateChange = (event, aptDate) => { date = moment(aptDate).format('YYYY-MM-DD') }; const handleTimeChange = (event, aptTime) => { time = moment(aptTime).format('hh:mm') }; const actions = [ <FlatButton label="OK" primary={true} onTouchTap={toggleDialog} /> ]; return ( <Paper style={paperStyle} zDepth={2}> // Card组件的expanded的值是一个布尔值,来自于父组件传下来的formExpanded,即应用的状态formExpanded,用来肯定是否显示表单 <Card style={{textAlign: 'left'}} expanded={formExpanded} onExpandChange={toggleFormExpanded}> <CardHeader title="New Appointment" showExpandableButton={true} /> <CardText expandable={true}> <TextField floatingLabelText="Guest's Name" underlineShow={false} onChange={e => guestName = e.target.value.trim()} /> <Divider /> <DatePicker hintText="Date" underlineShow={false} onChange={handleDateChange} /> <Divider /> <TimePicker hintText="Time" okLabel="OK" cancelLabel="Cancel" underlineShow={false} onChange={handleTimeChange} /> <Divider /> <TextField floatingLabelText="Note" underlineShow={false} onChange={e => note = e.target.value.trim()} /> <Divider /> <RaisedButton label="Add" primary={true} style={buttonStyle} onClick={onAdd}/> <RaisedButton label="Cancel" secondary={true} style={buttonStyle} onClick={toggleFormExpanded}/> </CardText> // Dialog组件的open的值也是一个布尔值,来自于父组件传下来的open,即应用的状态openDialog,用来验证表单 <Dialog title="Caution" actions={actions} modal={false} open={open} onRequestClose={toggleDialog} > All fileds are required! </Dialog> </Card> </Paper> ); }; export default AddForm;
components/Search.js
:
import React from 'react'; import TextField from 'material-ui/TextField'; const Search = ({handleSearch}) => { return ( <div> <TextField hintText="Search" onChange={ e => handleSearch(e.target.value) } /> </div> ); }; export default Search;
components/Sort.js
:
import React from 'react'; import SelectField from 'material-ui/SelectField'; import MenuItem from 'material-ui/MenuItem' const Sort = ({ orderBy, orderDir, handleOrderByChange, handleOrderDirChange }) => { return ( <div> <SelectField floatingLabelText="Order By" value={orderBy} style={{textAlign: 'left'}} onChange={(event, index, value) => {handleOrderByChange(value)}} > <MenuItem value='guestName' primaryText="Guest's name" /> <MenuItem value='date' primaryText="Date" /> </SelectField> <SelectField floatingLabelText="Order Direction" value={orderDir} style={{textAlign: 'left'}} onChange={(event, index, value) => {handleOrderDirChange(value)}} > <MenuItem value='asc' primaryText="Ascending" /> <MenuItem value='desc' primaryText="Descending" /> </SelectField> </div> ); }; export default Sort;
这个组件的做用就是显示预定列表,接受父组件传来的apts数组和handleDelete函数,在点击RaisedButton的时候将apt.id传入handleDelete并执行。
components/AptList.js
:
import React from 'react'; import {List, ListItem} from 'material-ui/List'; import {Card, CardActions, CardHeader, CardTitle, CardText} from 'material-ui/Card'; import RaisedButton from 'material-ui/RaisedButton'; const buttonStyle = { width: '60%', margin: '12px 20%', }; const AptList = ({apts, handleDelete}) => { return ( <div> <h2>Appointments List</h2> <List> // 这里的i也能够直接用apt.id {apts.map((apt, i) => ( <ListItem key={i}> <Card style={{textAlign: 'left'}}> <CardHeader title={apt.date} subtitle={apt.time} actAsExpander={true} showExpandableButton={true} /> <CardTitle title={apt.guestName}/> <CardText expandable={true}> {apt.note} <CardActions> <RaisedButton style={buttonStyle} label="Delete" secondary={true} onClick={() => handleDelete(apt.id)} /> </CardActions> </CardText> </Card> </ListItem> ))} </List> </div> ); }; export default AptList;
reducers/formExpanded.js
:
import { TOGGLE_FORM_EXPANDED } from '../constants'; // formExpanded默认为false,即不显示,当发起类型为TOGGLE_FORM_EXPANDED的action的时候,将状态切换为true或者false const formExpanded = (state=false, action) => { switch (action.type) { case TOGGLE_FORM_EXPANDED: return !state; default: return state; } }; export default formExpanded;
reducers/openDialog.js
:
import { TOGGLE_DIALOG } from '../constants'; // 这个action是由其余action引起的 const openDialog = (state=false, action) => { switch (action.type) { case TOGGLE_DIALOG: return !state; default: return state; } }; export default openDialog;
reducers/apts.js
:
import { ADD_APT, DELETE_APT } from '../constants'; // 用惟一的id来标识不一样的预定,也能够直接用时间戳new Date() let id = 0; // 根据传入的数组和id来执行删除操做 const apts = (state=[], action) => { const handleDelete = (arr, id) => { for(let i=0; i<arr.length; i++) { if (arr[i].id === id) { return [ ...arr.slice(0, i), ...arr.slice(i+1) ] } } }; switch (action.type) { // 根据action传入的数据apt再加上id来生成一个新的预定 case ADD_APT: return [ ...state, Object.assign({}, action.apt, { id: ++id }) ] case DELETE_APT: return handleDelete(state, action.id); default: return state; } }; export default apts;
这三个函数的做用就是根据action传入的数据,更新state里的对应值,在这里并不会真正的去处理预定的列表。
reducers/orderBy.js
:
import { CHANGE_ORDER_BY } from '../constants'; const orderBy = (state=null, action) => { switch (action.type) { case CHANGE_ORDER_BY: return action.orderBy default: return state; } }; export default orderBy;
reducers/orderDir.js
:
import { CHANGE_ORDER_DIR } from '../constants'; const orderDir = (state=null, action) => { switch (action.type) { case CHANGE_ORDER_DIR: return action.orderDir default: return state; } }; export default orderDir;
reducers/query.js
:
import { QUERY } from '../constants'; const query = (state=null, action) => { switch (action.type) { case QUERY: return action.query; default: return state; } } export default query;
reducers/index.js
:
import { combineReducers } from 'redux'; import apts from './apts'; import openDialog from './openDialog'; import formExpanded from './formExpanded'; import query from './query'; import orderBy from './orderBy'; import orderDir from './orderDir'; // redux提供的combineReducers函数用来将处理不一样部分的state的函数合成一个 // 每当action进来的时候会通过每个reducer函数,可是因为action类型(type)的不一样 // 只有符合(switch语句的判断)的reducer才会处理,其余的只是将state原封不动返回 const reducers = combineReducers({ apts, openDialog, formExpanded, query, orderBy, orderDir }); export default reducers;
containers/AddForm.js
:
import { connect } from 'react-redux'; import { addApt, toggleDialog, toggleFormExpanded } from '../actions'; import AddForm from '../components/AddForm'; // AddForm组件可经过props来获取两个state:open和formExpanded const mapStateToProps = (state) => ({ open: state.openDialog, formExpanded: state.formExpanded }); // 使得AddForm组件能够经过props获得三个回调函数,调用便可至关于发起action const mapDispatchToProps = ({ toggleFormExpanded, toggleDialog, handleAdd: newApt => addApt(newApt) }); // 使用react-redux提供的connect函数,能够将一个组件提高为容器组件,容器组件可直接获取到state、能够直接使用dispatch。 // 这个connect函数接受两个函数做为参数,这两个做为参数的函数的返回值都是对象, 按约定他们分别命名为mapStateToProps,mapDispatchToProps // mapStateToProps肯定了在这个组件中能够得到哪些state,这里的话只用到了两个UI相关的state:open和formExpanded,这些state均可经过组件的props来获取 // mapDispatchToProps原本应该是返回对象的函数,这里比较简单,直接写成一个对象,肯定了哪些action是这个组件能够发起的,也是经过组件的props来获取 // connect函数的返回值是一个函数,接受一个组件做为参数。 export default connect(mapStateToProps, mapDispatchToProps)(AddForm);
containers/App.js
:
import React from 'react'; import { connect } from 'react-redux'; import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider' import injectTapEventPlugin from 'react-tap-event-plugin'; injectTapEventPlugin(); import AppBar from 'material-ui/AppBar'; import Paper from 'material-ui/Paper'; import AddForm from '../containers/AddForm'; import Search from '../components/Search'; import Sort from '../components/Sort'; import AptList from '../components/AptList'; import { deleteApt, query, changeOrderBy, changeOrderDir } from '../actions'; const paperStyle = { minHeight: 600, width: 360, margin: '20px auto', textAlign: 'center' }; const App = ({ apts, dispatch, orderBy, orderDir, handleSearch, handleDelete, handleOrderByChange, handleOrderDirChange }) => ( <MuiThemeProvider> <div> <AppBar title="React Redux Appointment" showMenuIconButton={false} /> <Paper style={paperStyle} zDepth={5}> <AddForm /> <Search handleSearch={handleSearch}/> <Sort orderBy={orderBy} orderDir={orderDir} handleOrderByChange={handleOrderByChange} handleOrderDirChange={handleOrderDirChange} /> <AptList apts={apts} handleDelete={handleDelete} /> </Paper> </div> </MuiThemeProvider> ); // 处理搜索和排序,返回处理后数组 const handledApts = (apts, query, orderBy, orderDir) => { const filterArr = (arr, query) => { return arr.filter(item => ( item.guestName.toLowerCase().indexOf(query) !== -1 || item.date.indexOf(query) !== -1 || item.time.indexOf(query) !== -1 || item.note.toLowerCase().indexOf(query) !== -1) ); }; const sortArr = (arr, orderBy, orderDir) => { if (orderBy && orderDir) { return arr.sort((apt1, apt2) => { const value1 = apt1[orderBy].toString().toLowerCase(); const value2 = apt2[orderBy].toString().toLowerCase(); if (value1 < value2) { return orderDir === 'asc' ? -1 : 1; } else if (value1 > value2) { return orderDir === 'asc' ? 1 : -1; } else { return 0; } }) } else { return arr; } }; if (!query) { return sortArr(apts, orderBy, orderDir); } else { return sortArr(filterArr(apts, query), orderBy, orderDir); } }; // App组件可经过props来获取到四个state:query, orderBy, orderDir, apts // 这里是真正处理搜索和排序的地方,并非直接将state中的apts返回,而是调用handleApts,返回处理的数组 const mapStateToProps = (state) => ({ query: state.query, orderBy: state.orderBy, orderDir: state.orderDir, apts: handledApts(state.apts, state.query, state.orderBy, state.orderDir), }); // App组件可经过props来获取到四个函数,也就是发起四个action:handleSearch,handleDelete,handleOrderByChange,handleOrderDirChange const mapDispatchToProps = ({ handleSearch: searchText => query(searchText), handleDelete: id => deleteApt(id), handleOrderByChange: orderBy => changeOrderBy(orderBy), handleOrderDirChange: orderDir => changeOrderDir(orderDir) }); export default connect(mapStateToProps, mapDispatchToProps)(App);
src/index.js
:
import React from 'react'; import ReactDOM from 'react-dom'; import { createStore } from 'redux'; import { Provider } from 'react-redux'; import App from './containers/App'; import './index.css'; import reducers from './reducers'; // 使用createStore表示应用的store,传入的第一个参数是reducers,第二个参数是Redux的调试工具 const store = createStore(reducers, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()); // 使用react-redux提供的Provider组件,使App组件及子组件能够获得store的相关的东西,如store.getState(),store.dispatch()等。 ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') );
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吧。(笑脸)