All files / src/proxies StateProxy.jsx

100% Statements 30/30
100% Branches 12/12
100% Functions 9/9
100% Lines 30/30
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127          2x                     5x       4x 4x 4x       2x               4x       4x   3x 3x 1x 1x   2x   2x 1x           4x       2x       4x             3x   3x 2x         3x         4x             4x         4x       4x   4x               5x         5x                     5x    
import React from 'react';
import omit from 'lodash.omit';
import isEqual from 'lodash.isequal';
import ReactComponentTree from 'react-component-tree';
 
const defaults = {
  fixtureKey: 'state',
  // How often to read current state of preview component and report it up the
  // chain of proxies
  updateInterval: 500,
};
 
export default function createStateProxy(options) {
  const {
    fixtureKey,
    updateInterval,
  } = { ...defaults, ...options };
 
  class StateProxy extends React.Component {
    constructor(props) {
      super(props);
      this.onPreviewRender = this.onPreviewRender.bind(this);
      this.onStateUpdate = this.onStateUpdate.bind(this);
    }
 
    componentWillUnmount() {
      clearTimeout(this.timeoutId);
    }
 
    onPreviewRender(previewComponent) {
      const {
        fixture,
        onPreviewRef,
        disableLocalState,
      } = this.props;
 
      // Ref callbacks are also called on unmount with null value. We just need
      // to make sure to bubble up the unmount call in that case.
      if (previewComponent && !disableLocalState) {
        // Load initial state right after component renders
        const fixtureState = fixture[fixtureKey];
        if (fixtureState) {
          ReactComponentTree.injectState(previewComponent, fixtureState);
          this.scheduleStateUpdate();
        } else {
          const initialState = this.getStateTree(previewComponent);
          // No need to poll for state changes if entire component tree is stateless
          if (initialState) {
            this.updateState(initialState);
          }
        }
      }
 
      // Bubble up preview component ref callback
      onPreviewRef(this.previewComponent = previewComponent);
    }
 
    onStateUpdate() {
      this.updateState(this.getStateTree(this.previewComponent));
    }
 
    getStateTree(previewComponent) {
      return ReactComponentTree.serialize(previewComponent).state;
    }
 
    updateState(updatedState) {
      const {
        fixture,
        onFixtureUpdate,
      } = this.props;
 
      if (!isEqual(updatedState, fixture.state)) {
        onFixtureUpdate({
          state: updatedState,
        });
      }
 
      this.scheduleStateUpdate();
    }
 
    scheduleStateUpdate() {
      // TODO: Find a better way than polling to hook into state changes
      this.timeoutId = setTimeout(this.onStateUpdate, updateInterval);
    }
 
    render() {
      const {
        props,
        onPreviewRender,
      } = this;
      const {
        nextProxy,
        fixture,
        disableLocalState,
      } = props;
 
      // TODO: No longer omit when props will be read from fixture.props
      // https://github.com/skidding/react-cosmos/issues/217
      const childFixture = disableLocalState ? fixture : omit(fixture, 'state');
 
      return React.createElement(nextProxy.value, { ...props,
        nextProxy: nextProxy.next(),
        fixture: childFixture,
        onPreviewRef: onPreviewRender,
      });
    }
  }
 
  StateProxy.defaultProps = {
    // Parent proxies can enable this flag to disable this proxy
    disableLocalState: false,
  };
 
  StateProxy.propTypes = {
    nextProxy: React.PropTypes.shape({
      value: React.PropTypes.func,
      next: React.PropTypes.func,
    }).isRequired,
    fixture: React.PropTypes.object.isRequired,
    onPreviewRef: React.PropTypes.func.isRequired,
    onFixtureUpdate: React.PropTypes.func.isRequired,
    disableLocalState: React.PropTypes.bool,
  };
 
  return StateProxy;
}