概要
普段RailsとAndroidのコードしか書いてなくて、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
};
};
感想
いまみるとなかなかにひどいですけど読めば各フォルダに書くべき内容が何となくわかると思います。
一通り機能が全部完成したらリファクタリングをしていきます。