Skip to content

Commit b9e202b

Browse files
committed
Merge pull request #202 from optimizely/jordan/add-replace-stores
Add replaceStores
2 parents 6342f62 + 9483417 commit b9e202b

21 files changed

+412
-2
lines changed

README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# NuclearJS
1+
NuclearJS
22

33
[![Build Status](https://travis-ci.org/optimizely/nuclear-js.svg?branch=master)](https://travis-ci.org/optimizely/nuclear-js)
44
[![Coverage Status](https://coveralls.io/repos/optimizely/nuclear-js/badge.svg?branch=master)](https://coveralls.io/r/optimizely/nuclear-js?branch=master)
@@ -37,6 +37,7 @@ npm install nuclear-js
3737
- [Shopping Cart Example](./examples/shopping-cart) - Provides a general overview of basic NuclearJS concepts: actions, stores and getters with ReactJS.
3838
- [Flux Chat Example](./examples/flux-chat) - A classic Facebook flux chat example written in NuclearJS.
3939
- [Rest API Example](./examples/rest-api) - Shows how to deal with fetching data from an API using NuclearJS conventions.
40+
- [Hot reloadable stores](./examples/hot-reloading) - Shows how to setup stores to be hot reloadable using webpack hot module replacement.
4041

4142
## How NuclearJS differs from other Flux implementations
4243

TODO.md

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
TODO for `1.3.0`
2+
===
3+
4+
- [ ] add documentation for all new reactor options
5+
- [ ] link the nuclear-js package from the hot reloadable example
6+
- [ ] link `0.3.0` of `nuclear-js-react-addons` in hot reloadable example
7+
- [ ] add `nuclear-js-react-addons` link in example and documentation
8+
- [ ] publish doc site
9+

docs/src/docs/07-api.md

+13
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,19 @@ reactor.registerStores({
172172
})
173173
```
174174
175+
#### `Reactor#replacStores(stores)`
176+
177+
`stores` - an object of storeId => store instance
178+
179+
Replace the implementation only of specified stores without resetting to their initial state. This is useful when doing store hot reloading.
180+
181+
```javascript
182+
reactor.replaceStores({
183+
'threads': require('./stores/thread-store'),
184+
'currentThreadID': require('./stores/current-thread-id-store'),
185+
})
186+
```
187+
175188
#### `Reactor#reset()`
176189
177190
Causes all stores to be reset to their initial state. Extremely useful for testing, just put a `reactor.reset()` call in your `afterEach` blocks.

examples/hot-reloading/.babelrc

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
presets: ['es2015', 'react'],
3+
plugins: ['transform-decorators-legacy', 'syntax-decorators'],
4+
ignore: [
5+
'**/nuclear-js-react-addons/**',
6+
]
7+
}

examples/hot-reloading/.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
dist
2+
node_modules

examples/hot-reloading/README.md

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
NuclearJS Hot Reloading
2+
===
3+
4+
NuclearJS supports hot reloading of stores. Using the webpack Hot Module Replacement simply code like this to wherever your stores are registered.
5+
6+
7+
```js
8+
import { Reactor } from 'nuclear-js'
9+
import * as stores from './stores'
10+
11+
const reactor = new Reactor({
12+
debug: true,
13+
})
14+
reactor.registerStores(stores)
15+
16+
if (module.hot) {
17+
// Enable webpack hot module replacement for stores
18+
module.hot.accept('./stores', () => {
19+
reactor.replaceStores(require('./stores'))
20+
})
21+
}
22+
23+
export default reactor
24+
```
25+
26+
## Running Example
27+
28+
```
29+
npm install
30+
npm start
31+
```
32+
33+
Go to [http://localhost:3000](http://localhost:3000)
34+
35+
## Inpsiration & Thanks
36+
37+
Big thanks to [redux](https://github.com/rackt/redux) and [react-redux](https://github.com/rackt/react-redux) for proving out this architecture
38+
and creating simple APIs to accomplish hot reloading.

examples/hot-reloading/index.html

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<title>NuclearJS Hot Reloading</title>
5+
</head>
6+
7+
<body style="padding: 30px;">
8+
<div id="root"></div>
9+
<script src="http://localhost:3000/dist/app.js"></script>
10+
</body>
11+
</html>
12+

examples/hot-reloading/package.json

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"name": "nuclear-js-hot-reloading-example",
3+
"version": "1.0.0",
4+
"description": "Hot Reloading with NuclearJS",
5+
"main": "index.js",
6+
"scripts": {
7+
"start": "node webpack-server.js",
8+
"test": "echo \"Error: no test specified\" && exit 1"
9+
},
10+
"author": "Jordan Garcia",
11+
"license": "MIT",
12+
"dependencies": {
13+
"react": "^0.14.3",
14+
"react-dom": "^0.14.3"
15+
},
16+
"devDependencies": {
17+
"babel-cli": "^6.3.17",
18+
"babel-core": "^6.3.21",
19+
"babel-loader": "^6.2.0",
20+
"babel-plugin-syntax-decorators": "^6.3.13",
21+
"babel-plugin-transform-decorators": "^6.3.13",
22+
"babel-plugin-transform-decorators-legacy": "^1.3.4",
23+
"babel-preset-es2015": "^6.3.13",
24+
"babel-preset-react": "^6.3.13",
25+
"babel-preset-stage-0": "^6.3.13",
26+
"react-hot-loader": "^1.3.0",
27+
"webpack": "^1.12.9",
28+
"webpack-dev-server": "^1.12.1"
29+
}
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export function increment(reactor) {
2+
reactor.dispatch('increment')
3+
}
4+
5+
export function decrement(reactor) {
6+
reactor.dispatch('decrement')
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import React, { Component, PropTypes } from 'react'
2+
3+
class Counter extends Component {
4+
render() {
5+
const { increment, decrement, counter } = this.props
6+
return (
7+
<p>
8+
Clicked: {counter} times
9+
{' '}
10+
<button onClick={increment}>+</button>
11+
{' '}
12+
<button onClick={decrement}>-</button>
13+
</p>
14+
)
15+
}
16+
}
17+
18+
Counter.propTypes = {
19+
increment: PropTypes.func.isRequired,
20+
decrement: PropTypes.func.isRequired,
21+
counter: PropTypes.number.isRequired
22+
}
23+
24+
export default Counter
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import React, { Component } from 'react'
2+
import { connect } from 'nuclear-js-react-addons'
3+
import Counter from '../components/Counter'
4+
import { increment, decrement } from '../actions/counter'
5+
6+
@connect(props => ({
7+
counter: ['counter']
8+
}))
9+
export default class AppContainer extends Component {
10+
render() {
11+
let { reactor, counter } = this.props
12+
return <Counter
13+
counter={counter}
14+
increment={increment.bind(null, reactor)}
15+
decrement={decrement.bind(null, reactor)}
16+
/>
17+
}
18+
}

examples/hot-reloading/src/main.js

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import React from 'react'
2+
import { render } from 'react-dom'
3+
import { Provider } from 'nuclear-js-react-addons'
4+
import App from './containers/App'
5+
import reactor from './reactor'
6+
7+
render(
8+
<Provider reactor={reactor}>
9+
<App />
10+
</Provider>,
11+
document.getElementById('root')
12+
)

examples/hot-reloading/src/reactor.js

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { Reactor } from 'nuclear-js'
2+
import * as stores from './stores'
3+
4+
const reactor = new Reactor({
5+
debug: true,
6+
})
7+
reactor.registerStores(stores)
8+
9+
if (module.hot) {
10+
// Enable Webpack hot module replacement for stores
11+
module.hot.accept('./stores', () => {
12+
reactor.replaceStores(require('./stores'))
13+
})
14+
}
15+
16+
export default reactor
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Store } from 'nuclear-js'
2+
3+
export default new Store({
4+
getInitialState() {
5+
return 0
6+
},
7+
initialize() {
8+
this.on('increment', (state) => state + 1)
9+
this.on('decrement', (state) => state - 1)
10+
}
11+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import counter from './counter'
2+
3+
export {
4+
counter
5+
}
+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
var webpack = require('webpack');
2+
var WebpackDevServer = require('webpack-dev-server');
3+
var config = require('./webpack.config');
4+
5+
new WebpackDevServer(webpack(config), {
6+
publicPath: config.output.publicPath,
7+
hot: true,
8+
historyApiFallback: true
9+
}).listen(3000, 'localhost', function (err, result) {
10+
if (err) {
11+
console.log(err);
12+
}
13+
14+
console.log('Listening at localhost:3000');
15+
});
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
var path = require('path');
2+
var webpack = require('webpack');
3+
4+
module.exports = {
5+
entry: [
6+
'webpack-dev-server/client?http://localhost:3000',
7+
'webpack/hot/only-dev-server',
8+
'./src/main'
9+
],
10+
output: {
11+
path: path.join(__dirname, 'public', 'dist'),
12+
filename: 'app.js',
13+
publicPath: 'http://localhost:3000/dist/'
14+
},
15+
plugins: [
16+
new webpack.HotModuleReplacementPlugin()
17+
],
18+
module: {
19+
loaders: [
20+
{
21+
test: /\.js$/,
22+
exclude: /node_modules/,
23+
loaders: ['react-hot', 'babel-loader'],
24+
}
25+
]
26+
},
27+
devtool: 'eval',
28+
}

src/reactor.js

+10-1
Original file line numberDiff line numberDiff line change
@@ -158,13 +158,22 @@ class Reactor {
158158
}
159159

160160
/**
161-
* @param {Store[]} stores
161+
* @param {Object} stores
162162
*/
163163
registerStores(stores) {
164164
this.reactorState = fns.registerStores(this.reactorState, stores)
165165
this.__notify()
166166
}
167167

168+
/**
169+
* Replace store implementation (handlers) without modifying the app state or calling getInitialState
170+
* Useful for hot reloading
171+
* @param {Object} stores
172+
*/
173+
replaceStores(stores) {
174+
this.reactorState = fns.replaceStores(this.reactorState, stores)
175+
}
176+
168177
/**
169178
* Returns a plain object representing the application state
170179
* @return {Object}

src/reactor/fns.js

+22
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,28 @@ export function registerStores(reactorState, stores) {
4848
})
4949
}
5050

51+
/**
52+
* Overrides the store implementation without resetting the value of that particular part of the app state
53+
* this is useful when doing hot reloading of stores.
54+
* @param {ReactorState} reactorState
55+
* @param {Object<String, Store>} stores
56+
* @return {ReactorState}
57+
*/
58+
export function replaceStores(reactorState, stores) {
59+
return reactorState.withMutations((reactorState) => {
60+
each(stores, (store, id) => {
61+
const initialState = store.getInitialState()
62+
63+
if (getOption(reactorState, 'throwOnNonImmutableStore') && !isImmutableValue(initialState)) {
64+
throw new Error('Store getInitialState() must return an immutable value, did you forget to call toImmutable')
65+
}
66+
67+
reactorState
68+
.update('stores', stores => stores.set(id, store))
69+
})
70+
})
71+
}
72+
5173
/**
5274
* @param {ReactorState} reactorState
5375
* @param {String} actionType

tests/reactor-fns-tests.js

+50
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,56 @@ describe('reactor fns', () => {
7474
})
7575
})
7676

77+
describe('#registerStores', () => {
78+
let reactorState
79+
let store1
80+
let store2
81+
let newStore1
82+
let originalReactorState
83+
let nextReactorState
84+
85+
beforeEach(() => {
86+
reactorState = new ReactorState()
87+
store1 = new Store({
88+
getInitialState() {
89+
return toImmutable({
90+
foo: 'bar',
91+
})
92+
},
93+
})
94+
store2 = new Store({
95+
getInitialState() {
96+
return 2
97+
},
98+
})
99+
100+
newStore1 = new Store({
101+
getInitialState() {
102+
return toImmutable({
103+
foo: 'newstore',
104+
})
105+
},
106+
})
107+
108+
originalReactorState = fns.registerStores(reactorState, {
109+
store1,
110+
store2,
111+
})
112+
113+
nextReactorState = fns.replaceStores(originalReactorState, {
114+
store1: newStore1
115+
})
116+
})
117+
118+
it('should update reactorState.stores', () => {
119+
const expectedReactorState = originalReactorState.set('stores', Map({
120+
store1: newStore1,
121+
store2: store2,
122+
}))
123+
expect(is(nextReactorState, expectedReactorState)).toBe(true)
124+
})
125+
})
126+
77127
describe('#dispatch', () => {
78128
let initialReactorState, store1, store2
79129
let nextReactorState

0 commit comments

Comments
 (0)