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