Skip to content

Commit

Permalink
Merge pull request #202 from optimizely/jordan/add-replace-stores
Browse files Browse the repository at this point in the history
Add replaceStores
  • Loading branch information
jordangarcia committed Dec 27, 2015
2 parents 6342f62 + 9483417 commit b9e202b
Show file tree
Hide file tree
Showing 21 changed files with 412 additions and 2 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# NuclearJS
NuclearJS

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

## How NuclearJS differs from other Flux implementations

Expand Down
9 changes: 9 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
TODO for `1.3.0`
===

- [ ] add documentation for all new reactor options
- [ ] link the nuclear-js package from the hot reloadable example
- [ ] link `0.3.0` of `nuclear-js-react-addons` in hot reloadable example
- [ ] add `nuclear-js-react-addons` link in example and documentation
- [ ] publish doc site

13 changes: 13 additions & 0 deletions docs/src/docs/07-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,19 @@ reactor.registerStores({
})
```
#### `Reactor#replacStores(stores)`
`stores` - an object of storeId => store instance
Replace the implementation only of specified stores without resetting to their initial state. This is useful when doing store hot reloading.
```javascript
reactor.replaceStores({
'threads': require('./stores/thread-store'),
'currentThreadID': require('./stores/current-thread-id-store'),
})
```
#### `Reactor#reset()`
Causes all stores to be reset to their initial state. Extremely useful for testing, just put a `reactor.reset()` call in your `afterEach` blocks.
Expand Down
7 changes: 7 additions & 0 deletions examples/hot-reloading/.babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
presets: ['es2015', 'react'],
plugins: ['transform-decorators-legacy', 'syntax-decorators'],
ignore: [
'**/nuclear-js-react-addons/**',
]
}
2 changes: 2 additions & 0 deletions examples/hot-reloading/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dist
node_modules
38 changes: 38 additions & 0 deletions examples/hot-reloading/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
NuclearJS Hot Reloading
===

NuclearJS supports hot reloading of stores. Using the webpack Hot Module Replacement simply code like this to wherever your stores are registered.


```js
import { Reactor } from 'nuclear-js'
import * as stores from './stores'

const reactor = new Reactor({
debug: true,
})
reactor.registerStores(stores)

if (module.hot) {
// Enable webpack hot module replacement for stores
module.hot.accept('./stores', () => {
reactor.replaceStores(require('./stores'))
})
}

export default reactor
```

## Running Example

```
npm install
npm start
```

Go to [http://localhost:3000](http://localhost:3000)

## Inpsiration & Thanks

Big thanks to [redux](https://github.com/rackt/redux) and [react-redux](https://github.com/rackt/react-redux) for proving out this architecture
and creating simple APIs to accomplish hot reloading.
12 changes: 12 additions & 0 deletions examples/hot-reloading/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html>
<head>
<title>NuclearJS Hot Reloading</title>
</head>

<body style="padding: 30px;">
<div id="root"></div>
<script src="http://localhost:3000/dist/app.js"></script>
</body>
</html>

30 changes: 30 additions & 0 deletions examples/hot-reloading/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "nuclear-js-hot-reloading-example",
"version": "1.0.0",
"description": "Hot Reloading with NuclearJS",
"main": "index.js",
"scripts": {
"start": "node webpack-server.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Jordan Garcia",
"license": "MIT",
"dependencies": {
"react": "^0.14.3",
"react-dom": "^0.14.3"
},
"devDependencies": {
"babel-cli": "^6.3.17",
"babel-core": "^6.3.21",
"babel-loader": "^6.2.0",
"babel-plugin-syntax-decorators": "^6.3.13",
"babel-plugin-transform-decorators": "^6.3.13",
"babel-plugin-transform-decorators-legacy": "^1.3.4",
"babel-preset-es2015": "^6.3.13",
"babel-preset-react": "^6.3.13",
"babel-preset-stage-0": "^6.3.13",
"react-hot-loader": "^1.3.0",
"webpack": "^1.12.9",
"webpack-dev-server": "^1.12.1"
}
}
7 changes: 7 additions & 0 deletions examples/hot-reloading/src/actions/counter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function increment(reactor) {
reactor.dispatch('increment')
}

export function decrement(reactor) {
reactor.dispatch('decrement')
}
24 changes: 24 additions & 0 deletions examples/hot-reloading/src/components/counter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React, { Component, PropTypes } from 'react'

class Counter extends Component {
render() {
const { increment, decrement, counter } = this.props
return (
<p>
Clicked: {counter} times
{' '}
<button onClick={increment}>+</button>
{' '}
<button onClick={decrement}>-</button>
</p>
)
}
}

Counter.propTypes = {
increment: PropTypes.func.isRequired,
decrement: PropTypes.func.isRequired,
counter: PropTypes.number.isRequired
}

export default Counter
18 changes: 18 additions & 0 deletions examples/hot-reloading/src/containers/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React, { Component } from 'react'
import { connect } from 'nuclear-js-react-addons'
import Counter from '../components/Counter'
import { increment, decrement } from '../actions/counter'

@connect(props => ({
counter: ['counter']
}))
export default class AppContainer extends Component {
render() {
let { reactor, counter } = this.props
return <Counter
counter={counter}
increment={increment.bind(null, reactor)}
decrement={decrement.bind(null, reactor)}
/>
}
}
12 changes: 12 additions & 0 deletions examples/hot-reloading/src/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'nuclear-js-react-addons'
import App from './containers/App'
import reactor from './reactor'

render(
<Provider reactor={reactor}>
<App />
</Provider>,
document.getElementById('root')
)
16 changes: 16 additions & 0 deletions examples/hot-reloading/src/reactor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Reactor } from 'nuclear-js'
import * as stores from './stores'

const reactor = new Reactor({
debug: true,
})
reactor.registerStores(stores)

if (module.hot) {
// Enable Webpack hot module replacement for stores
module.hot.accept('./stores', () => {
reactor.replaceStores(require('./stores'))
})
}

export default reactor
11 changes: 11 additions & 0 deletions examples/hot-reloading/src/stores/counter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Store } from 'nuclear-js'

export default new Store({
getInitialState() {
return 0
},
initialize() {
this.on('increment', (state) => state + 1)
this.on('decrement', (state) => state - 1)
}
})
5 changes: 5 additions & 0 deletions examples/hot-reloading/src/stores/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import counter from './counter'

export {
counter
}
15 changes: 15 additions & 0 deletions examples/hot-reloading/webpack-server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
var webpack = require('webpack');
var WebpackDevServer = require('webpack-dev-server');
var config = require('./webpack.config');

new WebpackDevServer(webpack(config), {
publicPath: config.output.publicPath,
hot: true,
historyApiFallback: true
}).listen(3000, 'localhost', function (err, result) {
if (err) {
console.log(err);
}

console.log('Listening at localhost:3000');
});
28 changes: 28 additions & 0 deletions examples/hot-reloading/webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
var path = require('path');
var webpack = require('webpack');

module.exports = {
entry: [
'webpack-dev-server/client?http://localhost:3000',
'webpack/hot/only-dev-server',
'./src/main'
],
output: {
path: path.join(__dirname, 'public', 'dist'),
filename: 'app.js',
publicPath: 'http://localhost:3000/dist/'
},
plugins: [
new webpack.HotModuleReplacementPlugin()
],
module: {
loaders: [
{
test: /\.js$/,
exclude: /node_modules/,
loaders: ['react-hot', 'babel-loader'],
}
]
},
devtool: 'eval',
}
11 changes: 10 additions & 1 deletion src/reactor.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,13 +158,22 @@ class Reactor {
}

/**
* @param {Store[]} stores
* @param {Object} stores
*/
registerStores(stores) {
this.reactorState = fns.registerStores(this.reactorState, stores)
this.__notify()
}

/**
* Replace store implementation (handlers) without modifying the app state or calling getInitialState
* Useful for hot reloading
* @param {Object} stores
*/
replaceStores(stores) {
this.reactorState = fns.replaceStores(this.reactorState, stores)
}

/**
* Returns a plain object representing the application state
* @return {Object}
Expand Down
22 changes: 22 additions & 0 deletions src/reactor/fns.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,28 @@ export function registerStores(reactorState, stores) {
})
}

/**
* Overrides the store implementation without resetting the value of that particular part of the app state
* this is useful when doing hot reloading of stores.
* @param {ReactorState} reactorState
* @param {Object<String, Store>} stores
* @return {ReactorState}
*/
export function replaceStores(reactorState, stores) {
return reactorState.withMutations((reactorState) => {
each(stores, (store, id) => {
const initialState = store.getInitialState()

if (getOption(reactorState, 'throwOnNonImmutableStore') && !isImmutableValue(initialState)) {
throw new Error('Store getInitialState() must return an immutable value, did you forget to call toImmutable')
}

reactorState
.update('stores', stores => stores.set(id, store))
})
})
}

/**
* @param {ReactorState} reactorState
* @param {String} actionType
Expand Down
50 changes: 50 additions & 0 deletions tests/reactor-fns-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,56 @@ describe('reactor fns', () => {
})
})

describe('#registerStores', () => {
let reactorState
let store1
let store2
let newStore1
let originalReactorState
let nextReactorState

beforeEach(() => {
reactorState = new ReactorState()
store1 = new Store({
getInitialState() {
return toImmutable({
foo: 'bar',
})
},
})
store2 = new Store({
getInitialState() {
return 2
},
})

newStore1 = new Store({
getInitialState() {
return toImmutable({
foo: 'newstore',
})
},
})

originalReactorState = fns.registerStores(reactorState, {
store1,
store2,
})

nextReactorState = fns.replaceStores(originalReactorState, {
store1: newStore1
})
})

it('should update reactorState.stores', () => {
const expectedReactorState = originalReactorState.set('stores', Map({
store1: newStore1,
store2: store2,
}))
expect(is(nextReactorState, expectedReactorState)).toBe(true)
})
})

describe('#dispatch', () => {
let initialReactorState, store1, store2
let nextReactorState
Expand Down
Loading

0 comments on commit b9e202b

Please sign in to comment.