4/28 讀書會
- redux 溫故知新
- curry & HOC
- redux-form
Redux 溫故知新
-
store
-
action
-
reducers
-
connect
Redux => 狀態管理的框架
store
// @flow
/* eslint global-require: 0 */
import thunk from 'redux-thunk';
import {
createStore,
compose,
applyMiddleware,
} from 'redux';
import createHistory from 'history/createBrowserHistory';
import { routerMiddleware } from 'react-router-redux';
import fetchMiddleware from 'redux-middleware-fetch';
import reducers from './reducers/index';
/* eslint-disable no-underscore-dangle */
const REDUX_DEVTOOLS = window.__REDUX_DEVTOOLS_EXTENSION__;
/* eslint-enable */
export const history = createHistory();
const storeComposeArray = [applyMiddleware(
thunk,
routerMiddleware(history),
fetchMiddleware,
)];
if (REDUX_DEVTOOLS) {
storeComposeArray.push(REDUX_DEVTOOLS());
}
export const store = createStore(
reducers,
{},
compose(...storeComposeArray),
);
if (module.hot) {
module.hot.accept('./reducers', () => store.replaceReducer(require('./reducers/index.js').default));
}
像一個 globle State,存放所有狀態的地方
抱歉字太多 我們直接來看重點
import reducers from './reducers/index';
export const store = createStore(
reducers,
{},
compose(...storeComposeArray),
);
- index.js 使用 combineReducers
把所有 reducers 綁成一個 reducer - 引入 index.js (等於引入所有 reducers)
- 引入 reducers 然後 createStore
- export 成 store 然後給 entrypoint使用
entrypoint 是啥 ??
import React from 'react';
import ReactDOM from 'react-dom';
import {
store,
history,
} from './store.js';
import App from './App.jsx';
(async () => {
const root = document.getElementById('root');
if (root) {
ReactDOM.render(
<App store={store} history={history} />,
root,
);
}
})();
action
import { API_REQUEST } from 'redux-middleware-fetch';
export function fetchAllRedEnvelopes() {
return {
[API_REQUEST]: {
types: [
ALL_RED_ENVELOPES_FETCHED,
],
auth: true,
method: 'GET',
entrypoint: '/Activity/LuckyMoneyDetail',
},
};
}
傳遞資訊給 reducer
- type
- 其他資訊
type 本為字串,改成常數方便引入
reducers
export default (state: State = {
activities: [],
allRedEnvelopes: [],
redEnvelopesPress: {},
......//more initial state
}, action: any) => {
switch (action.type) {
case ALL_RED_ENVELOPES_FETCHED:
return {
...state,
allRedEnvelopes: action.data.map(redEnvelope => ({
id: redEnvelope.activityId,
title: redEnvelope.activityName,
remark: redEnvelope.activityRemark,
})),
};
......//more case
})
(previousState, action) => newState
Pure Function
connect
// @flow
import React, { PureComponent } from 'react';
import radium from 'radium';
import { connect } from 'react-redux';
import {
bindActionCreators,
compose,
} from 'redux';
import * as ActivityActions from '../../actions/Activity';
import AnnouncementLine from '../../components/elements/AnnouncementLine.jsx';
import ActivityRedEnvelopeBanner from '../../components/Activities/RedEnvelope/ActivityRedEnvelopeBanner';
const styles = {
wrapper: {
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
flexDirection: 'column',
'@media (max-width: 1023px)': {
padding: 0,
},
},
announce: {
flexShrink: 0,
width: '100%',
'@media (max-width: 1023px)': {
display: 'none',
},
},
borderWrapper: {
margin: '13px auto',
width: 'calc(100% - 24px)',
'@media (max-width: 1023px)': {
width: '100%',
height: '100%',
border: 0,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'flex-start',
margin: 0,
},
},
titleWrapper: {
width: '100%',
height: 70,
backgroundImage: 'linear-gradient(to bottom, #F5515F 0, #9F041B 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
'@media (max-width: 1023px)': {
display: 'none',
},
},
content: {
width: '100%',
height: '100%',
padding: '27px 0',
borderWidth: '0 12px 12px 12px',
borderColor: '#9F041B',
borderStyle: 'solid',
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
flexDirection: 'column',
overflow: 'auto',
'@media (max-width: 1023px)': {
padding: 0,
borderRadius: 5,
},
'@media (max-width: 767px)': {
border: 0,
},
},
container: {
width: 960,
display: 'flex',
alignItems: 'flex-start',
justifyContent: 'flex-start',
flexWrap: 'wrap',
'@media (max-width: 1278px)': {
width: 640,
},
'@media (max-width: 1023px)': {
width: 960,
},
'@media (max-width: 993px)': {
width: 640,
},
'@media (max-width: 649px)': {
width: 320,
},
},
textTitle: {
fontSize: 25,
fontWeight: 500,
letterSpacing: 2,
color: '#ffffff',
},
};
type Props = {
fetchAllRedEnvelopes: Function,
redEnvelopes: Array<RedEnvelopeType>,
};
class ActivityRedEnvelopeList extends PureComponent<Props> {
componentDidMount() {
const { fetchAllRedEnvelopes } = this.props;
fetchAllRedEnvelopes();
}
render() {
const { redEnvelopes } = this.props;
return (
<div style={styles.wrapper}>
<div style={styles.announce}>
<AnnouncementLine />
</div>
<div style={styles.borderWrapper}>
<header style={styles.titleWrapper}>
<span style={styles.textTitle}>红包列表</span>
</header>
<div style={styles.content}>
<div style={styles.container}>
{redEnvelopes.map(redEnvelope => (
<ActivityRedEnvelopeBanner
key={redEnvelope.id}
redEnvelope={redEnvelope} />
))}
</div>
</div>
</div>
</div>
);
}
}
const reduxHook = connect(
state => ({
redEnvelopes: state.Activity.allRedEnvelopes,
}),
dispatch => bindActionCreators({
...ActivityActions,
}, dispatch),
);
export default compose(
reduxHook,
radium,
)(ActivityRedEnvelopeList);
store 改變了,但 view 呢?
用 connect 去取用
- state (資料)
- dispatch (function)
connect
// @flow
import React, { PureComponent } from 'react';
import radium from 'radium';
import { connect } from 'react-redux';
import {
bindActionCreators,
compose,
} from 'redux';
import * as ActivityActions from '../../actions/Activity';
class ActivityRedEnvelopeList extends PureComponent<Props> {
componentDidMount() {
const { fetchAllRedEnvelopes } = this.props;
fetchAllRedEnvelopes();
}
render() {
const { redEnvelopes } = this.props;
...
<div style={styles.container}>
{redEnvelopes.map(redEnvelope => (
<ActivityRedEnvelopeBanner
key={redEnvelope.id}
redEnvelope={redEnvelope} />
))}
</div>
);
}
}
const reduxHook = connect(
state => ({
redEnvelopes: state.Activity.allRedEnvelopes,
}),
dispatch => bindActionCreators({
...ActivityActions,
}, dispatch),
);
export default compose(
reduxHook,
radium,
)(ActivityRedEnvelopeList);
curry & HOC
curry ?
f(g(x))
在理解 HOC (High-Order Component)之前,你必須了解 HOF(High-Order Function)
const add = (x, y) => (x + y);
add(9, 8); // 17
// create a high-order function
const createAdder = (a) => (b) => add(a, b);
const add2 = createAdder(9);
add2(8); // 17
f(g(x))
再來看看 HOC 的定義
Takes one or more component as arguments
Returns a component as its result
const hoc1 = (Component) => {
return class extends React.Component {
constructor(props) {
super(props);
}
render() {
return <Component {...this.props} {...this.state} />
}
};
}
const EnhancedComponent = ho1(Component);
redux-form
- 為什麼要用 redux-form
- 起手式 - combinereducers
- 建立 form - reduxForm
- 建立 input - Field
- 拿 form 的值 - formValueSelector
- 換新的值 - change
為什麼要用 redux-form
方便表單提交的資料管理
起手式 - combinereducers
往前看
// @flow
import {
createStore,
combineReducers,
compose,
applyMiddleware,
} from 'redux';
import thunk from 'redux-thunk';
import { reducer as form } from 'redux-form';
import { createBrowserHistory } from 'history';
import {
connectRouter,
routerMiddleware,
} from 'connected-react-router';
import Table from './reducers/Table';
export const history = createBrowserHistory();
export default createStore(combineReducers({
Table,
form,
router: connectRouter(history),
}), {}, compose(
applyMiddleware(
thunk,
routerMiddleware(history),
),
));
建立 form - reduxForm
import {
reduxForm,
Field,
formValueSelector,
change,
SubmissionError,
} from 'redux-form';
class BoogiYan extends Component {
...
return (
...
<form>
...
</form>
)
}
const formHook = reduxForm({
form: FORM_BOOGIE,
});
export default formHook(BoogieYan)
// @flow
export const FORM_PROGRAM = 'FORM/PROGRAM';
export const FORM_PROGRAM_MODAL_CREATOR = 'FORM/PROGRAM_MODAL_CREATOR';
export const FORM_LOGIN = 'FORM/LOGIN';
export const FORM_APPLIMENT_LIST_FILTER = 'FORM/APPLIMENT_LIST_FILTER';
export const FORM_APPLIMENT = 'FORM/APPLIMENT';
export const FORM_APPLIMENT_PROGRAM_PICKER = 'FORM/APPLIMENT_PROGRAM_PICKER';
export const FORM_APPLIMENT_JUDGE_PICKER = 'FORM/APPLIMENT_JUDGE_PICKER';
export const FORM_SCHEDULE_TASK = 'FORM/SCHEDULE_TASK';
export const FORM_SCHEDULE_TASK_PROGRAM_PICKER = 'FORM/SCHEDULE_TASK_PROGRAM_PICKER';
export const FORM_REGISTER = 'FORM/REGISTER';
export const FORM_SCHEDULE_TASK_EXECUTE = 'FORM/SCHEDULE_TASK_EXECUTE';
export const FORM_REVIEW = 'FORM/REVIEW';
export const FORM_OVERVIEW_FILTER = 'FORM/OVERVIEW_FILTER';
export const FORM_OVERVIEW_LIST = 'FORM/OVERVIEW_LIST';
export const FORM_OVERVIEW_WEEK = 'FORM/OVERVIEW_WEEK';
export const FORM_PROGRAM_CONTACTS = 'FORM/PROGRAM_CONTACTS';
export const FORM_EQUIPMENT_CREATE = 'FORM/EQUIPMENT_CREATE';
export const FORM_JUDGE_LIST = 'FORM/JUDGE_LIST';
export const FORM_SCORELIST = 'FORM/SCORELIST';
export const FORM_ADD_NEW_TEACHER = 'FORM/ADD_NEW_TEACHER';
export const FORM_FILTER_TEACHER = 'FORM/FILTER_TEACHER';
export const FORM_SUPERVISOR_RECORD = 'FORM/SUPERVISOR_RECORD';
export const FORM_PROGRAM_TECHNICAL_FIRE = 'FORM/PROGRAM_TECHNICAL_FIRE';
export const FORM_ANNOUNCEMENT = 'FORM/ANNOUNCEMENT';
export default null;
form.js
建立 input - Field
import { Field } from 'redux-form'
class BoogieYan extends Component {
return (
<Field
single
placeholder="請選擇日期"
component={DatePicker}
name="heldAt" />
)
}
基本上就是個
內含input的 Component
除了吃 Component
必須要給 name 去區分它是哪個 input
拿 form 的值 - formValueSelector
import {
formValueSelector,
change,
} from 'redux-form';
import { FORM_APPLIMENT } from '../../Constant/form';
const selector = formValueSelector(FORM_APPLIMENT);
class Boogie...
const reduxHook = connect(
state => ({
judges: selector(state, 'judges') || [],
}),
);
export default reduxHook(BoogieYan)
'judges' => field name
change
change(form:String, field:String, value:String)
Saves the value to the field.
const reduxHook = connect(
state => ({
programs: selector(state, 'programs') || [],
}),
dispatch => ({
updatePrograms: newPrograms => dispatch(change(FORM_APPLIMENT, 'programs', newPrograms)),
}),
);
4/28 讀書會
By Jay Chou
4/28 讀書會
- 291