diff --git a/.babelrc b/.babelrc
index 78de745..acc67d1 100644
--- a/.babelrc
+++ b/.babelrc
@@ -3,13 +3,6 @@
"es2015",
"react"
],
- "compact": true,
- "env": {
- "hmr": {
- "presets": [
- "react-hmre"
- ]
- }
- }
+ "compact": true
}
diff --git a/.flowconfig b/.flowconfig
new file mode 100644
index 0000000..d00a128
--- /dev/null
+++ b/.flowconfig
@@ -0,0 +1,3 @@
+[ignore]
+.*/node_modules/react/node_modules/.*
+
diff --git a/package.json b/package.json
index 9072613..937a652 100644
--- a/package.json
+++ b/package.json
@@ -13,7 +13,8 @@
"start": "node build/server/index.js",
"start:dev": "NODE_ENV=development node_modules/nodemon/bin/nodemon.js build/server/index.js",
"start:hot": "HMR=true npm run start:dev",
- "hot": "NODE_ENV=development BABEL_ENV=hmr node webpack-dev-server.js"
+ "hot": "NODE_ENV=development node webpack-dev-server.js",
+ "flow": "flow"
},
"license": "MIT",
"author": {
@@ -37,6 +38,7 @@
"webpack"
],
"devDependencies": {
+ "flow-bin": "^0.22.1",
"nodemon": "^1.8.1",
"webpack-dev-server": "^1.14.1"
},
@@ -46,12 +48,12 @@
"babel-loader": "^6.2.1",
"babel-preset-es2015": "^6.3.13",
"babel-preset-react": "^6.3.13",
- "babel-preset-react-hmre": "^1.0.1",
"css-loader": "^0.23.1",
+ "deep-freeze": "^0.0.1",
"express": "^4.13.3",
"extract-text-webpack-plugin": "^1.0.1",
"jade": "^1.11.0",
- "lodash": "^3.10.1",
+ "lodash": "^4.6.1",
"null-loader": "^0.1.1",
"postcss-import": "^8.0.2",
"postcss-loader": "^0.8.0",
@@ -60,6 +62,7 @@
"react-dom": "^0.14.7",
"react-router": "^2.0.0-rc4",
"rimraf": "^2.5.1",
+ "sculpt": "git@github.com:malectro/sculpt",
"serialize-javascript": "^1.1.2",
"serve-favicon": "^2.3.0",
"source-map-support": "^0.4.0",
diff --git a/src/client.js b/src/client.js
index 5aac124..fea4ab1 100644
--- a/src/client.js
+++ b/src/client.js
@@ -1,28 +1,59 @@
import React from 'react';
-import { render } from 'react-dom';
-import { Router, browserHistory, match } from 'react-router';
+import ReactDOM, {render} from 'react-dom';
+import {Router, browserHistory, match} from 'react-router';
import IndexStore from 'src/stores/index';
import routes from 'src/routes';
-import { getDependencies } from 'src/utils/index';
-import { FluxContext } from 'src/utils/wrappers';
+import {getDependencies} from 'src/utils/index';
+import FluxRoot from 'src/flux/root.jsx';
-const store = new IndexStore();
+let store = new IndexStore();
+store.initialize(data);
-store.initialize(window.data);
+let currentRoutes = routes;
// Get route dependencies whenever a route component is rendered.
const routeHandler = (Component, props) => {
getDependencies([props.route], store, props.params);
- return
+ return ;
+};
+
+function renderAll() {
+ match({currentRoutes, location}, () => {
+ render((
+
+
+
+ ), document.getElementById('app'));
+ });
}
-match({routes, location }, () => {
- render((
-
-
-
- ), document.getElementById('app'));
-})
+renderAll();
+
+
+if (module.hot) {
+ module.hot.accept('src/routes', () => {
+ // NOTE (kyle): i'm not sure if this is sound, but we'll never run HMR on prod
+ currentRoutes = require('src/routes').default;
+ ReactDOM.unmountComponentAtNode(document.getElementById('app'));
+ store.cache.setPerma(true);
+ renderAll();
+ store.cache.setPerma(false);
+ });
+
+ module.hot.accept('src/stores/index', () => {
+ const NewIndexStore = require('src/stores/index').default;
+ const data = store.serialize();
+
+ store = new NewIndexStore();
+ store.initialize(data);
+
+ ReactDOM.unmountComponentAtNode(document.getElementById('app'));
+ store.cache.setPerma(true);
+ renderAll();
+ store.cache.setPerma(false);
+ });
+}
+window.store = store;
diff --git a/src/components/app.jsx b/src/components/app.jsx
index 049614b..7328068 100644
--- a/src/components/app.jsx
+++ b/src/components/app.jsx
@@ -10,7 +10,7 @@ export default class App extends React.Component {
return (
- Hi world!
+ Hello world!!
About
Dashboard
Things
diff --git a/src/components/list/list.jsx b/src/components/list/list.jsx
index 3895329..f434636 100644
--- a/src/components/list/list.jsx
+++ b/src/components/list/list.jsx
@@ -3,12 +3,12 @@ import styles from './list.css';
import React from 'react';
import { Link } from 'react-router';
-import { subscribeToStore } from 'src/utils/wrappers';
+import FluxComponent from 'src/flux/component.jsx';
class List extends React.Component {
render() {
- const things = this.context.store.stores.ids.getState() || [];
+ const things = this.props.store.stores.ids.getOddThings() || [];
return (
{ things.map(thing => (
@@ -23,5 +23,5 @@ class List extends React.Component {
}
-export default subscribeToStore(List);
+export default FluxComponent(List);
diff --git a/src/components/thing/thing.jsx b/src/components/thing/thing.jsx
index 451255c..c4bcb05 100644
--- a/src/components/thing/thing.jsx
+++ b/src/components/thing/thing.jsx
@@ -2,22 +2,22 @@ import styles from './thing.css';
import React from 'react';
-import { subscribeToStore } from 'src/utils/wrappers';
+import FluxComponent from 'src/flux/component.jsx';
class Thing extends React.Component {
render() {
const id = parseInt(this.props.params.id, 10);
- const things = this.context.store.stores.things.getState();
+ const things = this.props.store.stores.things.getState();
const thing = things && things[id] || {};
return (
- Thing { thing.id } : { thing.text }
+ Things { thing.id } : { thing.text }
);
}
}
-export default subscribeToStore(Thing);
+export default FluxComponent(Thing);
diff --git a/src/flux/component.jsx b/src/flux/component.jsx
new file mode 100644
index 0000000..1c6b93b
--- /dev/null
+++ b/src/flux/component.jsx
@@ -0,0 +1,24 @@
+/* @flow */
+
+import React from 'react';
+
+import typeof Store from 'stores/index';
+
+
+export default function(Component: Object): Function {
+ const FluxComponent = (props, context) => {
+ const { store } = context;
+ const fluxProps: { store: Store; ref?: Function } = {
+ store,
+ };
+ if (props.fluxRef) {
+ fluxProps.ref = props.fluxRef;
+ }
+ return
;
+ }
+ FluxComponent.contextTypes = {
+ store: React.PropTypes.object,
+ };
+ return FluxComponent;
+};
+
diff --git a/src/flux/root.jsx b/src/flux/root.jsx
new file mode 100644
index 0000000..1892235
--- /dev/null
+++ b/src/flux/root.jsx
@@ -0,0 +1,45 @@
+import React from 'react';
+
+
+/**
+ * Top-level component that provides flux objects via context to all children.
+ *
+ * Should be used as a wrapper around any routing to provide the flux objects
+ * to the React subtree.
+ *
+ * Based on Redux's Provider component:
+ * https://github.com/rackt/react-redux/blob/master/src/components/Provider.js
+ *
+ * NOTE: Only supplies the store in this implementation but could be updated
+ * to provide other flux-related objects if necessary.
+ */
+export default class FluxRoot extends React.Component {
+ constructor(props, context) {
+ super(props, context);
+ this.store = props.store;
+ }
+
+ getChildContext() {
+ return {
+ store: this.store,
+ };
+ }
+
+ componentDidMount() {
+ this.listener = () => this.forceUpdate();
+ this.store.on('update', this.listener);
+ }
+
+ componentWillUnmount() {
+ this.store.removeListener('update', this.listener);
+ }
+
+ render() {
+ return this.props.children;
+ }
+}
+
+FluxRoot.childContextTypes = {
+ store: React.PropTypes.object,
+};
+
diff --git a/src/routes.js b/src/routes.js
index 53c6d0c..08d68ce 100644
--- a/src/routes.js
+++ b/src/routes.js
@@ -4,6 +4,10 @@ import { Route, IndexRoute } from 'react-router';
import App from 'src/components/app.jsx';
import Dashboard from 'src/components/dashboard/dashboard.jsx';
+import About from 'src/components/about/about.jsx';
+import Things from 'src/components/things/things.jsx';
+import ListOfThings from 'src/components/list/list.jsx';
+import Thing from 'src/components/thing/thing.jsx';
import * as actions from 'src/actions/index';
@@ -11,56 +15,13 @@ import * as actions from 'src/actions/index';
if (typeof require.ensure !== 'function') require.ensure = (d, c) => c(require)
export default (
-
{
- require.ensure([], (require) => {
- cb(null, [
- {
- path: 'about',
- getComponent(location, cb) {
- require.ensure([], () => {
- cb(null, require('src/components/about/about.jsx').default);
- });
- }
- },
- {
- path: 'list',
- getComponent(location, cb) {
- require.ensure([], () => {
- cb(null, require('src/components/things/things.jsx').default);
- });
- },
- dependencies: actions.getIds,
- getChildRoutes(location, cb) {
- require.ensure([], (require) => {
- cb(null, [
- {
- path: 'thing/:id',
- getComponent(location, cb) {
- require.ensure([], () => {
- cb(null, require('src/components/thing/thing.jsx').default);
- });
- },
- dependencies: actions.getThing,
- }
- ]);
- });
- },
- getIndexRoute(location, cb) {
- require.ensure([], (require) => {
- cb(null, {
- getComponent(location, cb) {
- require.ensure([], () => {
- cb(null, require('src/components/list/list.jsx').default);
- });
- }
- });
- });
- }
- }
- ]);
- });
- }}>
+
+
+
+
+
+
);
diff --git a/src/server.js b/src/server.js
index 25dc2d1..d39504a 100644
--- a/src/server.js
+++ b/src/server.js
@@ -10,7 +10,7 @@ import routes from './routes';
import IndexStore from './stores/index';
import { getDependencies } from './utils/index';
-import { FluxContext } from './utils/wrappers';
+import FluxRoot from './flux/root.jsx';
const DEVELOPMENT = process.env.NODE_ENV === 'development';
@@ -57,9 +57,9 @@ app.get('/*', function(req, res) {
Promise.all(dependencies)
.then(() => {
const content = renderToString((
-
+
-
+
));
const data = serialize(store.serialize());
res.render('index', {
@@ -69,8 +69,8 @@ app.get('/*', function(req, res) {
base: HOT_MODULE_REPLACEMENT ? 'http://localhost:8080' : '',
});
})
- .catch((error) => {
- console.log(error);
+ .catch(error => {
+ console.error(error.stack || error);
res.status(404).send('Not found');
});
} else {
diff --git a/src/stores/base.js b/src/stores/base.js
index 59b108b..aee1fe7 100644
--- a/src/stores/base.js
+++ b/src/stores/base.js
@@ -1,6 +1,7 @@
import _ from 'lodash';
+import deepFreeze from 'deep-freeze';
import EventEmitter from 'events';
-import update from 'react-addons-update';
+import sculpt from 'sculpt';
export default class Store extends EventEmitter {
@@ -8,18 +9,29 @@ export default class Store extends EventEmitter {
super();
this.key = key;
this.state = null;
+ this._selections = new WeakMap();
}
initialize(data) {
- this.state = data[this.key];
+ const state = data[this.key];
+ this.state = state ? deepFreeze(state) : state;
+ this._selections = new WeakMap();
}
getState() {
- return _.cloneDeep(this.state);
+ return this.state;
}
setState(data) {
- this.state = this.state ? update(this.state, {$merge: data}) : data;
+ // TODO (kyle): i'm not sure $merg'ing here is expected behavior
+ this.state = this.state ? sculpt(this.state, {$assign: data}) : deepFreeze(data);
+ this._selections = new WeakMap();
+ this.emit('update');
+ }
+
+ updateState(spec) {
+ this.state = sculpt(this.state, spec);
+ this._selections = new WeakMap();
this.emit('update');
}
@@ -28,5 +40,15 @@ export default class Store extends EventEmitter {
[this.key]: this.getState(),
};
}
+
+ createSelector(method) {
+ return function () {
+ const result = this._selections[method];
+ if (result) {
+ return result;
+ }
+ return this._selections[method] = method.apply(this, arguments);
+ }
+ }
}
diff --git a/src/stores/cache.js b/src/stores/cache.js
index 623e206..e5edb03 100644
--- a/src/stores/cache.js
+++ b/src/stores/cache.js
@@ -1,4 +1,5 @@
import _ from 'lodash';
+import {set} from 'sculpt';
export default class Cache {
@@ -11,10 +12,14 @@ export default class Cache {
}
set(key) {
- this.cache[key] = new Date().getTime();
+ this.cache = set(this.cache, key, new Date().getTime());
}
expired(key, ttl) {
+ if (this.__perma) {
+ return false;
+ }
+
const cached = this.cache[key];
if (!cached) {
return true;
@@ -22,7 +27,7 @@ export default class Cache {
const now = new Date().getTime();
if (now - cached > ttl) {
- delete this.cache[key];
+ this.cache = _.omit(this.cache, key);
return true;
}
@@ -32,5 +37,9 @@ export default class Cache {
serialize() {
return this.cache;
}
+
+ setPerma(perma) {
+ this.__perma = perma;
+ }
}
diff --git a/src/stores/ids.js b/src/stores/ids.js
index 05142f2..75a153c 100644
--- a/src/stores/ids.js
+++ b/src/stores/ids.js
@@ -1,8 +1,15 @@
+import _ from 'lodash';
import Store from './base';
export default class IdStore extends Store {
constructor() {
super('ids');
+
+ this.getOddThings = this.createSelector(this.getOddThings);
+ }
+
+ getOddThings() {
+ return this.state.filter(id => id % 2);
}
}
diff --git a/src/stores/index.js b/src/stores/index.js
index f9b72fc..c8a4818 100644
--- a/src/stores/index.js
+++ b/src/stores/index.js
@@ -1,6 +1,6 @@
import _ from 'lodash';
import EventEmitter from 'events';
-import update from 'react-addons-update';
+import sculpt from 'sculpt';
import IdStore from './ids';
import ThingStore from './things';
@@ -35,12 +35,16 @@ class IndexStore extends EventEmitter {
let data = _.reduce(this.stores, (state, store) => {
const storeState = store.serialize();
if (storeState) {
- state = update(state, {$merge: storeState});
+ state = sculpt(state, {$assign: storeState});
}
return state;
}, {});
- data._cache = this.cache.serialize();
+ data = sculpt(data, {
+ _cache: {
+ $set: this.cache.serialize(),
+ },
+ });
return data;
}
diff --git a/src/utils/wrappers.js b/src/utils/wrappers.js
deleted file mode 100644
index fab1757..0000000
--- a/src/utils/wrappers.js
+++ /dev/null
@@ -1,78 +0,0 @@
-import React from 'react';
-
-
-// Note: This module is used to create higher order components as an
-// alternative to using mixins, based on the pattern here:
-// https://medium.com/@dan_abramov/
-// mixins-are-dead-long-live-higher-order-components-94a0d2f9e750
-
-
-/**
- * Re-render the given Component whenever the store updates and add the
- * store to the Component's context (e.g. `this.context.store`).
- *
- * @param {Component} Component to re-render on store updates.
- *
- * @return {Component} Higher order component that will re-render on store
- * updates.
- */
-export function subscribeToStore(Component) {
- // Add store to component's context (via `this.context.store`).
- Component.contextTypes = {
- store: React.PropTypes.object,
- };
-
- return React.createClass({
- contextTypes: {
- store: React.PropTypes.object,
- },
-
- componentDidMount() {
- this.context.store.on('update', this.rerender);
- },
-
- componentWillUnmount() {
- this.context.store.removeListener('update', this.rerender);
- },
-
- rerender() {
- this.forceUpdate();
- },
-
- render() {
- return ;
- }
- });
-};
-
-
-/**
- * Top-level component that provides flux objects via context to all children.
- *
- * Should be used as a wrapper around any routing to provide the flux objects
- * to the React subtree.
- *
- * Based on Redux's Provider component:
- * https://github.com/rackt/react-redux/blob/master/src/components/Provider.js
- *
- * NOTE: Only supplies the store in this implementation but could be updated
- * to provide other flux-related objects if necessary.
- */
-export class FluxContext extends React.Component {
- getChildContext() {
- return { store: this.store };
- }
-
- constructor(props, context) {
- super(props, context);
- this.store = props.store;
- }
-
- render() {
- return this.props.children;
- }
-}
-FluxContext.childContextTypes = {
- store: React.PropTypes.object,
-}
-