はじめに
今回は React + Redux といったイマドキの構成を使用し、2カラムの SPA(シングルページアプリケーション)を作成してみます。
なお Redux についての詳細な解説をすると長くなってしまうので今回は省略します。
環境
- Node 8.9.0
- Webpack 3.8.1
package.json
{ "name": "gaprot", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "material-ui": "^0.19.4", "react": "^16.0.0", "react-dom": "^16.0.0", "react-redux": "^5.0.6", "redux": "^v3.7.2" }, "devDependencies": { "babel-core": "^6.26.0", "babel-loader": "^7.1.2", "babel-preset-react": "^6.24.1", "webpack": "^3.8.1" } }
- material-ui を使用する場合は react-tap-event-plugin が必要でしたが、 material-ui v0.19.0 から廃止され、不要になりました。
構成
Gaprot ├── dist <- webpack によって生成される │ └── bundle.js ├── node_modules <- npm でインストールしたライブラリ ├── src │ ├── Actions <- Action Creators │ │ └── index.js │ ├── components <- Presentational Component │ │ ├── App.js │ │ ├── Content.js │ │ ├── Header.js │ │ ├── Sidebar.js │ │ └── contents │ │ ├── Page1.js │ │ ├── Page2.js │ │ ├── Page3.js │ │ └── Page4.js │ ├── constants <- Action │ │ └── ActionTypes.js │ ├── containers <- Container Component │ │ ├── content.js │ │ └── sidebar.js │ ├── index.js │ ├── reducers <- Reducer │ │ ├── index.js │ │ └── sidebar.js │ └── store <- Store │ └── configureStore.js ├── index.html └── main.css
今回はファイル構成 基本に習って細かくしていますが、個人的には Ducks がオススメです。
Ducks : https://github.com/erikras/ducks-modular-redux
1. ベースとなる HTML ファイルの作成
index.html
<!DOCTYPE html> <html> <head> <meta charset='UTF-8'> <title>React Redux</title> <link rel="stylesheet" href="main.css"> </head> <body> <div id="app"></div> <script src="dist/bundle.js"></script> </body> </html>
2. 画面を構成す コンポーネントとなる js を作成
- 基本的にこのファイル群は props で渡ってきたデータを表示するだけになります。
- このようなファイルを Presentational component と呼ぶようです。
components/App.js
import React from 'react' import Header from './Header' import Sidebar from '../containers/sidebar' import Content from '../containers/content' const App = () => { return ( <div style={styles.main}> <div style={styles.side}> <Sidebar/> </div> <div style={styles.contents}> <Content/> </div> <div style={styles.header}> <Header/> </div> </div> ) } export default App const styles = { main: { flex: 1, minHeight: '100vh' }, side: { width: '10vw', position: 'fixed', minHeight: '100vh', top: 44, backgroundColor: '#E91E63' }, contents: { position: 'fixed', top: 44, left: '10vw', bottom: 0, maxHeight: '100vh', overflowY: 'auto' }, header: { position: 'fixed', width: '100vw', right: 0 } }
components/Header.js
import React from 'react' import {AppBar, IconButton} from 'material-ui' import FaceIcon from 'material-ui/svg-icons/action/face' const Header = () => { return ( <header style={{height: 44}}> <AppBar title="Header" iconElementRight={ <IconButton> <FaceIcon/> </IconButton> } /> </header> ) } export default Header
components/Sidebar.js
import React from 'react' import FlatButton from 'material-ui/FlatButton' const Sidebar = (props) => { return ( <div> <h1 style={styles.title}>Menu</h1> <p><FlatButton style={styles.sidebarButton} label="PAGE1" onClick={props.actions.page1}/></p> <p><FlatButton style={styles.sidebarButton} label="PAGE2" onClick={props.actions.page2}/></p> <p><FlatButton style={styles.sidebarButton} label="PAGE3" onClick={props.actions.page3}/></p> <p><FlatButton style={styles.sidebarButton} label="PAGE4" onClick={props.actions.page4}/></p> </div> ) } export default Sidebar const styles = { title: { textAlign: 'center', color: '#FFFFFF', }, sidebarButton: { display: 'block', width: '100%', textAlign: 'left', textDecoration: 'none', color: '#FFFFFF', } }
components/Content.js
import React from 'react' import Page1 from './contents/Page1' import Page2 from './contents/Page2' import Page3 from './contents/Page3' import Page4 from "./contents/Page4" const Content = (props) => { const type = props.types.value console.log("content props", type) switch (type) { case 'PAGE1': return <Page1/> case 'PAGE2': return <Page2/> case 'PAGE3': return <Page3/> case 'PAGE4': return <Page4/> } } export default Content
components/contents/Page.js
- コンテンツ内容を表示するファイル。
- 今回はこのファイルを Page1~Page4 まで作成しています。
import React from 'react' const Page1 = () => { return ( <div style={{marginLeft: 20, marginTop: 20}}> <h1>Page1</h1> </div> ) }; export default Page1
3. Action を作成
constants/ActionTypes.js
export const PAGE1 = 'PAGE1' export const PAGE2 = 'PAGE2' export const PAGE3 = 'PAGE3' export const PAGE4 = 'PAGE4'
4. Action Creator を作成
- アクションオブジェクトを返す関数。
- type フィールドは必須。
- type 以外のアクションオブジェクトの構造は、自由に設定できます。
Actions/index.js
import * as types from '../constants/ActionTypes' export const page1 = () => { return { type: types.PAGE1 } } export const page2 = () => { return { type: types.PAGE2 } } export const page3 = () => { return { type: types.PAGE3 } } export const page4 = () => { return { type: types.PAGE4 } }
5. Container を作成
- connect を使用することで store を通じでコンポーネントの props に state を渡しています。
- 後述するルー となる
src/index.js
Provider によって store のインスタンスが使用できるようになります。
<Provider store={store}>
containers/content.js
import Content from '../components/Content' import {connect} from 'react-redux' export default connect( // mapStateToProps. (store) => ({types: store.sidebar}) )(Content)
containers/sidebar.js
import Sidebar from '../components/Sidebar' import {connect} from 'react-redux' import {bindActionCreators} from 'redux' import {page1, page2, page3, page4} from '../Actions' const mapDispatchToProps = (dispatch) => { return { actions: bindActionCreators({page1, page2, page3, page4}, dispatch) } } export default connect( // mapStateToProps. (store) => ({types: store.sidebar}), mapDispatchToProps )(Sidebar)
6. Reducer を作成
reducers/index.js
Reducer の関数をまとめて store に登録します。 combineReducers を使用することで複数の Reducer 設定することができます。import {combineReducers} from 'redux' import sidebar from './sidebar' const rootReducer = combineReducers({ sidebar }) export default rootReducer
reducers/sidebar.js
state と action から、新しい state を生成します。import {PAGE1, PAGE2, PAGE3, PAGE4} from '../constants/ActionTypes' const initialState = {value: 'PAGE1'} export default function sidebar(state = initialState, action) { switch (action.type) { case PAGE1: return {value: 'PAGE1'} case PAGE2: return {value: 'PAGE2'} case PAGE3: return {value: 'PAGE3'} case PAGE4: return {value: 'PAGE4'} default: return state } }
7. Store を作成
store/configureStore.js
import {createStore} from 'redux' import rootReducer from '../reducers' const configureStore = () => { const store = createStore( rootReducer ) return store } export default configureStore
8. 最後にルートとなる index.js を作成
src/index.js
import React from 'react' import {render} from 'react-dom' import {Provider} from 'react-redux' import App from './components/App' import configureStore from './store/configureStore' // material-ui の theme を設定. import lightBaseTheme from 'material-ui/styles/baseThemes/lightBaseTheme' import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider' import getMuiTheme from 'material-ui/styles/getMuiTheme' const store = configureStore() render( <Provider store={store}> <MuiThemeProvider muiTheme={getMuiTheme(lightBaseTheme)}> <App/> </MuiThemeProvider> </Provider>, document.getElementById('app') )
動作を確認
webpack --progress --colors python -m SimpleHTTPServer
ブラウザを開いて http://localhost:8000 に接続。
画面遷移を行わずにコンテンツ部分が変更されていると思います。

まとめ
Redux の他にも、 Angular や Meteor など、 SPA の作成に適したフレームワークがあります。 SPA を取り入れることで、サーバー・ブラウザの負荷軽減や、通信量の削減にも繋がります。 Web サービスなどではまだまだ浸透していないように思いますが、選択肢の一つとして良いと思います。
今回のサンプルコードは github にも公開してありますので、よろしければ参考にしてみてください。
github : https://github.com/gaprot/Redux-SPA