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

Rewrite for React Hot Loader 3 #56

Merged
merged 6 commits into from
Mar 6, 2017
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,11 @@ deepForceUpdate(rootInstance);
* Replaces static getters and setters
* Replaces unbound static methods
* Replaces static properties unless they were overwritten by code
* Sets up `this.constructor` to match the most recent class

## Known Limitations

* Does not replace ES7 instance properties
* Does not replace bound static methods
* Replacing a method using [`autobind-decorator`](https://github.com/andreypopp/autobind-decorator) causes its identity to change

## Contributing
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-proxy",
"version": "2.0.8",
"version": "3.0.0-alpha.1",
"description": "Proxies React components without unmounting or losing their state.",
"main": "modules/index.js",
"scripts": {
Expand Down
103 changes: 74 additions & 29 deletions src/createClassProxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import supportsProtoAssignment from './supportsProtoAssignment';

const RESERVED_STATICS = [
'length',
'displayName',
'name',
'arguments',
'caller',
Expand All @@ -28,6 +29,13 @@ function isEqualDescriptor(a, b) {
return true;
}

function getDisplayName(Component) {
const displayName = Component.displayName || Component.name;
return (displayName && displayName !== 'ReactComponent') ?
displayName :
'Unknown';
}

// This was originally a WeakMap but we had issues with React Native:
// https://github.com/gaearon/react-proxy/issues/50#issuecomment-192928066
let allProxies = [];
Expand All @@ -49,23 +57,15 @@ function proxyClass(InitialComponent) {

let CurrentComponent;
let ProxyComponent;

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

function instantiate(factory, context, params) {
const component = factory();

try {
return component.apply(context, params);
} catch (err) {
// Native ES6 class instantiation
const instance = new component(...params);

Object.keys(instance).forEach(key => {
if (RESERVED_STATICS.indexOf(key) > -1) {
return;
Expand All @@ -75,10 +75,11 @@ function proxyClass(InitialComponent) {
}
}

let displayName = getDisplayName(InitialComponent);
try {
// Create a proxy constructor with matching name
ProxyComponent = new Function('factory', 'instantiate',
`return function ${InitialComponent.name || 'ProxyComponent'}() {
`return function ${displayName}() {
Copy link

@leidegre leidegre Apr 25, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gaearon this appears to cause an issue with Redux, it tries to evaluate Connect(ComponentName) as a function name and it results in a silent SyntaxError. It only shows up if you run with Pause On Caught Exceptions and no other error in console. Is it necessary to sanitize the function name?

There is a reasonable fallback here. No further action might be necessary.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah, we should sanitize it. Would you like to send a PR against 3.x branch?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll do what I can.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made an attempt here #58

return instantiate(factory, this, arguments);
}`
)(() => CurrentComponent, instantiate);
Expand All @@ -88,6 +89,11 @@ function proxyClass(InitialComponent) {
return instantiate(() => CurrentComponent, this, arguments);
};
}
try {
Object.defineProperty(ProxyComponent, 'name', {
value: displayName
});
} catch (err) { }

// Proxy toString() to the current constructor
ProxyComponent.toString = function toString() {
Expand All @@ -105,6 +111,9 @@ function proxyClass(InitialComponent) {
if (typeof NextComponent !== 'function') {
throw new Error('Expected a constructor.');
}
if (NextComponent === CurrentComponent) {
return;
}

// Prevent proxy cycles
var existingProxy = findProxy(NextComponent);
Expand All @@ -113,62 +122,98 @@ function proxyClass(InitialComponent) {
}

// Save the next constructor so we call it
const PreviousComponent = CurrentComponent;
CurrentComponent = NextComponent;

// Try to infer displayName
ProxyComponent.displayName = NextComponent.displayName || NextComponent.name;
displayName = getDisplayName(NextComponent);
ProxyComponent.displayName = displayName;
try {
Object.defineProperty(ProxyComponent, 'name', {
value: displayName
});
} catch (err) { }

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

// Copy static methods and properties
// Copy over static methods and properties added at runtime
if (PreviousComponent) {
Object.getOwnPropertyNames(PreviousComponent).forEach(key => {
if (RESERVED_STATICS.indexOf(key) > -1) {
return;
}

const prevDescriptor = Object.getOwnPropertyDescriptor(PreviousComponent, key);
const savedDescriptor = savedDescriptors[key];

if (!isEqualDescriptor(prevDescriptor, savedDescriptor)) {
Object.defineProperty(NextComponent, key, prevDescriptor);
}
});
}

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

const staticDescriptor = {
const prevDescriptor = PreviousComponent && Object.getOwnPropertyDescriptor(PreviousComponent, key);
const savedDescriptor = savedDescriptors[key];

// Skip redefined descriptors
if (prevDescriptor && savedDescriptor && !isEqualDescriptor(savedDescriptor, prevDescriptor)) {
Object.defineProperty(NextComponent, key, prevDescriptor);
Object.defineProperty(ProxyComponent, key, prevDescriptor);
return;
}

if (prevDescriptor && !savedDescriptor) {
Object.defineProperty(ProxyComponent, key, prevDescriptor);
return;
}

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

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

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

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

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

// Delete static unless user has redefined it at runtime
if (!wasStaticModifiedByUser(key)) {
delete ProxyComponent[key];
delete staticDescriptors[key];
const prevDescriptor = PreviousComponent && Object.getOwnPropertyDescriptor(PreviousComponent, key);
const savedDescriptor = savedDescriptors[key];

// Skip redefined descriptors
if (prevDescriptor && savedDescriptor && !isEqualDescriptor(savedDescriptor, prevDescriptor)) {
return;
}

delete ProxyComponent[key];
});

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

// Set up the constructor property so accessing the statics work
ProxyComponent.prototype.constructor = ProxyComponent;
ProxyComponent.prototype.constructor = NextComponent;

// We might have added new methods that need to be auto-bound
mountedInstances.forEach(bindAutoBindMethods);
Expand Down
5 changes: 5 additions & 0 deletions src/createPrototypeProxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ export default function createPrototypeProxy() {
// Copy properties of the original function, if any
assign(proxiedMethod, current[name]);
proxiedMethod.toString = proxyToString(name);
try {
Object.defineProperty(proxiedMethod, 'name', {
value: name
});
} catch (err) { }

return proxiedMethod;
}
Expand Down
Loading