REACT RADIOACTIVE
INTRO
SPEAKER
AGENDA
History
Result
Implementation
Radioactive
Architecture
Challenges
TIMELINE
INTEREST
NEGOTIATION
SELLING
START
RADIOACTIVE
PREPARATION
Flux
ReactJS
ECMAScript
POWER STATION
ARCHITECTURE
POWER ARCHITECTURE
FLYPRO
DEMAND
CONCEPT
SPECIFICATION
import chai, { expect } from 'chai';
import sinon from 'sinon';
import sinonChai from 'sinon-chai';
import createStore from '../../src/';
chai.use(sinonChai);
describe('Create store', () => {
let mockStore;
let mockHandler;
let mockState;
let mockListener;
beforeEach(() => {
mockHandler = (state, command) => command.type === 'TEST_FLYPRO' ? 'newState' : state;
mockState = () => 'mockState';
mockStore = createStore(mockHandler, mockState);
mockListener = sinon.stub();
});
it('should return public API', () => {
expect(mockStore).to.deep.equal({
send: mockStore.send,
subscribe: mockStore.subscribe,
getState: mockStore.getState,
getListeners: mockStore.getListeners
});
});
it('should always retrieve relevant state', () => {
expect(mockStore.getState()).to.equal(mockState);
});
it('should always retrieve relevant listeners', () => {
expect(mockStore.getListeners()).to.deep.equal([]);
});
it('should add listener when subscribing to store', () => {
mockStore.subscribe(mockListener);
expect(mockStore.getListeners()).to.deep.equal([mockListener]);
});
it('should remove listener when unsubscribing from store', () => {
const mockStoreUnsubscribe = mockStore.subscribe(mockListener);
mockStoreUnsubscribe();
expect(mockStore.getListeners()).to.deep.equal([]);
});
it('should change current state when sending a command', () => {
mockStore.send({ type: 'TEST_FLYPRO' });
expect(mockStore.getState()).to.equal('newState');
});
it('should invoke listener when sending a command', () => {
mockStore.subscribe(mockListener);
mockStore.send({ type: 'TEST_FLYPRO' });
expect(mockListener).calledWith();
});
});
IMPLEMENTATION
export default function createStore(handler, state) {
const currentHandler = handler;
let currentState = state;
const listeners = [];
function getState() {
return currentState;
}
function getListeners() {
return listeners;
}
function subscribe(listener) {
listeners.push(listener);
return function unsubscribe() {
listeners.splice(listeners.indexOf(listener), 1);
};
}
function send(command) {
currentState = currentHandler(currentState, command);
listeners.slice().forEach(listener => listener());
return command;
}
send({ type: 'INIT_FLYPRO' });
return { send, subscribe, getState, getListeners };
}
REACT BINDINGS
import React, { PropTypes } from 'react';
import chai, { expect } from 'chai';
import sinon from 'sinon';
import sinonChai from 'sinon-chai';
import { mount } from 'enzyme';
import chaiEnzyme from 'chai-enzyme';
import wrap from '../../src/';
chai.use(sinonChai);
chai.use(chaiEnzyme());
describe('Wrap', () => {
let mockStore;
let mockStates;
let mockHandlers;
let MockComponent;
let ExtendedComponent;
let instanceOfExtendedComponent;
const instanceOfExtendedComponentUnsubscribeStub = sinon.stub();
beforeEach(() => {
mockStore = {
getState: () => {},
send: () => {},
subscribe: sinon.stub().returns(instanceOfExtendedComponentUnsubscribeStub)
};
mockStates = () => ({
mockState: 'mockStateValue'
});
mockHandlers = () => ({
mockHandler: 'mockHandlerValue'
});
});
beforeEach(() => {
MockComponent = () => (
<div className="mock-component" />
);
ExtendedComponent = wrap(mockStates, mockHandlers)(<MockComponent store={mockStore} />);
instanceOfExtendedComponent = new ExtendedComponent();
// pass the store to each component explicitly
instanceOfExtendedComponent.props = mount(<MockComponent store={mockStore} />).props();
});
it('should validate prop types', () => {
const propTypes = ExtendedComponent.propTypes();
expect(propTypes).to.deep.equal({
store: PropTypes.object.isRequired
});
});
it('should extend base component with store', () => {
const WrappedComponent = instanceOfExtendedComponent.render();
expect(WrappedComponent.props.store).to.deep.equal(mockStore);
});
it('should extend base component with states', () => {
const WrappedComponent = instanceOfExtendedComponent.render();
expect(WrappedComponent.props.mockState).to.equal('mockStateValue');
});
it('should extend base component with handlers', () => {
const WrappedComponent = instanceOfExtendedComponent.render();
expect(WrappedComponent.props.mockHandler).to.equal('mockHandlerValue');
});
it('should subscribe for store changes on component did mount', () => {
instanceOfExtendedComponent.componentDidMount();
expect(mockStore.subscribe).calledWith(sinon.match.func);
});
it('should unsubscribe for store changes on component will unmount', () => {
instanceOfExtendedComponent.componentDidMount(); // to create unsubscribe handler
instanceOfExtendedComponent.componentWillUnmount();
expect(instanceOfExtendedComponentUnsubscribeStub).calledWith();
});
});
IMPLEMENTATION
import React, { Component, PropTypes } from 'react';
export default function wrap(states, handlers) {
return function extend(WrappedComponent) {
return class extends Component {
static propTypes() {
return {
store: PropTypes.object.isRequired
};
}
render() {
return (
<WrappedComponent
{...this.props}
{...states(this.props.store.getState(), this.props)}
{...handlers(this.props.store.send, this.props)}
/>
);
}
componentDidMount() {
this.unsubscribe = this.props.store.subscribe(this.forceUpdate.bind(this));
}
componentWillUnmount() {
this.unsubscribe();
}
};
};
}
EFFECT
CHALLENGES
ASYNC THUNK
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
LOCAL STORAGE
import 'es6-promise';
import * as storage from 'redux-storage';
import createEngine from 'redux-storage-engine-localstorage';
import * as actionTypes from '../actions/actionTypes';
const replacer = (key, value) => {
if (key === '') {
const {
promo: { isHidden },
settings: { showPedigree }
} = value;
return { promo: { isHidden }, settings: { showPedigree } };
}
return value;
};
export default function createLoader() {
const localStorageEngine = createEngine('state', replacer);
const localStorageMiddleware = storage.createMiddleware(
localStorageEngine,
[],
[
actionTypes.HIDE_PROMO,
actionTypes.CHANGE_SETTINGS
]
);
const load = storage.createLoader(localStorageEngine);
return {
load,
localStorageMiddleware
};
}
PREDEFINED STATE
<?php
include './ReactJS.php';
$rjs = new ReactJS(
file_get_contents('build/react-bundle.js'),
file_get_contents('build/table.js')
);
$data =
array('data' => array(
array(1, 2, 3),
array(4, 5, 6),
array(7, 8, 9)
));
$rjs->setComponent('Table', $data);
?>
<html>
<head>
<title>React page</title>
</head>
<body>
<!-- render server content here -->
<div id="page"><?php echo $rjs->getMarkup(); ?></div>
<!-- load react and app code -->
<script src="react/build/react.min.js"></script>
<script src="build/table.js"></script>
<script><?php echo $rjs->getJS('#page', "GLOB"); ?></script>
</body>
</html>
SELECTORS
SVG
import React, { PropTypes } from 'react';
const Svg = ({ className = '', size, iconName }) => (
<svg className={`ui-icon ui-icon_size${size} ${className}`}>
<use xlinkHref={`img/icons.svg#icon-${iconName}`} />
</svg>
);
Svg.propTypes = {
iconName: PropTypes.string.isRequired,
size: PropTypes.string,
className: PropTypes.string
};
export default Svg;
ASSETS
const replace = require('replace');
function AssetsVersions(options) {
this.config = options;
}
AssetsVersions.prototype.apply = function resolve(compiler) {
const paths = {
index: this.config.indexPath,
css: this.config.cssPath,
js: this.config.jsPath
};
const patterns = {
cssJs: /(\.css(?!\/)|\.js(?!\/))(\?_t=\d+)?/ig,
img: /(\.png|\.jpg|\.jpeg|\.gif)(\?_t=\d+)?/ig,
replace: `$1?_t=${Math.floor(new Date().getTime() / 1000)}`
};
const files = [
{
path: paths.index,
pattern: patterns.cssJs
},
{
path: paths.css,
pattern: patterns.img
},
{
path: paths.js,
pattern: patterns.img
}
];
compiler.plugin('done', () => {
files.forEach(file => {
if (!file.path) {
return;
}
replace({
regex: file.pattern,
replacement: patterns.replace,
paths: [
file.path
],
recursive: false,
silent: false
});
});
});
};
module.exports = AssetsVersions;
SHARED STATE
INTEGRATION
OUTCOME
RESULT
REACTION
RECOGNITION
IMPACT
ENGINEERING DNA
React Radioactive
By Roman Stremedlovskyi
React Radioactive
Engineering is in our DNA in EPAM Systems
- 1,415