├── public └── .empty ├── .gitignore ├── app ├── containers │ ├── TEMPLATE.css │ ├── ReadmePage.css │ ├── ChatList.css │ ├── SomePage.jsx │ ├── NotFoundPage.jsx │ ├── HomePage.jsx │ ├── TEMPLATE.jsx │ ├── Application.css │ ├── ReadmePage.jsx │ ├── TodoPage.jsx │ ├── README.md │ ├── Application.jsx │ ├── ChatList.jsx │ ├── ChatPage.jsx │ ├── TodoListPage.jsx │ └── TodoItemPage.jsx ├── elements │ ├── ReactLogo │ │ ├── logo.jpg │ │ ├── logo.png │ │ └── logo.svg │ ├── TEMPLATE.jsx │ ├── ReactLogo.jsx │ └── README.md ├── route-handlers │ ├── ChatPage.jsx │ ├── HomePage.jsx │ ├── SomePage.jsx │ ├── TEMPLATE.jsx │ ├── TodoPage.jsx │ ├── Application.jsx │ ├── ReadmePage.jsx │ ├── NotFoundPage.jsx │ ├── TodoItemPage.jsx │ ├── TodoListPage.jsx │ ├── async.js │ └── README.md ├── simple.html ├── update-helpers │ ├── list.js │ └── react.js ├── mainPrerender.html ├── components │ ├── TEMPLATE.jsx │ ├── TodoListMenu.jsx │ ├── ChatUserView.jsx │ ├── ChatLineView.jsx │ ├── TodoItemEditor.jsx │ ├── MainMenu.jsx │ ├── NewTodoItemEditor.jsx │ ├── TodoItem.jsx │ ├── README.md │ └── NewChatEditor.jsx ├── actions.js ├── store-helpers │ ├── Chat.js │ └── Todo.js ├── .eslintrc ├── mainStoresDescriptions.js ├── mainRoutes.jsx ├── fetch-helpers │ └── rest.js └── mainStores.js ├── lib ├── server-development.js ├── server-production.js ├── mainPrerenderApi.js ├── DB.js ├── dbs.js ├── server.js └── api.js ├── webpack.config.js ├── NOTES ├── importsDiagram.png ├── importsDiagram.txt └── HowStuffWorks.md ├── .eslintrc ├── webpack-dev-server.config.js ├── webpack-hot-dev-server.config.js ├── config ├── mainApp.jsx ├── withTimeout.jsx ├── StoresWrapper.jsx ├── SimpleRenderer.js ├── createStoresForPrerender.jsx ├── loadersByExtension.js ├── mainPrerenderer.jsx ├── .eslintrc ├── Prerenderer.jsx └── renderApplication.jsx ├── webpack-production.config.js ├── Vagrantfile ├── package.json ├── README.md └── make-webpack-config.js /public/.empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /build 3 | .vagrant 4 | -------------------------------------------------------------------------------- /app/containers/TEMPLATE.css: -------------------------------------------------------------------------------- 1 | .this { 2 | background: red; 3 | } -------------------------------------------------------------------------------- /lib/server-development.js: -------------------------------------------------------------------------------- 1 | require("./server")({ 2 | defaultPort: 8080 3 | }); 4 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./make-webpack-config")({ 2 | 3 | }); -------------------------------------------------------------------------------- /NOTES/importsDiagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webpack/react-starter/master/NOTES/importsDiagram.png -------------------------------------------------------------------------------- /app/elements/ReactLogo/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webpack/react-starter/master/app/elements/ReactLogo/logo.jpg -------------------------------------------------------------------------------- /app/elements/ReactLogo/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webpack/react-starter/master/app/elements/ReactLogo/logo.png -------------------------------------------------------------------------------- /app/containers/ReadmePage.css: -------------------------------------------------------------------------------- 1 | .this { 2 | background-color: white; 3 | border: 1px dotted gray; 4 | padding: 1em; 5 | } 6 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "rules": { 6 | "strict": 0, 7 | "curly": 0 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/server-production.js: -------------------------------------------------------------------------------- 1 | require("./server")({ 2 | prerender: true, 3 | separateStylesheet: true, 4 | defaultPort: 8080 5 | }); 6 | -------------------------------------------------------------------------------- /app/containers/ChatList.css: -------------------------------------------------------------------------------- 1 | .this { 2 | height: 200px; 3 | overflow-y: scroll; 4 | background: white; 5 | border: 1px solid black; 6 | } 7 | -------------------------------------------------------------------------------- /webpack-dev-server.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./make-webpack-config")({ 2 | devServer: true, 3 | devtool: "eval", 4 | debug: true 5 | }); 6 | -------------------------------------------------------------------------------- /app/route-handlers/ChatPage.jsx: -------------------------------------------------------------------------------- 1 | import { createContainer } from "items-store"; 2 | import ChatPage from "containers/ChatPage"; 3 | 4 | export default createContainer(ChatPage); 5 | -------------------------------------------------------------------------------- /app/route-handlers/HomePage.jsx: -------------------------------------------------------------------------------- 1 | import { createContainer } from "items-store"; 2 | import HomePage from "containers/HomePage"; 3 | 4 | export default createContainer(HomePage); 5 | -------------------------------------------------------------------------------- /app/route-handlers/SomePage.jsx: -------------------------------------------------------------------------------- 1 | import { createContainer } from "items-store"; 2 | import SomePage from "containers/SomePage"; 3 | 4 | export default createContainer(SomePage); 5 | -------------------------------------------------------------------------------- /app/route-handlers/TEMPLATE.jsx: -------------------------------------------------------------------------------- 1 | import { createContainer } from "items-store"; 2 | import TEMPLATE from "containers/TEMPLATE"; 3 | 4 | export default createContainer(TEMPLATE); 5 | -------------------------------------------------------------------------------- /app/route-handlers/TodoPage.jsx: -------------------------------------------------------------------------------- 1 | import { createContainer } from "items-store"; 2 | import TodoPage from "containers/TodoPage"; 3 | 4 | export default createContainer(TodoPage); 5 | -------------------------------------------------------------------------------- /webpack-hot-dev-server.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./make-webpack-config")({ 2 | devServer: true, 3 | hotComponents: true, 4 | devtool: "eval", 5 | debug: true 6 | }); 7 | -------------------------------------------------------------------------------- /app/route-handlers/Application.jsx: -------------------------------------------------------------------------------- 1 | import { createContainer } from "items-store"; 2 | import Application from "containers/Application"; 3 | 4 | export default createContainer(Application); 5 | -------------------------------------------------------------------------------- /app/route-handlers/ReadmePage.jsx: -------------------------------------------------------------------------------- 1 | import { createContainer } from "items-store"; 2 | import ReadmePage from "containers/ReadmePage"; 3 | 4 | export default createContainer(ReadmePage); 5 | -------------------------------------------------------------------------------- /app/route-handlers/NotFoundPage.jsx: -------------------------------------------------------------------------------- 1 | import { createContainer } from "items-store"; 2 | import NotFoundPage from "containers/NotFoundPage"; 3 | 4 | export default createContainer(NotFoundPage); 5 | -------------------------------------------------------------------------------- /app/route-handlers/TodoItemPage.jsx: -------------------------------------------------------------------------------- 1 | import { createContainer } from "items-store"; 2 | import TodoItemPage from "containers/TodoItemPage"; 3 | 4 | export default createContainer(TodoItemPage); 5 | -------------------------------------------------------------------------------- /app/route-handlers/TodoListPage.jsx: -------------------------------------------------------------------------------- 1 | import { createContainer } from "items-store"; 2 | import TodoListPage from "containers/TodoListPage"; 3 | 4 | export default createContainer(TodoListPage); 5 | -------------------------------------------------------------------------------- /app/simple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | -------------------------------------------------------------------------------- /app/update-helpers/list.js: -------------------------------------------------------------------------------- 1 | export function mergeUpdates(a, b) { 2 | return a.concat(b); 3 | } 4 | 5 | export function applyUpdate(oldData, update) { 6 | return oldData.concat(update.map((u) => ({ sending: true, ...u }))); 7 | } 8 | -------------------------------------------------------------------------------- /config/mainApp.jsx: -------------------------------------------------------------------------------- 1 | import routes from "../app/mainRoutes"; 2 | import stores from "../app/mainStores"; 3 | import renderApplication from "./renderApplication"; 4 | 5 | renderApplication(routes, stores, { 6 | timeout: 600 7 | }); 8 | -------------------------------------------------------------------------------- /app/route-handlers/async.js: -------------------------------------------------------------------------------- 1 | // Specify which route handlers should be loaded async. 2 | // the configuration requires this file and adds the 3 | // react-proxy-loader for specified files 4 | module.exports = ["SomePage", "ReadmePage"]; 5 | -------------------------------------------------------------------------------- /app/mainPrerender.html: -------------------------------------------------------------------------------- 1 |
CONTENT
2 | -------------------------------------------------------------------------------- /config/withTimeout.jsx: -------------------------------------------------------------------------------- 1 | export default function(fn, timeout, callback) { 2 | var timedOut = false; 3 | var to = setTimeout(function() { 4 | timedOut = true; 5 | callback(); 6 | }, timeout); 7 | fn(function() { 8 | clearTimeout(to); 9 | if(!timedOut) callback(); 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /app/containers/SomePage.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default class SomePage extends React.Component { 4 | static getProps() { 5 | return {}; 6 | } 7 | render() { 8 | return
9 |

SomePage

10 |

This is just some page... (loaded on demand)

11 |
; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/containers/NotFoundPage.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default class NotFoundPage extends React.Component { 4 | static getProps() { 5 | return {}; 6 | } 7 | render() { 8 | return
9 |

Not found

10 |

The page you requested was not found.

11 |
; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /webpack-production.config.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | require("./make-webpack-config")({ 3 | // commonsChunk: true, 4 | longTermCaching: true, 5 | separateStylesheet: true, 6 | minimize: true 7 | // devtool: "source-map" 8 | }), 9 | require("./make-webpack-config")({ 10 | prerender: true, 11 | minimize: true 12 | }) 13 | ]; -------------------------------------------------------------------------------- /app/containers/HomePage.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default class HomePage extends React.Component { 4 | static getProps() { 5 | return {}; 6 | } 7 | render() { 8 | return
9 |

Homepage

10 |

This is the homepage.

11 |

Try to go to a todo list page.

12 |
; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/containers/TEMPLATE.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./TEMPLATE.css"; 3 | 4 | export default class TEMPLATE extends React.Component { 5 | static getProps(stores, params) { 6 | return { 7 | // Fetch data here 8 | }; 9 | } 10 | render() { 11 | return
12 | This is a Container. 13 |
; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/containers/Application.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #eee; 3 | } 4 | 5 | .this { 6 | opacity: 1; 7 | } 8 | 9 | .this.loading { 10 | opacity: 0.2; 11 | transition: opacity 600ms; 12 | } 13 | 14 | .this .loadingElement { 15 | display: none; 16 | } 17 | 18 | .this.loading .loadingElement { 19 | display: inherit; 20 | } 21 | 22 | .loadingElement { 23 | float: right; 24 | } 25 | -------------------------------------------------------------------------------- /config/StoresWrapper.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default class StoresWrapper { 4 | getChildContext() { 5 | return { 6 | stores: this.props.stores 7 | }; 8 | } 9 | 10 | render() { 11 | var Application = this.props.Component; 12 | return ; 13 | } 14 | }; 15 | 16 | StoresWrapper.childContextTypes = { 17 | stores: React.PropTypes.object 18 | }; -------------------------------------------------------------------------------- /app/containers/ReadmePage.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./ReadmePage.css"; 3 | 4 | export default class ReadmePage extends React.Component { 5 | static getProps() { 6 | return {}; 7 | } 8 | render() { 9 | var readme = { __html: require("./../../README.md") }; 10 | return
; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/elements/TEMPLATE.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default class TEMPLATE extends React.Component { 4 | render() { 5 | var { aaa, bbb, ccc } = this.props; 6 | return
7 | This is an Element. 8 |
; 9 | } 10 | } 11 | TEMPLATE.propTypes = { 12 | aaa: React.PropTypes.object.isRequired, 13 | bbb: React.PropTypes.string.isRequired, 14 | ccc: React.PropTypes.bool 15 | }; 16 | -------------------------------------------------------------------------------- /app/components/TEMPLATE.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default class TEMPLATE extends React.Component { 4 | render() { 5 | var { aaa, bbb, ccc } = this.props; 6 | return
7 | This is a Component. 8 |
; 9 | } 10 | } 11 | TEMPLATE.propTypes = { 12 | aaa: React.PropTypes.object.isRequired, 13 | bbb: React.PropTypes.string.isRequired, 14 | ccc: React.PropTypes.bool 15 | }; 16 | -------------------------------------------------------------------------------- /app/components/TodoListMenu.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router"; 3 | 4 | export default class ListMenu extends React.Component { 5 | render() { 6 | return
7 |

Lists:

8 | 12 |
; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /config/SimpleRenderer.js: -------------------------------------------------------------------------------- 1 | var fs = require("fs"); 2 | var path = require("path"); 3 | var html = fs.readFileSync(path.resolve(__dirname, "../app/simple.html"), "utf-8"); 4 | 5 | function SimpleRenderer(options) { 6 | this.html = html.replace("SCRIPT_URL", options.scriptUrl); 7 | } 8 | 9 | SimpleRenderer.prototype.render = function(_path, _readItems, callback) { 10 | callback(null, this.html); 11 | }; 12 | 13 | module.exports = SimpleRenderer; 14 | -------------------------------------------------------------------------------- /app/components/ChatUserView.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default class ChatUserView extends React.Component { 4 | render() { 5 | var { name, messages } = this.props; 6 | return messages ? 7 | 8 | {name} ({messages}) 9 | : 10 | 11 | {name} 12 | ; 13 | } 14 | } 15 | ChatUserView.propTypes = { 16 | name: React.PropTypes.string.isRequired, 17 | messages: React.PropTypes.number 18 | }; 19 | -------------------------------------------------------------------------------- /app/elements/ReactLogo.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default class ReactLogo extends React.Component { 4 | render() { 5 | var { type } = this.props; 6 | var url = { 7 | "svg": require("./ReactLogo/logo.svg"), 8 | "png": require("./ReactLogo/logo.png"), 9 | "jpg": require("./ReactLogo/logo.jpg") 10 | }[type]; 11 | return ; 12 | } 13 | } 14 | ReactLogo.propTypes = { 15 | type: React.PropTypes.oneOf(["svg", "png", "jpg"]) 16 | }; 17 | -------------------------------------------------------------------------------- /app/actions.js: -------------------------------------------------------------------------------- 1 | import { Actions } from "items-store"; 2 | 3 | // All the actions of the application 4 | 5 | export var Todo = Actions.create([ 6 | "update", // (id, update): Update a todo item 7 | "add", // (list, item): Add an todo item to a todo list 8 | "fetch", // (): Fetch all todo data 9 | "fetchItem" // (id): Fetch todo item data 10 | ]); 11 | 12 | export var Chat = Actions.create([ 13 | "send", // (room, message): Send a chat message to a room 14 | "fetch" // (): Fetch all chat data 15 | ]); 16 | -------------------------------------------------------------------------------- /app/store-helpers/Chat.js: -------------------------------------------------------------------------------- 1 | export function fetchChatUser(stores, name) { 2 | return { 3 | ...stores.ChatUser.getItem(name), 4 | name: name 5 | }; 6 | } 7 | 8 | export function fetchChatMessage(stores, data) { 9 | return { 10 | ...data, 11 | user: data.user && fetchChatUser(stores, data.user) 12 | }; 13 | } 14 | 15 | export function fetchChatMessages(stores, room) { 16 | var messages = stores.ChatRoom.getItem(room); 17 | if(!messages) return messages; 18 | return messages.map((msg) => fetchChatMessage(stores, msg)); 19 | } 20 | -------------------------------------------------------------------------------- /app/components/ChatLineView.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ChatUserView from "components/ChatUserView"; 3 | 4 | export default class ChatLineView extends React.Component { 5 | render() { 6 | var { user, message, sending } = this.props; 7 | return
8 | : {message} 9 | {sending && (sending...)} 10 |
; 11 | } 12 | } 13 | ChatLineView.propTypes = { 14 | user: React.PropTypes.object.isRequired, 15 | message: React.PropTypes.string.isRequired, 16 | sending: React.PropTypes.bool 17 | }; 18 | -------------------------------------------------------------------------------- /config/createStoresForPrerender.jsx: -------------------------------------------------------------------------------- 1 | import { ItemsStore } from "items-store"; 2 | 3 | // create stores for prerending 4 | // readItems contains async methods for fetching the data from database 5 | export default function createStoresForPrerender(storesDescriptions, readItems) { 6 | return Object.keys(storesDescriptions).reduce(function(obj, name) { 7 | obj[name] = new ItemsStore(Object.assign({ 8 | readSingleItem: readItems[name], 9 | queueRequest: function(fn) { fn(); } 10 | }, storesDescriptions[name])); 11 | return obj; 12 | }, {}); 13 | } 14 | -------------------------------------------------------------------------------- /app/containers/TodoPage.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ListMenu from "components/TodoListMenu"; 3 | import { RouteHandler } from "react-router"; 4 | import { Todo } from "actions"; 5 | 6 | export default class TodoPage extends React.Component { 7 | static getProps() { 8 | return {}; 9 | } 10 | render() { 11 | return
12 |

TodoPage

13 | 14 | 15 | 16 |
; 17 | } 18 | 19 | fetch() { 20 | Todo.fetch(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/route-handlers/README.md: -------------------------------------------------------------------------------- 1 | # route-handlers 2 | 3 | > These handlers are bound to Routes in `../mainRoutes`. 4 | 5 | A route handler should do nothing, but only bind the handler to a technology. 6 | 7 | A component is allowed to import the following stuff: 8 | * `containers/*` 9 | 10 | Don't import: 11 | * `element/*` 12 | * `components/*` 13 | * `store-helpers/*` 14 | * `route-handlers/*` 15 | * `actions/*` 16 | 17 | Currently all route handlers are implemented by `items-store` containers. 18 | 19 | The technology can be easily replaced on per route handler level. This allow easy migration to a future technology. 20 | -------------------------------------------------------------------------------- /app/store-helpers/Todo.js: -------------------------------------------------------------------------------- 1 | export function fetchTodoItem(stores, item) { 2 | return { 3 | ...stores.TodoItem.getItem(item), 4 | ...stores.TodoItem.getItemInfo(item), 5 | id: item 6 | }; 7 | } 8 | 9 | export function fetchTodoList(stores, list) { 10 | var data = { 11 | items: stores.TodoList.getItem(list), 12 | ...stores.TodoList.getItemInfo(list), 13 | id: list 14 | }; 15 | var newId = 0; 16 | if(data.items) { 17 | data.items = data.items.map((item) => { 18 | if(typeof item === "object") return { 19 | ...item, 20 | sending: true, 21 | id: "new" + (newId++) 22 | }; 23 | return fetchTodoItem(stores, item); 24 | }); 25 | } 26 | return data; 27 | } 28 | -------------------------------------------------------------------------------- /app/components/TodoItemEditor.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default class TodoItemEditor extends React.Component { 4 | render() { 5 | var { text, done, onUpdate } = this.props; 6 | return 7 | { 8 | onUpdate({ 9 | done: event.target.checked 10 | }); 11 | }} /> 12 | { 13 | onUpdate({ 14 | text: event.target.value 15 | }); 16 | }} /> 17 | ; 18 | } 19 | } 20 | TodoItemEditor.propTypes = { 21 | text: React.PropTypes.string.isRequired, 22 | done: React.PropTypes.bool.isRequired, 23 | onUpdate: React.PropTypes.func.isRequired 24 | }; 25 | -------------------------------------------------------------------------------- /lib/mainPrerenderApi.js: -------------------------------------------------------------------------------- 1 | var dbs = require("./dbs"); 2 | var todosDb = dbs.todos; 3 | var listsDb = dbs.lists; 4 | var chatRoomsDb = dbs.chatRooms; 5 | var chatUsersDb = dbs.chatUsers; 6 | 7 | module.exports = function(/* req */) { 8 | var api = {}; 9 | 10 | api.TodoItem = function(item, callback) { 11 | callback(null, todosDb.get(item.id, {})); 12 | }; 13 | 14 | api.TodoList = function(item, callback) { 15 | callback(null, listsDb.get(item.id, [])); 16 | }; 17 | 18 | api.ChatRoom = function(item, callback) { 19 | callback(null, chatRoomsDb.get(item.id, [])); 20 | }; 21 | 22 | api.ChatUser = function(item, callback) { 23 | callback(null, chatUsersDb.get(item.id)); 24 | }; 25 | 26 | return api; 27 | }; 28 | -------------------------------------------------------------------------------- /app/containers/README.md: -------------------------------------------------------------------------------- 1 | # containers 2 | 3 | > These containers are used by `../route-handlers` or by other containers. 4 | 5 | A container does the data fetching for a small part of the application. It can represent a page or a fragment of a page. 6 | 7 | A container is allowed to import the following stuff: 8 | * `containers/*` 9 | * `components/*` 10 | * `store-helpers/*` 11 | * `actions` 12 | 13 | Don't import: 14 | * `elements/*` -> use a component instead 15 | * `route-handlers/*` 16 | 17 | Every container exposes a `getProps(stores, params)` method which can be used by the caller to fetch the `props` for the container. It's expected that `getProps` is called within dependency-tracker context (This is done by `items-store` `createContainer`). 18 | -------------------------------------------------------------------------------- /app/components/MainMenu.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router"; 3 | import ReactLogo from "elements/ReactLogo"; 4 | 5 | export default class MainMenu extends React.Component { 6 | render() { 7 | return
8 | 9 |

MainMenu:

10 |
    11 |
  • The home page.
  • 12 |
  • Do something on the todo page.
  • 13 |
  • Switch to some page.
  • 14 |
  • Open the chat demo: Chat.
  • 15 |
  • Open the page that shows README.md.
  • 16 |
17 |
; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/components/NewTodoItemEditor.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default class NewTodoItemEditor extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = { 7 | text: "" 8 | }; 9 | } 10 | render() { 11 | var { onAdd } = this.props; 12 | var { text } = this.state; 13 | return
14 | { 15 | this.setState({ 16 | text: event.target.value 17 | }); 18 | }} /> 19 | 26 |
; 27 | } 28 | } 29 | NewTodoItemEditor.propTypes = { 30 | onAdd: React.PropTypes.func.isRequired 31 | }; 32 | -------------------------------------------------------------------------------- /lib/DB.js: -------------------------------------------------------------------------------- 1 | var update = require("react/lib/update"); 2 | 3 | function DB(initialData) { 4 | this.data = initialData || {}; 5 | } 6 | 7 | module.exports = DB; 8 | 9 | DB.prototype.get = function(id, createDefaultData) { 10 | var d = this.data["_" + id]; 11 | if(!d) { 12 | this.data["_" + id] = createDefaultData; 13 | return createDefaultData; 14 | } 15 | return d; 16 | }; 17 | 18 | DB.prototype.update = function(id, upd) { 19 | var res = this.data["_" + id] = update(this.data["_" + id], upd); 20 | return res; 21 | }; 22 | 23 | DB.prototype.set = function(id, data) { 24 | var res = this.data["_" + id] = data; 25 | return res; 26 | }; 27 | 28 | DB.prototype.getIds = function() { 29 | return Object.keys(this.data).map(function(key) { 30 | return key.substr(1); 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /app/containers/Application.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { RouteHandler } from "react-router"; 3 | import MainMenu from "components/MainMenu"; 4 | 5 | import styles from "./Application.css"; 6 | 7 | export default class Application extends React.Component { 8 | static getProps(stores, params) { 9 | var transition = stores.Router.getItem("transition"); 10 | return { 11 | loading: !!transition 12 | }; 13 | } 14 | render() { 15 | var { loading } = this.props; 16 | return
17 |
loading...
18 |

react-starter

19 | 20 | 21 |
; 22 | } 23 | } 24 | 25 | Application.contextTypes = { 26 | stores: React.PropTypes.object 27 | }; 28 | -------------------------------------------------------------------------------- /config/loadersByExtension.js: -------------------------------------------------------------------------------- 1 | function extsToRegExp(exts) { 2 | return new RegExp("\\.(" + exts.map(function(ext) { 3 | return ext.replace(/\./g, "\\."); 4 | }).join("|") + ")(\\?.*)?$"); 5 | } 6 | 7 | module.exports = function loadersByExtension(obj) { 8 | var loaders = []; 9 | Object.keys(obj).forEach(function(key) { 10 | var exts = key.split("|"); 11 | var value = obj[key]; 12 | var entry = { 13 | extensions: exts, 14 | test: extsToRegExp(exts) 15 | }; 16 | if(Array.isArray(value)) { 17 | entry.loaders = value; 18 | } else if(typeof value === "string") { 19 | entry.loader = value; 20 | } else { 21 | Object.keys(value).forEach(function(valueKey) { 22 | entry[valueKey] = value[valueKey]; 23 | }); 24 | } 25 | loaders.push(entry); 26 | }); 27 | return loaders; 28 | }; 29 | -------------------------------------------------------------------------------- /app/components/TodoItem.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router"; 3 | 4 | export default class TodoItem extends React.Component { 5 | render() { 6 | var { id, text, done, sending, onUpdate } = this.props; 7 | return
8 | { 9 | if(sending) return; 10 | onUpdate({ 11 | done: event.target.checked 12 | }); 13 | }}/> 14 | { sending ? 15 | {text} : 16 | {text} 17 | } 18 |
; 19 | } 20 | } 21 | TodoItem.propTypes = { 22 | id: React.PropTypes.string.isRequired, 23 | text: React.PropTypes.string.isRequired, 24 | done: React.PropTypes.bool, 25 | sending: React.PropTypes.bool, 26 | onUpdate: React.PropTypes.func.isRequired 27 | }; 28 | -------------------------------------------------------------------------------- /app/components/README.md: -------------------------------------------------------------------------------- 1 | # components 2 | 3 | > These components are used by `../containers` or by other components. 4 | 5 | A component doesn't do any data fetching and expects data pass via `props`. It represents a reusesable component that can be used in different contexts. A component can have state. A component expects high-level data. 6 | 7 | A component can have styles for layouting. It should not have styles for visual stuff. 8 | 9 | A component is allowed to import the following stuff: 10 | * `components/*` 11 | * `elements/*` 12 | 13 | Don't import: 14 | * `store-helpers/*` -> do it from the container instead and pass data via `props`. 15 | * `actions` -> pass a callback via `props` and call the action in the container instead. 16 | * external styles and components -> wrap the style or component in an element 17 | * `route-handlers/*` 18 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | _script = <` to `app/prerender.html`. 136 | 4. Modify the server code to require, serve and prerender the other entry point. 137 | 5. Restart compilation. 138 | 139 | ### Switch devtool to SourceMaps 140 | 141 | Change `devtool` property in `webpack-dev-server.config.js` and `webpack-hot-dev-server.config.js` to `"source-map"` (better module names) or `"eval-source-map"` (faster compilation). 142 | 143 | SourceMaps have a performance impact on compilation. 144 | 145 | ### Enable SourceMaps in production 146 | 147 | 1. Uncomment the `devtool` line in `webpack-production.config.js`. 148 | 2. Make sure that the folder `build\public\debugging` is access controlled, i. e. by password. 149 | 150 | SourceMaps have a performance impact on compilation. 151 | 152 | SourceMaps contains your unminimized source code, so you need to restrict access to `build\public\debugging`. 153 | 154 | ### Coffeescript 155 | 156 | Coffeescript is not installed/enabled by default to not disturb non-coffee developer, but you can install it easily: 157 | 158 | 1. `npm install coffee-redux-loader --save` 159 | 2. In `make-webpack-config.js` add `".coffee"` to the `var extensions = ...` line. 160 | 161 | 162 | ## License 163 | 164 | Copyright (c) 2012-2015 Tobias Koppers [![Gittip donate button](http://img.shields.io/gittip/sokra.png)](https://www.gittip.com/sokra/) 165 | 166 | MIT (http://www.opensource.org/licenses/mit-license.php) 167 | -------------------------------------------------------------------------------- /make-webpack-config.js: -------------------------------------------------------------------------------- 1 | var path = require("path"); 2 | var webpack = require("webpack"); 3 | var ExtractTextPlugin = require("extract-text-webpack-plugin"); 4 | var StatsPlugin = require("stats-webpack-plugin"); 5 | var loadersByExtension = require("./config/loadersByExtension"); 6 | 7 | module.exports = function(options) { 8 | var entry = { 9 | main: options.prerender ? "./config/mainPrerenderer" : "./config/mainApp" 10 | // second: options.prerender ? "./config/secondPrerenderer" : "./config/secondApp" 11 | }; 12 | var loaders = { 13 | "jsx": options.hotComponents ? ["react-hot-loader", "babel-loader?stage=0"] : "babel-loader?stage=0", 14 | "js": { 15 | loader: "babel-loader?stage=0", 16 | include: path.join(__dirname, "app") 17 | }, 18 | "json": "json-loader", 19 | "coffee": "coffee-redux-loader", 20 | "json5": "json5-loader", 21 | "txt": "raw-loader", 22 | "png|jpg|jpeg|gif|svg": "url-loader?limit=10000", 23 | "woff|woff2": "url-loader?limit=100000", 24 | "ttf|eot": "file-loader", 25 | "wav|mp3": "file-loader", 26 | "html": "html-loader", 27 | "md|markdown": ["html-loader", "markdown-loader"] 28 | }; 29 | var cssLoader = options.minimize ? "css-loader?module" : "css-loader?module&localIdentName=[path][name]---[local]---[hash:base64:5]"; 30 | var stylesheetLoaders = { 31 | "css": cssLoader, 32 | "less": [cssLoader, "less-loader"], 33 | "styl": [cssLoader, "stylus-loader"], 34 | "scss|sass": [cssLoader, "sass-loader"] 35 | }; 36 | var additionalLoaders = [ 37 | // { test: /some-reg-exp$/, loader: "any-loader" } 38 | ]; 39 | var alias = { 40 | 41 | }; 42 | var aliasLoader = { 43 | 44 | }; 45 | var externals = [ 46 | 47 | ]; 48 | var modulesDirectories = ["web_modules", "node_modules"]; 49 | var extensions = ["", ".web.js", ".js", ".jsx"]; 50 | var root = path.join(__dirname, "app"); 51 | var publicPath = options.devServer ? 52 | "http://localhost:2992/_assets/" : 53 | "/_assets/"; 54 | var output = { 55 | path: path.join(__dirname, "build", options.prerender ? "prerender" : "public"), 56 | publicPath: publicPath, 57 | filename: "[name].js" + (options.longTermCaching && !options.prerender ? "?[chunkhash]" : ""), 58 | chunkFilename: (options.devServer ? "[id].js" : "[name].js") + (options.longTermCaching && !options.prerender ? "?[chunkhash]" : ""), 59 | sourceMapFilename: "debugging/[file].map", 60 | libraryTarget: options.prerender ? "commonjs2" : undefined, 61 | pathinfo: options.debug || options.prerender 62 | }; 63 | var excludeFromStats = [ 64 | /node_modules[\\\/]react(-router)?[\\\/]/, 65 | /node_modules[\\\/]items-store[\\\/]/ 66 | ]; 67 | var plugins = [ 68 | new webpack.PrefetchPlugin("react"), 69 | new webpack.PrefetchPlugin("react/lib/ReactComponentBrowserEnvironment") 70 | ]; 71 | if(options.prerender) { 72 | plugins.push(new StatsPlugin(path.join(__dirname, "build", "stats.prerender.json"), { 73 | chunkModules: true, 74 | exclude: excludeFromStats 75 | })); 76 | aliasLoader["react-proxy$"] = "react-proxy/unavailable"; 77 | aliasLoader["react-proxy-loader$"] = "react-proxy-loader/unavailable"; 78 | externals.push( 79 | /^react(\/.*)?$/, 80 | /^reflux(\/.*)?$/, 81 | "superagent", 82 | "async" 83 | ); 84 | plugins.push(new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 })); 85 | } else { 86 | plugins.push(new StatsPlugin(path.join(__dirname, "build", "stats.json"), { 87 | chunkModules: true, 88 | exclude: excludeFromStats 89 | })); 90 | } 91 | if(options.commonsChunk) { 92 | plugins.push(new webpack.optimize.CommonsChunkPlugin("commons", "commons.js" + (options.longTermCaching && !options.prerender ? "?[chunkhash]" : ""))); 93 | } 94 | var asyncLoader = { 95 | test: require("./app/route-handlers/async").map(function(name) { 96 | return path.join(__dirname, "app", "route-handlers", name); 97 | }), 98 | loader: options.prerender ? "react-proxy-loader/unavailable" : "react-proxy-loader" 99 | }; 100 | 101 | 102 | 103 | Object.keys(stylesheetLoaders).forEach(function(ext) { 104 | var stylesheetLoader = stylesheetLoaders[ext]; 105 | if(Array.isArray(stylesheetLoader)) stylesheetLoader = stylesheetLoader.join("!"); 106 | if(options.prerender) { 107 | stylesheetLoaders[ext] = stylesheetLoader.replace(/^css-loader/, "css-loader/locals"); 108 | } else if(options.separateStylesheet) { 109 | stylesheetLoaders[ext] = ExtractTextPlugin.extract("style-loader", stylesheetLoader); 110 | } else { 111 | stylesheetLoaders[ext] = "style-loader!" + stylesheetLoader; 112 | } 113 | }); 114 | if(options.separateStylesheet && !options.prerender) { 115 | plugins.push(new ExtractTextPlugin("[name].css" + (options.longTermCaching ? "?[contenthash]" : ""))); 116 | } 117 | if(options.minimize && !options.prerender) { 118 | plugins.push( 119 | new webpack.optimize.UglifyJsPlugin({ 120 | compressor: { 121 | warnings: false 122 | } 123 | }), 124 | new webpack.optimize.DedupePlugin() 125 | ); 126 | } 127 | if(options.minimize) { 128 | plugins.push( 129 | new webpack.DefinePlugin({ 130 | "process.env": { 131 | NODE_ENV: JSON.stringify("production") 132 | } 133 | }), 134 | new webpack.NoErrorsPlugin() 135 | ); 136 | } 137 | 138 | return { 139 | entry: entry, 140 | output: output, 141 | target: options.prerender ? "node" : "web", 142 | module: { 143 | loaders: [asyncLoader].concat(loadersByExtension(loaders)).concat(loadersByExtension(stylesheetLoaders)).concat(additionalLoaders) 144 | }, 145 | devtool: options.devtool, 146 | debug: options.debug, 147 | resolveLoader: { 148 | root: path.join(__dirname, "node_modules"), 149 | alias: aliasLoader 150 | }, 151 | externals: externals, 152 | resolve: { 153 | root: root, 154 | modulesDirectories: modulesDirectories, 155 | extensions: extensions, 156 | alias: alias 157 | }, 158 | plugins: plugins, 159 | devServer: { 160 | stats: { 161 | cached: false, 162 | exclude: excludeFromStats 163 | } 164 | } 165 | }; 166 | }; 167 | -------------------------------------------------------------------------------- /NOTES/HowStuffWorks.md: -------------------------------------------------------------------------------- 1 | ![importsDiagram](importsDiagram.png) 2 | 3 | *An arrow means: `imports`* 4 | 5 | # How Stuff Works 6 | This documentation is for grasping the overall design. No details by intention. To get up and running quickly see `README.md`. 7 | 8 | At first sight, it may not be immediately clear why/how certain things are called/triggered. 9 | The below explanation points you in the right direction, so you can research further. If you want more details or documentation, see the relevant package. 10 | Some (inconspicuously) used/relevant node modules are explicitly mentioned with package name. 11 | 12 | ## Table of contents 13 | 14 | * [How the app is served.](#how-the-app-is-served) 15 | * [development mode](#development-mode) 16 | * [production mode](#production-mode) 17 | * [How the page updates while you are programming.](#how-the-page-updates-while-you-are-programming) 18 | * [page reloading](#page-reloading) 19 | * [hot reloading](#hot-reloading) 20 | * [How the routes work.](#how-the-routes-work) 21 | * [How the server JSON API works.](#how-the-server-json-api-works) 22 | * [How the stores work.](#how-the-stores-work) 23 | * [Stores setup](#stores-setup) 24 | * [Stores in use](#stores-in-use) 25 | * [How the actions work.](#how-the-actions-work) 26 | * [How the server DB works.](#how-the-server-db-works) 27 | * [How the 'Random fail!' works.](#how-the-random-fail-works) 28 | * [How the build works.](#how-the-build-works) 29 | * [Q&A Why](#qa-why) 30 | 31 | ***** 32 | 33 | ## How the app is served. 34 | A JS webserver is included in the form of `lib/server.js`. It can be run in two modes: 35 | 36 | ### development mode 37 | Run the server using `npm run start-dev`. 38 | It will use `lib/server-development.js` which will use `lib/server.js` which will use `config/simple.js` which will use `app/simple.html`. A tiny HTML file is loaded, the JS is downloaded + executed + rendered, and ultimately output is shown. 39 | 40 | ### production mode 41 | In this mode, the React HTML output is (pre)rendered (and populated) on the server (a.k.a. isomorphic). 42 | 43 | Run the server using `npm run start`. 44 | It will use `lib/server-production.js` which will use `lib/server.js` which will use `config/mainPrerenderer.jsx` which will use `app/mainPrerender.html`. A big HTML file is loaded (and output is shown immediately), the JS is downloaded + executed + rendered, and output is updated. 45 | 46 | This server side render (prerender) is possible because the React JS can be executed on the server. 47 | In your browser the main `React.render(, document.getElementById("content"))` call (in `config/mainApp.jsx`) outputs to the browser DOM. 48 | But when pre-rendering, the main `React.renderToString()` call (in `app/prerender.jsx`) outputs to a string, which is inserted into the HTML (including React component states) as content. 49 | The browser now can show the HTML instantly in the DOM, but proceeds to run the React JS that resumes the usual DOM mutations. 50 | 51 | Note: Routes that are asynchronously loaded can not be pre-rendered. 52 | 53 | 54 | ## How the page updates while you are programming. 55 | After running the app webserver in development mode (see above) you'd have to manually reload the page after changing a JSX file. 56 | It is possible to automatically reload _or_ update the page to reflect your changes: 57 | 58 | ### page reloading 59 | Ensure that you are running the app webserver in development mode. 60 | 61 | Then run another server using `npm run dev-server`. 62 | It will rebuild while watching for file changes, and it will trigger your page to reload afterwards. 63 | 64 | ### hot reloading 65 | Ensure that you are running the app webserver in development mode. 66 | 67 | Then run another server using `npm run hot-dev-server`. 68 | It will rebuild while watching for file changes, and it will update the currently shown and affected component(s) while keeping their state. 69 | Note this is experimental, and in some cases you'll need to refresh manually. 70 | 71 | 72 | ## How the routes work. 73 | In React after opening the app and going to some page, there is no actual HTML loaded from the server. React app just replaces a component that acts as a page, including any child components. 74 | But you'd like to have the URL reflect this, and allow user to use browser history (back/forward). A router takes care of these things. (package react-router) 75 | 76 | In this case, the root of your app is not the Application React component. 77 | This starts at `lib/server.js` which will use `config/mainApp.jsx` which instantiates the router and ultimately uses `app/mainRoutes.jsx` to load routes. 78 | You'll find that all pages are subroutes within the `app` route, which instantiates `app/Application/index.jsx`, which contains a `` component that inserts subroute output. 79 | 80 | 81 | ## How the server JSON API works. 82 | The `lib/server.js` serves the application, but it also serves the API URLs (`lib/api.js`) that the stores talk with. 83 | It initializes two databases (todolist and todoitem) once, and then continues to listen for GET/POST requests on specific URLs. 84 | 85 | 86 | ## How the stores work. 87 | As in Flux; the Stores affect the React components. (package `items-store`) And the stores talk to the JSON API (`app/fetch-helpers/rest.js`). 88 | See [Q&A Why](#qa-why) 89 | 90 | ### Stores setup 91 | The stores are constructed as such: startpoint is `config/mainApp.jsx` which will use `app/mainStores.js`. This then: 92 | 93 | - defines routines to handle JSON API read/writes (package `superagent`), and 94 | - sets up a queue that only allows one REST request at a time, and aggregates subsequent changes. 95 | These are then sent as one request.. 96 | - and ultimately constructs the two respective `ItemStore` objects (based on `app/mainStoresDescriptions.js`) to which it assigns these routines, queue, and the initial data. This initial data may have been inserted via `app/prerender.html` and `app/`. 97 | - These are then exported back to `config/mainApp.jsx`, along with a store named `Router`. 98 | 99 | The store `Router` just holds a `transition` value which is later read in `app/containers/Application.jsx` to show "loading..." in UI. 100 | The stores `TodoList` and `TodoItem` only have minor differences. 101 | 102 | If you wonder where the default data is inserted, see (#how-the-server-db-works). 103 | 104 | ### Stores in use 105 | 106 | todo :question: this section needs review and should be extended. 107 | 108 | Note: Components should not access the stores except for reading in `getProps()`. Everything else should be done with actions. 109 | 110 | 111 | ## How the actions work. 112 | The Actions affect the stores. (package items-store) 113 | 114 | The actions are setup in `app/mainStores.js` (bottom) from `app/actions.js` which uses the implementation supplied with items-store. 115 | They are triggered/made by the input fields in containers, such as `app/containers/TodoListPage.jsx`. 116 | They end up affecting a store. See [How the stores work.](#how-the-stores-work) 117 | 118 | 119 | ## How the server DB works 120 | When you run `npm run start-dev` (or without `-dev` ofcourse) this will start the server, as you can see defined in `package.json`. This lets node execute `lib/server-development.js` which uses `lib/server.js` where the default data is loaded, and a server (package express) is thrown together that responds to GET POST and DELETE. 121 | 122 | This (REST API with JSON data format) server is accessible via `http://localhost:8080/_/list/mylist` for example, and this is what the application uses to fetch data for the stores. 123 | 124 | 125 | ## How the 'Random fail!' works. 126 | This [Chaos Monkey](https://github.com/Netflix/SimianArmy/wiki) lives in `lib/server.js` and helps you experience realistic server-client retrieval times and errors while developing. 127 | At some time your application is requesting 3 things from the server, and they return in the wrong order and incomplete. Will it break? 128 | Or a form could not be sent to the server. Will it notify the user? 129 | 130 | 131 | ## How the build works. 132 | A build can compile your JS files into one big file, while applying some optimisations (to make it smaller, faster, or obfuscated). 133 | Its also used to add files and features (otherwise not supported by JS in the browser) to your project, such as `JSX`, `markdown`, or `SASS`. 134 | 135 | When you start run a dev-server (like `npm run dev-server` or `npm run hot-dev-server`) it does the builds for you, while it watches for changing files. 136 | Or you can manually do a normal build using the `npm run build`. Note: these `npm run` scripts are defined in `package.json`. 137 | 138 | Depending on what way you used to do this build, a different build configuration is used. For example the normal build script as seen in `package.json` starts webpack with custom config `webpack-production.config.js` which uses shared config `make-webpack-config.js`. 139 | Most webpack configuration is in that shared config file, per entry. Only one main entry is defined. Pre-rendering happens depending on the custom config. 140 | This is where a lot of node modules (packages) come into play: loaders add JSX support, debug options are set, and the output is set to `build/` is set. 141 | 142 | # Q&A Why 143 | **Design choices explained.** 144 | Ok, so now you know the how. But why do it that way? Questions about design choices, answered by the author(s). (Tobias Koppers) 145 | 146 | **(interim, this document should only concern the How question) :exclamation: TODO extract Q&A info and document it.** 147 | 148 | What is the argument for using items-store? (from https://github.com/webpack/react-starter/pull/49 ) 149 | > Q: What was the argument for using items-store? 150 | > A: 151 | > 152 | > I didn't want to write a new module. I actually tried using Reflux.js, but couldn't find a good workflow for pre-rendering, optimistic updates and merging of multiple writes. You could do it manually but that is something I would expect from a flux implementation. items-store is a very simple store base class that coordinate this behavior (the repo actually also contains a simple helper for actions and a react mixin, but these are theoretically independent and you don't have to use them with the store). 153 | > 154 | > items-store allows to serialize and deserialize the store data to inject the data after pre-rendering. It manages optimistic updates and merges multiple writes. But items-store only offers a simple key-value store API and forces you to map more complex operations (via store-helpers) to this model. It's just a caching layer with events to the remote API (which need to be provided in the constructor). 155 | > 156 | > items-store basically provides a "raw data" store, while other implementations give you the ability to write higher level stores. In items-store you need to put this higher-level behavior in store-helpers. The advantage is that this way basic functionality is already provided by items-store. 157 | > 158 | > Now there is an alternative to items-store, which could provide "pre-rendering" part too: http://fluxible.io/ they use dehydrate and rehydrate to (de)serialize to data from the stores and provide it to the client stores. 159 | 160 | Regarding the paths that store data travels (from https://github.com/webpack/react-starter/pull/51 ) 161 | > Q: How is it triggered to refresh everything from the server, 162 | > A: `config/mainApp.jsx` invalidates store data when you change the page. When the pages read the data again it is invalid and will be refetched. 163 | 164 | > Q: how does it propagate changes when the user edits items, and 165 | > A: The component fires an action (from `app/actions.jsx`) on edit. The action is handled in `app/mainStores.jsx` and writes some items on stores. The stores update their internal cache (which is displayed on the page as optimistic update) and do server requests (through their registered handlers in `app/mainStores.jsx`. 166 | 167 | > Q: how do values travel before ending up in some components render() ? 168 | > A: component requests values from store in getProps() -> `app/mainStores.jsx` read (unavailable or invalidated) data from server -> internal `items-store` cache for the store -> getProps() -> components this.props -> render() 169 | 170 | > Q: how does the queue combine multiple requests? I cant imagine subsequent add/remove/edits would result in just one rest call.. do they? 171 | > A: items-store store a single update per entry. Any subsequent updateItem() call causes both updates to be merged (mergeUpdates from items-store). Requests from stores to the server are queued (queueRequest in app/mainStores.jsx). Here only a single ongoing request is allowed. All writes that happen in the meantime are merged into a single write. 172 | --------------------------------------------------------------------------------