All application state is stored in one Immutable Map, similar to Om.
Stores declaratively register pure functions to handle state changes, massively simplifying testing and debugging state changes.
Compose and transform your data together statelessly and efficiently using a functional lens concept called Getters.
This allows your views to receive exactly the data they need in a way that is fully decoupled from stores. Best of all, this pattern eliminates the confusing store.waitsFor
method found in other Flux implementations.
Any Getter can be observed by a view to be notified whenever its derived value changes.
NuclearJS includes tools to integrate with libraries such as React and VueJS out of the box.
Thanks to immutable data, change detection can be efficiently performed at any level of granularity by a constant time reference equality (===)
check.
Since Getters use pure functions, NuclearJS utilizes memoization to only recompute parts of the dataflow that might change.
Name | Type | Price |
---|---|---|
banana | food | 1 |
doritos | food | 4 |
shirt | clothes | 15 |
pants | clothes | 20 |
AppState {
"typeFilter": null,
"items": [
{"type": "food","name": "banana","price": 1},
{"type": "food","name": "doritos","price": 4},
{"type": "clothes","name": "shirt","price": 15},
{"type": "clothes","name": "pants","price": 20}
]
}
filteredItems Getter [
{"type": "food","name": "banana","price": 1},
{"type": "food","name": "doritos","price": 4},
{"type": "clothes","name": "shirt","price": 15},
{"type": "clothes","name": "pants","price": 20}
]
import { Reactor, Store, toImmutable } from 'nuclear-js'
import React from 'react'
const reactor = new Reactor({ debug: true });
reactor.registerStores({
typeFilter: Store({
getInitialState() {
return null;
},
initialize() {
this.on('FILTER_TYPE', (state, type) => type)
}
}),
items: Store({
getInitialState() {
return toImmutable([
{ type: 'food', name: 'banana', price: 1 },
{ type: 'food', name: 'doritos', price: 4 },
{ type: 'clothes', name: 'shirt', price: 15 },
{ type: 'clothes', name: 'pants', price: 20 },
])
},
initialize() {
this.on('ADD_ITEM', (state, item) => state.push(item))
}
})
})
Reactor
In NuclearJS the reactor
acts as the dispatcher, maintains the application state and provides an API for data access and observation.
Stores determine the shape of your application state. Stores define two methods:
getInitialState()
- Returns the initial state for that stores specific key in the application state.
initialize()
- Sets up any action handlers, by specifying the action type and a function that transforms
(storeState, action) => (newStoreState)
const filteredItemsGetter = [
['typeFilter'],
['items'],
(filter, items) => {
return (filter)
? items.filter(i => i.get('type') === filter)
: items
}
]
Getters allow you to easily compose and transform your application state in a reusable way.
const ItemViewer = React.createClass({
mixins: [reactor.ReactMixin],
getDataBindings() {
return {
items: filteredItemsGetter
}
},
render() {
return (
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Price</th>
</tr>
</thead>
<tbody>
{this.state.items.map(item => {
return <tr>
<td>{item.get('name')}</td>
<td>{item.get('type')}</td>
<td>{item.get('price')}</td>
</tr>
})}
</tbody>
</table>
)
}
})
Simply use the reactor.ReactMixin
and implement the getDataBindings()
function to automatically sync any getter to a this.state
property on a React component.
Since application state can only change after a dispatch then NuclearJS can be intelligent and only call this.setState
whenever the actual value of the getter changes. Meaning less pressure on React's DOM diffing.
This example shows how to use NuclearJS with React, however the same concepts can be extended to any reactive UI framework. In fact, the ReactMixin code is only about 40 lines.
const actions = {
setFilter(type) {
reactor.dispatch('FILTER_TYPE' type)
},
addItem(name, type, price) {
reactor.dispatch('ADD_ITEM', toImmutable({
name,
type,
price
}))
}
}
actions.addItem('computer', 'electronics', 1999)
actions.setFilter('electronics')
NuclearJS maintains a very non-magical approach to dispatching actions. Simply call reactor.dispatch
with the actionType
and payload
.
All action handling is done synchronously, leaving the state of the system very predictable after every action.
Because actions are simply functions, it is very easy to compose actions together using plain JavaScript.
// Evaluate by key path
var itemsList = reactor.evaluate(['items'])
var item0Price = reactor.evaluate(['items', 0, 'price'])
// Evaluate by getter
var filteredItems = reactor.evaluate(filteredItemsGetter)
// Evaluate and coerce to plain JavaScript
var itemsPOJO = reactor.evaluateToJS(filteredItemsGetter)
// Observation
reactor.observe(filteredItemsGetter, items => {
console.log(items)
})
NuclearJS also provides imperative mechanisms for evaluating and observing state.
In fact any getter can synchronously and imperatively evaluated or observed. The entire ReactMixin
is built using only those two functions.
Optimizely has been using NuclearJS in production since 2014 and will offer long term support and a stable API.
With NuclearJS' built in logger you can inspect your application state from the beginning of time. NuclearJS makes tracking down difficult bugs a breeze, allowing you to focus more time writing code.
When building with NuclearJS there is never a question of "How do I test this?". There are prescribed testing strategies for every type of thing you will build with NuclearJS.
For large codebases the prescribed way of organization is to group all stores, actions and getters of the same domain in a module.
This method of code organization is extremely portable, making it almost trivial to refactor, split code into multiple bundles and create contracts between modules.
In fact, Optimizely's codebase has over 50 modules and is growing everyday. Using this pattern makes it easy for teams to consume other teams modules, leading to great code reusability.