先來一波硬廣:個人博客歡迎觀光:傳送門
這個小應用使用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吧。(笑臉)