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