├── .babelrc ├── .gitignore ├── README.md ├── client ├── components │ ├── left-panel-controller │ │ ├── index.js │ │ ├── left-panel-controller.js │ │ └── styles.css │ ├── menu │ │ ├── index.js │ │ ├── menu.js │ │ └── style.css │ └── user-list │ │ ├── index.js │ │ ├── styles.css │ │ ├── user-list-item.js │ │ └── user-list.js ├── index.html ├── index.js ├── routes.js ├── stores │ ├── menu-store.js │ └── user-store.js └── views │ ├── app.css │ ├── app.js │ ├── global.css │ └── home │ └── index.js ├── conf ├── webpack.base.config.js ├── webpack.development.config.js └── webpack.production.config.js ├── package.json ├── server.js └── webpack.config.babel.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "react" 5 | ], 6 | "plugins": [ 7 | "react-hot-loader/babel", 8 | "transform-decorators-legacy", 9 | "transform-class-properties" 10 | ] 11 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-hot-mobx-es6 2 | A minimal skeleton for building React ES6 apps using Babel. 3 | 4 | ##Getting Started 5 | 6 | ```npm install``` - Install dependencies. 7 | ```npm run build-dev``` - Run development build with hot reload. If it doesn't start, make sure you aren't running anything else in the same port. Default PORT=7700 8 | ```npm run build-prod``` - Run production build. See `public` folder. 9 | 10 | Start modifying the code. The browser should pick up the changes. 11 | -------------------------------------------------------------------------------- /client/components/left-panel-controller/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { inject } from 'mobx-react'; 3 | 4 | import LeftPanelController from './left-panel-controller'; 5 | 6 | const Component = inject('leftMenuStore')(({ leftMenuStore }) => { 7 | return ( 8 | leftMenuStore.openLeftPanel()} 10 | closePanel={() => leftMenuStore.closeLeftPanel()} /> 11 | ); 12 | }); 13 | 14 | Component.displayName = 'LeftPanelControllerContainer'; 15 | export default Component; 16 | -------------------------------------------------------------------------------- /client/components/left-panel-controller/left-panel-controller.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | /* styles */ 4 | import style from './styles.css'; 5 | 6 | const LeftPanelController = props => ( 7 | 8 | props.openPanel()}>Open left panel 9 | props.closePanel()}>Close left panel 10 | 11 | ); 12 | 13 | export default LeftPanelController; 14 | -------------------------------------------------------------------------------- /client/components/left-panel-controller/styles.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: column; 4 | } -------------------------------------------------------------------------------- /client/components/menu/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { observer, inject } from 'mobx-react'; 3 | 4 | import Menu from './menu' 5 | 6 | const Component = inject('leftMenuStore')(observer(({ leftMenuStore }) => ( 7 | leftMenuStore.toggleLeftPanel()} 9 | isOpenLeftPanel={leftMenuStore.isOpenLeftPanel} /> 10 | ))); 11 | 12 | Component.displayName = 'MenuContainer'; 13 | export default Component; 14 | -------------------------------------------------------------------------------- /client/components/menu/menu.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import cn from 'classnames'; 3 | 4 | import styles from './style.css'; 5 | 6 | const Menu = props => ( 7 | 8 | ☰ 10 | 11 | ); 12 | 13 | export default Menu; 14 | -------------------------------------------------------------------------------- /client/components/menu/style.css: -------------------------------------------------------------------------------- 1 | .menu { 2 | position: fixed; 3 | top: 0; 4 | left: -180px; 5 | bottom: 0; 6 | width: 220px; 7 | background-color: tomato; 8 | &.active { 9 | left: 0; 10 | } 11 | & .toggle-btn { 12 | position: absolute; 13 | top: 5px; 14 | right: 10px; 15 | font-size: 26px; 16 | font-weight: 500; 17 | color: white; 18 | cursor: pointer; 19 | } 20 | } -------------------------------------------------------------------------------- /client/components/user-list/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { observer, inject } from 'mobx-react'; 3 | 4 | import UserList from './user-list'; 5 | 6 | const Component = inject('userStore')(observer(({ userStore }) => { 7 | return ( 8 | 11 | ); 12 | })); 13 | 14 | Component.displayName = 'UserList'; 15 | export default Component; 16 | -------------------------------------------------------------------------------- /client/components/user-list/styles.css: -------------------------------------------------------------------------------- 1 | .container { 2 | 3 | } -------------------------------------------------------------------------------- /client/components/user-list/user-list-item.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const UserListItem = props => ( 4 | props.onToggle()} />{props.text} 5 | 6 | ); 7 | export default UserListItem; 8 | -------------------------------------------------------------------------------- /client/components/user-list/user-list.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | /* components */ 4 | import UserListItem from './user-list-item'; 5 | 6 | /* styles */ 7 | import style from './styles.css'; 8 | 9 | const UserList = props => { 10 | return ( 11 | 12 | 13 | {props.users.map(userStore => { 14 | return ( 15 | userStore.toggle()} />); 20 | })} 21 | 22 | {`Users:${props.users.length}`} 23 | {`Selected users: ${props.selectedUsersCount}`} 24 | 25 | ); 26 | }; 27 | 28 | export default UserList; 29 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Simple react + mobx 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { AppContainer } from 'react-hot-loader'; 3 | import ReactDOM from 'react-dom'; 4 | import AppRouter from './routes'; 5 | 6 | const render = (Component) => 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('app') 12 | ); 13 | 14 | render(AppRouter); 15 | if (module.hot) { 16 | module.hot.accept('./routes', () => { 17 | require('./routes'); 18 | render(AppRouter); 19 | }); 20 | } 21 | 22 | -------------------------------------------------------------------------------- /client/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Router, 4 | Route, 5 | IndexRoute, 6 | browserHistory 7 | } from 'react-router'; 8 | 9 | /* views */ 10 | import App from './views/app'; 11 | import Home from './views/home'; 12 | 13 | export default () => ( 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | -------------------------------------------------------------------------------- /client/stores/menu-store.js: -------------------------------------------------------------------------------- 1 | import { observable, computed, action, autorun } from 'mobx'; 2 | 3 | class MenuStore { 4 | @observable show; 5 | 6 | constructor() { 7 | this.show = false; 8 | } 9 | 10 | @computed get isOpenLeftPanel() { 11 | return this.show; 12 | } 13 | 14 | @action('toggle left panel') 15 | toggleLeftPanel() { 16 | this.show = !this.show; 17 | } 18 | 19 | @action('show left panel') 20 | openLeftPanel() { 21 | this.show = true; 22 | } 23 | 24 | @action('hide left panel') 25 | closeLeftPanel() { 26 | this.show = false; 27 | } 28 | } 29 | 30 | const menuStore = new MenuStore(); 31 | 32 | autorun(() => { 33 | console.log(menuStore.show); 34 | }); 35 | 36 | export default menuStore; 37 | export { MenuStore }; 38 | -------------------------------------------------------------------------------- /client/stores/user-store.js: -------------------------------------------------------------------------------- 1 | import { observable, computed, action, asMap, autorun } from 'mobx'; 2 | 3 | class User { 4 | @observable user = observable.map(); 5 | 6 | constructor(userData = {}, checked = false) { 7 | this.user.merge(userData); 8 | this.user.set("checked", checked); 9 | } 10 | 11 | @computed get userInfo() { 12 | return `${this.user.get("name")} - ${this.user.get("age")}`; 13 | } 14 | 15 | @action toggle() { 16 | this.user.set("checked", !this.user.get("checked")); 17 | } 18 | } 19 | 20 | class UserStore { 21 | @observable users; 22 | 23 | constructor() { 24 | this.users = []; 25 | this.fetch(); 26 | } 27 | 28 | @computed get selectedCount() { 29 | return this.users.filter(userStore => { 30 | return userStore.user.get("checked"); 31 | }).length; 32 | } 33 | 34 | getUsers() { 35 | return this.users; 36 | } 37 | 38 | fetch() { 39 | fetch('/users', { method: 'GET' }) 40 | .then(res => res.json()) 41 | .then(json => this.putUsers(json)); 42 | } 43 | 44 | @action putUsers(users) { 45 | let userArray = []; 46 | users.forEach(user => { 47 | userArray.push(new User(user)); 48 | }); 49 | this.users = userArray; 50 | } 51 | } 52 | const userStore = new UserStore(); 53 | export default userStore; 54 | 55 | autorun(() => { 56 | console.log(userStore.getUsers().toJS()); 57 | }); 58 | 59 | export { UserStore }; 60 | -------------------------------------------------------------------------------- /client/views/app.css: -------------------------------------------------------------------------------- 1 | .app-container { 2 | position: relative; 3 | } 4 | 5 | .page-container { 6 | display: flex; 7 | justify-content: center; 8 | } -------------------------------------------------------------------------------- /client/views/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'mobx-react'; 3 | import { useStrict } from 'mobx'; 4 | 5 | /* components */ 6 | import Menu from '../components/menu'; 7 | 8 | /* stores */ 9 | import leftMenuStore from '../stores/menu-store'; 10 | import userStore from '../stores/user-store'; 11 | 12 | /* styles */ 13 | import './global.css'; 14 | import styles from './app.css'; 15 | 16 | /* use mobx strict mode */ 17 | useStrict(true); 18 | 19 | const stores = { leftMenuStore, userStore }; 20 | 21 | const App = props => ( 22 | 23 | 24 | 25 | 26 | {props.children} 27 | 28 | 29 | 30 | ); 31 | 32 | export default App; 33 | -------------------------------------------------------------------------------- /client/views/global.css: -------------------------------------------------------------------------------- 1 | :global { 2 | html, body { 3 | padding: 0; 4 | margin: 0; 5 | } 6 | } -------------------------------------------------------------------------------- /client/views/home/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import LeftPanelController from '../../components/left-panel-controller'; 3 | import UserList from '../../components/user-list'; 4 | 5 | const Home = () => ( 6 | 7 | Left panel controller 8 | 9 | Users from server 10 | 11 | 12 | ); 13 | export default Home; 14 | 15 | -------------------------------------------------------------------------------- /conf/webpack.base.config.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import Config from 'webpack-config'; 3 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 4 | import autoprefixer from 'autoprefixer'; 5 | import precss from 'precss'; 6 | 7 | export default new Config().merge({ 8 | entry: './client/index.js', 9 | output: { 10 | path: __dirname + '/../public', 11 | }, 12 | module: { 13 | loaders: [ 14 | { 15 | test: /.jsx?$/, 16 | loader: 'babel-loader', 17 | exclude: /node_modules/, 18 | } 19 | ] 20 | }, 21 | plugins: [ 22 | new HtmlWebpackPlugin({ 23 | template: './client/index.html', 24 | inject: "body" 25 | }), 26 | new webpack.LoaderOptionsPlugin({ options: { postcss: [precss, autoprefixer] } }) 27 | ] 28 | }); -------------------------------------------------------------------------------- /conf/webpack.development.config.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import Config from 'webpack-config'; 3 | 4 | export default new Config().extend('conf/webpack.base.config.js').merge({ 5 | entry: [ 6 | 'webpack-hot-middleware/client?reload=true', 7 | 'react-hot-loader/patch', 8 | __dirname + '/../client/index.js' 9 | ], 10 | devtool: 'inline-source-map', 11 | output: { 12 | filename: 'bundle.js' 13 | }, 14 | module: { 15 | loaders: [{ 16 | test: /\.css$/, 17 | use: [ 18 | 'style-loader', 19 | { 20 | loader: 'css-loader', 21 | options: { 22 | modules: true, 23 | importLoaders: 1, 24 | localIdentName: "[local]__[hash:base64:5]", 25 | minimize: false 26 | } 27 | }, 28 | { loader: 'postcss-loader' }, 29 | ] 30 | }] 31 | }, 32 | plugins: [ 33 | new webpack.HotModuleReplacementPlugin() 34 | ] 35 | }); -------------------------------------------------------------------------------- /conf/webpack.production.config.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import Config from 'webpack-config'; 3 | 4 | export default new Config().extend('conf/webpack.base.config.js').merge({ 5 | output: { 6 | filename: 'bundle.min.js' 7 | }, 8 | devtool: 'source-map', 9 | module: { 10 | loaders: [{ 11 | test: /\.css$/, 12 | use: [ 13 | 'style-loader', 14 | { 15 | loader: 'css-loader', 16 | options: { 17 | modules: true, 18 | importLoaders: 1, 19 | localIdentName: "[hash:base64:10]", 20 | minimize: true 21 | } 22 | }, 23 | { loader: 'postcss-loader' }, 24 | ] 25 | }] 26 | }, 27 | plugins: [ 28 | new webpack.optimize.UglifyJsPlugin({ 29 | compress: { 30 | warnings: true 31 | } 32 | }), 33 | new webpack.DefinePlugin({ 34 | "process.env": { 35 | NODE_ENV: JSON.stringify("production") 36 | } 37 | }),] 38 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-mobx", 3 | "version": "1.0.0", 4 | "description": "react.js + mobx.js simple tutorial", 5 | "scripts": { 6 | "build-dev": "set NODE_ENV=development&& babel-node server.js", 7 | "build-prod": "set NODE_ENV=production&& webpack && babel-node server.js" 8 | }, 9 | "keywords": [ 10 | "react", 11 | "webpack", 12 | "mobx", 13 | "tutorial" 14 | ], 15 | "author": "a.ryashentsev", 16 | "license": "ISC", 17 | "dependencies": { 18 | "classnames": "^2.2.5", 19 | "express": "^4.14.1", 20 | "mobx": "^3.1.2", 21 | "mobx-react": "^4.1.1", 22 | "react": "^15.4.2", 23 | "react-dom": "^15.4.2", 24 | "react-router": "^3.0.2" 25 | }, 26 | "devDependencies": { 27 | "autoprefixer": "^6.7.6", 28 | "babel-cli": "^6.23.0", 29 | "babel-core": "^6.22.1", 30 | "babel-loader": "^6.3.2", 31 | "babel-plugin-transform-class-properties": "^6.23.0", 32 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 33 | "babel-preset-es2015": "^6.22.0", 34 | "babel-preset-react": "^6.22.0", 35 | "css-loader": "^0.26.2", 36 | "html-webpack-plugin": "^2.28.0", 37 | "postcss-loader": "^1.3.3", 38 | "precss": "^1.4.0", 39 | "react-hot-loader": "^3.0.0-beta.6", 40 | "style-loader": "^0.13.2", 41 | "webpack": "^2.1.0-beta.22", 42 | "webpack-config": "^7.0.0", 43 | "webpack-dev-middleware": "^1.7.0", 44 | "webpack-hot-middleware": "^2.12.2" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import path from 'path'; 3 | 4 | const PORT = 7700; 5 | const USERS = [ 6 | { id: 1, name: "Alexey", age: 30 }, 7 | { id: 2, name: "Ignat", age: 15 }, 8 | { id: 3, name: "Sergey", age: 26 }, 9 | ]; 10 | const PUBLIC_PATH = __dirname + '/public'; 11 | 12 | const app = express(); 13 | const isDevelopment = process.env.NODE_ENV === 'development'; 14 | 15 | if (isDevelopment) { 16 | const webpack = require('webpack'); 17 | const webpackConfig = require('./webpack.config.babel').default; 18 | const compiler = webpack(webpackConfig); 19 | app.use(require('webpack-dev-middleware')(compiler, { 20 | hot: true, 21 | stats: { 22 | colors: true 23 | } 24 | })); 25 | app.use(require('webpack-hot-middleware')(compiler)); 26 | } else { 27 | app.use(express.static(PUBLIC_PATH)); 28 | } 29 | 30 | app.get("/users", function(req, res) { 31 | res.send(USERS); 32 | }); 33 | 34 | app.all("*", function(req, res) { 35 | res.sendFile(path.resolve(PUBLIC_PATH, 'index.html')); 36 | }); 37 | 38 | app.listen(PORT, function() { 39 | console.log('Listening on port ' + PORT + '...'); 40 | }); -------------------------------------------------------------------------------- /webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | import Config, { environment } from 'webpack-config'; 2 | 3 | environment.setAll({ 4 | env: () => process.env.NODE_ENV 5 | }); 6 | 7 | export default new Config().extend('conf/webpack.[env].config.js'); --------------------------------------------------------------------------------