Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 54 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,56 @@
# react-component-tree [![Build Status](https://travis-ci.org/skidding/react-component-tree.svg?branch=master)](https://travis-ci.org/skidding/react-component-tree) [![Coverage Status](https://coveralls.io/repos/skidding/react-component-tree/badge.svg?branch=master)](https://coveralls.io/r/skidding/react-component-tree?branch=master)
# React ComponentTree [![Build Status](https://travis-ci.org/skidding/react-component-tree.svg?branch=master)](https://travis-ci.org/skidding/react-component-tree) [![Coverage Status](https://coveralls.io/repos/skidding/react-component-tree/badge.svg?branch=master)](https://coveralls.io/r/skidding/react-component-tree?branch=master)

Serialize and reproduce the state of an entire tree of React components.

A few examples where this can be useful:
- Using fixtures to load and test components in multiple supported states
- Extracting the app state when an error occurs in the page and reproducing
that exact state later on when debugging
- "Pausing" the app state and resuming it later (nice for games)

## ComponentTree.serialize

Generate a snapshot with the props and state of a component combined, including
the state of all nested child components.

```js
var ComponentTree = require('react-component-tree');

myCompany.setProps({public: true});
myCompany.setState({profitable: true});
myCompany.refs.employee54.setState({bored: false});

var snapshot = ComponentTree.serialize(myCompany);
```

The snapshot looks like this:
```js
{
public: true,
state: {
profitable: true,
children: {
employee54: {
bored: false
}
}
},
}
```

## ComponentTree.render

Render a component and reproduce a state snapshot by recursively injecting the
nested state into the component tree it generates.

```js
var myOtherCompany = ComponentTree.render({
component: CompanyClass,
snapshot: snapshot,
container: document.getElementById('content')
});

console.log(myOtherCompany.props.public); // returns true
console.log(myOtherCompany.state.profitable); // returns true
console.log(myOtherCompany.refs.employee54.state.bored); // returns false
```
4 changes: 3 additions & 1 deletion entry.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
module.exports = {
Mixin: require('./src/load-child-mixin.js'),
Component: require('./src/load-child-component.js'),
serialize: require('./src/serialize.js').serialize
serialize: require('./src/serialize.js').serialize,
render: require('./src/render.js').render,
injectState: require('./src/render.js').injectState
};
44 changes: 44 additions & 0 deletions src/render.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
var _ = require('lodash'),
React = require('react');

exports.render = function(options) {
/**
* Render a component and reproduce a state snapshot by recursively injecting
* the nested state into the component tree it generates.
*
* @param {Object} options
* @param {ReactClass} options.component
* @param {Object} options.snapshot
* @param {DOMElement} options.container
*
* @returns {ReactComponent} Reference to the rendered component
*/
var props = _.omit(options.snapshot, 'state'),
state = options.snapshot.state;

var element = React.createElement(options.component, props),
component = React.render(element, options.container);

if (!_.isEmpty(state)) {
exports.injectState(component, state);
}

return component;
};

exports.injectState = function(component, state) {
var rootState = _.omit(state, 'children'),
childrenStates = state.children;

component.setState(rootState);

if (_.isEmpty(childrenStates)) {
return;
}

_.each(component.refs, function(child, ref) {
if (!_.isEmpty(childrenStates[ref])) {
exports.injectState(child, childrenStates[ref]);
}
});
};
6 changes: 5 additions & 1 deletion src/serialize.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ var _ = require('lodash');

exports.serialize = function(component) {
/**
* Generate a snapshot with the the props and state of a component combined,
* Generate a snapshot with the props and state of a component combined,
* including the state of all nested child components.
*
* @param {ReactComponent} component Rendered React component instance
*
* @returns {Object} Snapshot with component props and nested state
*/
var snapshot = _.clone(component.props),
state = getComponentTreeState(component);
Expand Down
87 changes: 87 additions & 0 deletions tests/render-inject-state.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
var React = require('react/addons'),
renderIntoDocument = React.addons.TestUtils.renderIntoDocument,
render = require('../src/render.js').render;

describe('Render and inject state', function() {
var component;

class ChildComponent extends React.Component {
render() {
return React.DOM.span();
}
}

class ParentComponent extends React.Component {
render() {
return React.createElement(ChildComponent, {ref: 'child'});
}
}

class GrandparentComponent extends React.Component {
render() {
return React.createElement(ParentComponent, {ref: 'child'});
}
}

beforeEach(function() {
component = renderIntoDocument(React.createElement(GrandparentComponent));

sinon.spy(component, 'setState');
sinon.spy(component.refs.child, 'setState');
sinon.spy(component.refs.child.refs.child, 'setState');

sinon.stub(React, 'render').returns(component);
});

afterEach(function() {
React.render.restore();
});

it('should set state on root component', function() {
render({
component: GrandparentComponent,
snapshot: {
state: {foo: 'bar'}
}
});

var stateSet = component.setState.lastCall.args[0];
expect(stateSet.foo).to.equal('bar');
});

it('should set state on child component', function() {
render({
component: GrandparentComponent,
snapshot: {
state: {
children: {
child: {foo: 'bar'}
}
}
}
});

var stateSet = component.refs.child.setState.lastCall.args[0];
expect(stateSet.foo).to.equal('bar');
});

it('should set state on grandchild component', function() {
render({
component: GrandparentComponent,
snapshot: {
state: {
children: {
child: {
children: {
child: {foo: 'bar'}
}
}
}
}
}
});

var stateSet = component.refs.child.refs.child.setState.lastCall.args[0];
expect(stateSet.foo).to.equal('bar');
});
});
68 changes: 68 additions & 0 deletions tests/render.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
var React = require('react'),
render = require('../src/render.js').render;

describe('Render', function() {
var domContainer;

class Component extends React.Component {
render() {
return React.DOM.span();
}
}

beforeEach(function() {
sinon.spy(React, 'createElement');
sinon.stub(React, 'render');

domContainer = document.createElement('div');
});

afterEach(function() {
React.createElement.restore();
React.render.restore();
});

it('should create element for component', function() {
render({
component: Component,
snapshot: {foo: 'bar'},
container: domContainer
});

var args = React.createElement.lastCall.args;
expect(args[0]).to.equal(Component);
});

it('should create element with props', function() {
render({
component: Component,
snapshot: {foo: 'bar'},
container: domContainer
});

var args = React.createElement.lastCall.args;
expect(args[1].foo).to.equal('bar');
});

it('should render created element', function() {
render({
component: Component,
snapshot: {foo: 'bar'},
container: domContainer
});

var args = React.render.lastCall.args;
expect(args[0]).to.equal(React.createElement.returnValues[0]);
});

it('should render in given container', function() {
render({
component: Component,
snapshot: {foo: 'bar'},
container: domContainer
});

var args = React.render.lastCall.args;
expect(args[1]).to.equal(domContainer);
});
});