読者です 読者をやめる 読者になる 読者になる

ゆとりエンジニア

大学生です。成長できないエンジニア。

ReactとReduxをES6で試す 実践編

概要

普段RailsAndroidのコードしか書いてなくて、js系のフレームワークを全く触ってなかったので流行りのReact.jsを使ってWebアプリを製作してみることにした。 調べてみたらReduxというmoduleが良いと書いてあったので試してみた。

読者対象は基本的に将来の自分ですが、もっとこんなのあるよ!とここ間違ってるよ!みたいな編集リクエストとかは大歓迎です。

今回の学習で自動車免許学科試験というWebアプリを作りました。よければ使ってください。

今回は製作編です。

今回作るもの

がっつりDOMの生成をしまくるやつが良かったので自動車試験の学科試験問題をひたすら解くWebアプリを作った

雛形ファイルの作成

yeoman reduxジェネレーターで雛形ファイルを生成します。

$ npm install -g yo
$ npm install -g generator-redux
$ yo redux
? What's the name of your application? Counter
? Describe your application in one sentence: ...
? Which port would you like to run on? 3000
? Install dependencies? Yes

jsのフォルダ構成

actions

「何が起きた」ということについての記述

components

コンポーネント

containers

メイン部分

reducers

Actionに従ったステートの更新を記述

store

ActionとReducerをつなげる

utils

デバグツール

各フォルダの内容

メモと一緒に記載しておきますが、フォルダ構成は真似しないでください。今見るとかなり無駄な構成となっています。

このフォルダではこんな感じのことやるのか とか こんなメソッドを使うのね 程度の参考に使っていただければと思います。

index.js

全体の構成部 Providerにstoreを渡しておいて、connectでcomponentsからアクセスする。

import React from 'react';
import ReactDOM from 'react-dom';
import App from './containers/App';
import { Provider } from 'react-redux';
import configureStore from './store/configureStore';
// 全体を生成
const store = configureStore();
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('main')
);

store

reducersからstoreを生成する。

import { createStore } from 'redux';
import rootReducer from '../reducers';

export default function configureStore(initialState) {
  const store = createStore(rootReducer, initialState);
  if (module.hot) {
    // Enable Webpack hot module replacement for reducers
    module.hot.accept('../reducers', () => {
      const nextReducer = require('../reducers').default;
      store.replaceReducer(nextReducer);
    });
  }
  return store;
}

reducers

個別のreducers。 こんな感じでreducerが複雑にならないように増やしていく

import * as types from '../constants/ActionTypes';
import json from 'json!../../data.json';
const initialState = {
  problems: json,
  id: Math.floor(Math.random()*json.length),
  ans: 1,
  dialog: false
}

export default function problems(state = initialState, action) {
  // Action以下ではstateは書き換えてはいけない。書き換えたものを渡しても更新が反映されないので注意
  let data = {};
  switch (action.type) {
    case types.CLICK_ANS:
      data = Object.assign({}, state);
      data.ans = action.id;
      data.dialog = true;
      return data;
    case types.DISMISS_DIALOG:
      data = Object.assign({}, state);
      data.dialog = false;
      data.id = Math.floor(Math.random()*state.problems.length)
      console.log(data);
      return data
    default:
      return state;
  }
}

reducerたちをまとめてstoreに渡すようのreducer

import { combineReducers } from 'redux';
import problems from './problems';

const rootReducer = combineReducers({
  problems,
});

export default rootReducer;

containers

componetを内包するRootComponent

import React, { Component, PropTypes } from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import Header from '../components/Header';
import Footer from '../components/Footer'
import MainSection from '../components/MainSection';
import * as Actions from '../actions';


class App extends Component {
  render() {
    const style = {
      body: {
        backgroundColor: "#FAFAFA",
        height: "100%",
        display: "flex",
        flexDirection: "column"
      },
    };
    const { problems, actions } = this.props;
    return (
      <div style={style.body}>
        <Header/>
        <MainSection problems={problems} actions={actions}/>
        <Footer actions={actions}/>
      </div>
    );
  }
}

// global変数から取得してpropsに渡す
function mapStateToProps(state) {
  return {
    problems: state.problems,
  };
}

function mapDispatchToProps(dispatch) {
  return {
    actions: bindActionCreators(Actions, dispatch),
  };
}
// this.propsの初期値となるproblemsとactionsを渡す
//=> const { problems, actions } = this.props
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(App);

constants

ActionのTypeを宣言しているだけ。 別に直接文字列を打ち込んでも問題はない。

export const DELETE_TODO = 'DELETE_TODO';
export const DISMISS_DIALOG = 'DISMISS_DIALOG';
export const CLICK_ANS = 'CLICK_ANS';

components

実際の要素 こいつらを組み合わせてアプリを作っていく

import React, { Component, PropTypes } from 'react';
import Problem from '../components/problem'

class MainSection extends Component {
  render() {
    const problems = this.props.problems
    const actions = this.props.actions
    const style=  {
      main: {
        margin: "16px",
        padding: "16px",
        backgroundColor: "#FFFFFF",
        flex: "1"
      }
    };
    return (
      <section className="main" style={style.main}>
        <Problem problems={problems} actions={actions} />
      </section>
    );
  }
}
export default MainSection;
import React, { Component, PropTypes } from 'react';
import Dialog from './dialog';
import * as Actions from '../actions';

class Problem extends Component {
  constructor(props) {
    super(props);
  }
  render() {
    const problems = this.props.problems.problems
    const clickAns = this.props.actions.clickAns
    const id = this.props.problems.id
    let choices = [
      {id: 1, text: "まる"},
      {id: 2, text: "ばつ"}
    ]

    const style = {
      choices: {
        marginTop: "48px",
        display: "flex"
      },
      choice: {
        flex: "1",
        textAlign: "center",
        fontWeight: "700"
      },
      problem: {
        position: "relative",
        display: "flex",
        flexDirection: "column",
        height: "100%"
      },
      text: {
        flex: "1"
      }
    }

    return (
      <div style={style.problem}>
        < Dialog problem={problems[id]} problems={this.props.problems} action={this.props.actions.dismissDialog}/>
        <p style={style.text}>{ problems[id].text }</p>
        <div style={style.choices}>
          {choices.map(choice =>
            <div style={style.choice} onClick={() => clickAns(choice.id)} key={choice.id}>{choice.text}</div>
          )}
        </div>
      </div>
    );
  }
}
export default Problem;
import React, { Component, PropTypes } from 'react';
import Problem from '../components/problem'

class MainSection extends Component {
  render() {
    const problems = this.props.problems
    const actions = this.props.actions
    const style=  {
      main: {
        margin: "16px",
        padding: "16px",
        backgroundColor: "#FFFFFF",
        flex: "1"
      }
    };
    return (
      <section className="main" style={style.main}>
        <Problem problems={problems} actions={actions} />
      </section>
    );
  }
}
export default MainSection;
import React, { Component, PropTypes } from 'react';

class Header extends Component {
  render() {
    const style = {
      header: {
        height: "64px",
        background: "#202026",
        verticalAlign: "middle",
      },
      title: {
        color: "#fff",
        fontSize: "18px",
        lineHeight: 64-8*2 + "px",
        verticalAlign: "middle",
        display: "inline-block",
        margin: "0px",
        padding: "8px 32px"
      }
    };
    return (
      <header className="header" style={style.header}>
        <h1 style={style.title}>自動車免許学科試験問題集</h1>
      </header>
    );
  }
}

export default Header;
import React, { Component, PropTypes } from 'react';

class Footer extends Component {
  render() {
    const style = {
      footer: {
        display: "flex",
        flexWrap: "nowrap",
        backgroundColor: "#fff"

      },
      menu: {
        flex: "1",
        textAlign: "center",
        padding: "16px",
        borderRight: "solid 1px #EEEEEE"
      }
    };
    return (
      <footer style={style.footer} className="footer">
        <div style={style.menu}>仮免試験</div>
        <div style={style.menu}>本免試験</div>
        <div style={style.menu}>一覧</div>
        <div style={style.menu}>とき直し</div>
      </footer>
    );
  }
}

export default Footer;
import React, { Component, PropTypes } from 'react';

class Dialog extends Component {
  render() {
    const props = this.props.problems
    const problem = this.props.problem
    // 表示状態を変える

    const style = {
      dialogBg: {
         //こんな感じでstyleに関しても変数を用いることができる
        display: props.dialog ? "block": "none",
        position: "fixed",
        zIndex: "1",
        width: "100%",
        height: "100%",
        background: "rgba(255, 255, 255, 0.8)",
        top: "0",
        left: "0"

      },
      dialog: {
        zIndex: "1",
        width: "70%",
        height: "auto",
        background: "#fff",
        margin: "0",
        padding: "24px",
        zIndex: "2",
        position: "absolute",
        top: "50%",
        left: "50%",
        transform: "translate(-50%, -50%)",
        border: "solid 1px #CCCCCC",
        borderRadius: "24px"
      },
      p: {
        marginBottom: "12px"
      },
      next: {
        textAlign: "right",
        fontWeight: "700"
      }
    };
    let judge = ""
    if (props.ans == problem.correct) {
      judge = "正解"
    } else {
      judge = "不正解"
    }
    const exp = problem.correct == 1 ? "まる" : "ばつ"
    return (
      <div style={style.dialogBg}>
        <div style={style.dialog}>
          <p style={style.p}>結果: {judge}</p>
          <p style={style.p}>問題: { problem.text }</p>
          <p style={style.p}>答え: { exp }</p>
          <p style={style.p}>{ problem.explanation }</p>
          <div style={style.next} onClick={() => this.props.action()}>次へ</div>
        </div>
      </div>
    );
  }
}
export default Dialog;

Actions

実際のActionたち。 処理をするのはreducerなのでほぼほぼ宣言だけ

import * as types from '../constants/ActionTypes';

export function dismissDialog() {
  return { type: types.DISMISS_DIALOG};
};

export function clickAns(id) {
  return {
    type: types.CLICK_ANS,
    id: id
  };
};

感想

いまみるとなかなかにひどいですけど読めば各フォルダに書くべき内容が何となくわかると思います。 一通り機能が全部完成したらリファクタリングをしていきます。

参考文献

React Redux React Redux 勝手にチュートリアル