Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support React 0.14 #31

Merged
merged 1 commit into from
Oct 19, 2015
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
33 changes: 19 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
# React Proxy [![build status](https://img.shields.io/travis/gaearon/react-proxy/master.svg?style=flat-square)](https://travis-ci.org/gaearon/react-proxy) [![npm version](https://img.shields.io/npm/v/react-proxy.svg?style=flat-square)](https://www.npmjs.com/package/react-proxy)

A generic React component proxy used as the new engine by React Hot Loader.
A generic React component proxy useful for hot reloading.

## Requirements

* React 0.13+
* React 0.14+

## Usage

Intended to be used from hot reloading tools like React Hot Loader.
If you’re an application developer, it’s unlikely you’ll want to use it directly.

You will need something like [react-deep-force-update](https://github.com/gaearon/react-deep-force-update) to re-render the component tree after applying the update.

```js
import React, { Component } from 'react';

Expand Down Expand Up @@ -40,7 +42,10 @@ React.render(<ComponentVersion2 />, rootEl);
With React Proxy:

```js
import { createProxy, getForceUpdate } from 'react-proxy';
import React from 'react';
import { render } from 'react-dom';
import createProxy from 'react-proxy';
import deepForceUpdate from 'react-deep-force-update';

// Create a proxy object, given the initial React component class.
const proxy = createProxy(ComponentVersion1);
Expand All @@ -50,26 +55,26 @@ const proxy = createProxy(ComponentVersion1);
const Proxy = proxy.get();

// Render the component (proxy, really).
React.render(<Proxy />, rootEl);
const rootInstance = render(<Proxy />, rootEl);

// Point the proxy to the new React component class by calling update().
// Instances will stay mounted and their state will be intact, but their methods will be updated.
// The update() method returns an array of mounted instances so we can do something with them.
const mountedInstances = proxy.update(ComponentVersion2);

// React Proxy also provides us with getForceUpdate() method that works even if the component
// instance doesn't descend from React.Component, and doesn't have a forceUpdate() method.
const forceUpdate = getForceUpdate(React);
proxy.update(ComponentVersion2);

// Force-update all the affected instances!
mountedInstances.forEach(forceUpdate);
// Force-update the whole React component tree.
// Until React provides an official DevTools API to do this,
// you should keep the reference to the root instance(s).
deepForceUpdate(rootInstance);
```

## React Native

This will work with React Native when [facebook/react-native#2985](https://github.com/facebook/react-native/issues/2985) lands.
For now, you can keep using 1.x.

## Features

* Supports both classic (`React.createClass()`) and modern (ES6 classes) style
* Supports classes that don’t descend from `React.Component`
* Supports classes with strict `shouldComponentUpdate`
* Supports inherited and base classes (although you shouldn’t use inheritance with React)
* Supports classic `createClass()` autobinding and modern [`autobind-decorator`](https://github.com/andreypopp/autobind-decorator)
* Contains an extensive test suite to avoid regressions
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@
"babel-loader": "^5.0.0",
"expect": "^1.9.0",
"mocha": "^2.2.4",
"react": "^0.13.2",
"react": "^0.14.0",
"react-addons-test-utils": "^0.14.0",
"rimraf": "^2.4.2",
"webpack": "1.4.8"
},
"dependencies": {
"lodash": "^3.7.0",
"react-deep-force-update": "^1.0.0"
"lodash": "^3.7.0"
}
}
96 changes: 49 additions & 47 deletions src/createClassProxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,130 +26,132 @@ function isEqualDescriptor(a, b) {
return true;
}

export default function proxyClass(InitialClass) {
export default function proxyClass(InitialComponent) {
// Prevent double wrapping.
// Given a proxy class, return the existing proxy managing it.
if (Object.prototype.hasOwnProperty.call(InitialClass, '__reactPatchProxy')) {
return InitialClass.__reactPatchProxy;
if (Object.prototype.hasOwnProperty.call(InitialComponent, '__reactPatchProxy')) {
return InitialComponent.__reactPatchProxy;
}

const prototypeProxy = createPrototypeProxy();
let CurrentClass;
let CurrentComponent;
let ProxyComponent;

let staticDescriptors = {};
function wasStaticModifiedByUser(key) {
// Compare the descriptor with the one we previously set ourselves.
const currentDescriptor = Object.getOwnPropertyDescriptor(ProxyClass, key);
const currentDescriptor = Object.getOwnPropertyDescriptor(ProxyComponent, key);
return !isEqualDescriptor(staticDescriptors[key], currentDescriptor);
}

let ProxyClass;
try {
// Create a proxy constructor with matching name
ProxyClass = new Function('getCurrentClass',
`return function ${InitialClass.name || 'ProxyClass'}() {
return getCurrentClass().apply(this, arguments);
ProxyComponent = new Function('getCurrentComponent',
`return function ${InitialComponent.name || 'ProxyComponent'}() {
return getCurrentComponent().apply(this, arguments);
}`
)(() => CurrentClass);
)(() => CurrentComponent);
} catch (err) {
// Some environments may forbid dynamic evaluation
ProxyClass = function () {
return CurrentClass.apply(this, arguments);
ProxyComponent = function () {
return CurrentComponent.apply(this, arguments);
};
}

// Point proxy constructor to the proxy prototype
ProxyClass.prototype = prototypeProxy.get();

// Proxy toString() to the current constructor
ProxyClass.toString = function toString() {
return CurrentClass.toString();
ProxyComponent.toString = function toString() {
return CurrentComponent.toString();
};

function update(NextClass) {
if (typeof NextClass !== 'function') {
let prototypeProxy;
if (InitialComponent.prototype && InitialComponent.prototype.isReactComponent) {
// Point proxy constructor to the proxy prototype
prototypeProxy = createPrototypeProxy();
ProxyComponent.prototype = prototypeProxy.get();
}

function update(NextComponent) {
if (typeof NextComponent !== 'function') {
throw new Error('Expected a constructor.');
}

// Prevent proxy cycles
if (Object.prototype.hasOwnProperty.call(NextClass, '__reactPatchProxy')) {
return update(NextClass.__reactPatchProxy.__getCurrent());
if (Object.prototype.hasOwnProperty.call(NextComponent, '__reactPatchProxy')) {
return update(NextComponent.__reactPatchProxy.__getCurrent());
}

// Save the next constructor so we call it
CurrentClass = NextClass;

// Update the prototype proxy with new methods
const mountedInstances = prototypeProxy.update(NextClass.prototype);
CurrentComponent = NextComponent;

// Set up the constructor property so accessing the statics work
ProxyClass.prototype.constructor = ProxyClass;
// Try to infer displayName
ProxyComponent.displayName = NextComponent.displayName || NextComponent.name;

// Set up the same prototype for inherited statics
ProxyClass.__proto__ = NextClass.__proto__;
ProxyComponent.__proto__ = NextComponent.__proto__;

// Copy static methods and properties
Object.getOwnPropertyNames(NextClass).forEach(key => {
Object.getOwnPropertyNames(NextComponent).forEach(key => {
if (RESERVED_STATICS.indexOf(key) > -1) {
return;
}

const staticDescriptor = {
...Object.getOwnPropertyDescriptor(NextClass, key),
...Object.getOwnPropertyDescriptor(NextComponent, key),
configurable: true
};

// Copy static unless user has redefined it at runtime
if (!wasStaticModifiedByUser(key)) {
Object.defineProperty(ProxyClass, key, staticDescriptor);
Object.defineProperty(ProxyComponent, key, staticDescriptor);
staticDescriptors[key] = staticDescriptor;
}
});

// Remove old static methods and properties
Object.getOwnPropertyNames(ProxyClass).forEach(key => {
Object.getOwnPropertyNames(ProxyComponent).forEach(key => {
if (RESERVED_STATICS.indexOf(key) > -1) {
return;
}

// Skip statics that exist on the next class
if (NextClass.hasOwnProperty(key)) {
if (NextComponent.hasOwnProperty(key)) {
return;
}

// Skip non-configurable statics
const descriptor = Object.getOwnPropertyDescriptor(ProxyClass, key);
const descriptor = Object.getOwnPropertyDescriptor(ProxyComponent, key);
if (descriptor && !descriptor.configurable) {
return;
}

// Delete static unless user has redefined it at runtime
if (!wasStaticModifiedByUser(key)) {
delete ProxyClass[key];
delete ProxyComponent[key];
delete staticDescriptors[key];
}
});

// Try to infer displayName
ProxyClass.displayName = NextClass.displayName || NextClass.name;
if (prototypeProxy) {
// Update the prototype proxy with new methods
const mountedInstances = prototypeProxy.update(NextComponent.prototype);

// We might have added new methods that need to be auto-bound
mountedInstances.forEach(bindAutoBindMethods);
mountedInstances.forEach(deleteUnknownAutoBindMethods);
// Set up the constructor property so accessing the statics work
ProxyComponent.prototype.constructor = ProxyComponent;

// Let the user take care of redrawing
return mountedInstances;
// We might have added new methods that need to be auto-bound
mountedInstances.forEach(bindAutoBindMethods);
mountedInstances.forEach(deleteUnknownAutoBindMethods);
}
};

function get() {
return ProxyClass;
return ProxyComponent;
}

function getCurrent() {
return CurrentClass;
return CurrentComponent;
}

update(InitialClass);
update(InitialComponent);

const proxy = { get, update };

Expand All @@ -160,7 +162,7 @@ export default function proxyClass(InitialClass) {
value: getCurrent
});

Object.defineProperty(ProxyClass, '__reactPatchProxy', {
Object.defineProperty(ProxyComponent, '__reactPatchProxy', {
configurable: false,
writable: false,
enumerable: false,
Expand Down
3 changes: 1 addition & 2 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export { default as createProxy } from './createClassProxy';
export { default as getForceUpdate } from 'react-deep-force-update';
export default from './createClassProxy';
32 changes: 5 additions & 27 deletions test/consistency.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React, { Component } from 'react';
import createShallowRenderer from './helpers/createShallowRenderer';
import expect from 'expect';
import { createProxy } from '../src';
import createProxy from '../src';

const fixtures = {
modern: {
Bar: class Bar {
Bar: class Bar extends Component {
componentWillUnmount() {
this.didUnmount = true;
}
Expand All @@ -18,7 +18,7 @@ const fixtures = {
}
},

Baz: class Baz {
Baz: class Baz extends Component {
componentWillUnmount() {
this.didUnmount = true;
}
Expand All @@ -28,7 +28,7 @@ const fixtures = {
}
},

Foo: class Foo {
Foo: class Foo extends Component {
static displayName = 'Foo (Custom)';

componentWillUnmount() {
Expand Down Expand Up @@ -85,7 +85,7 @@ describe('consistency', () => {

beforeEach(() => {
renderer = createShallowRenderer();
warnSpy = expect.spyOn(console, 'warn').andCallThrough();
warnSpy = expect.spyOn(console, 'error').andCallThrough();
});

afterEach(() => {
Expand Down Expand Up @@ -210,28 +210,6 @@ describe('consistency', () => {
});
});

describe('classic only', () => {
const { Bar, Baz } = fixtures.classic;

it('sets up legacy type property', () => {
let proxy = createProxy(Bar);
const Proxy = proxy.get();
const barInstance = renderer.render(<Proxy />);

warnSpy.destroy();
const localWarnSpy = expect.spyOn(console, 'warn');
expect(barInstance.constructor.type).toEqual(Proxy);

proxy.update(Baz);
const BazProxy = proxy.get();
expect(Proxy).toEqual(BazProxy);
expect(barInstance.constructor.type).toEqual(BazProxy);

expect(localWarnSpy.calls.length).toBe(1);
localWarnSpy.destroy();
});
});

describe('modern only', () => {
const { Bar, Baz } = fixtures.modern;

Expand Down
Loading