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

The Reconciler #711

Merged
merged 11 commits into from
Dec 21, 2017
Merged

The Reconciler #711

merged 11 commits into from
Dec 21, 2017

Conversation

theKashey
Copy link
Collaborator

This implements idea from #703
Fixes: #650 (HoC), #666 (HoC), and any other HoC or decorator issues.

Idea - instead of relying on registering the variables - relay on react-tree.

Algorithm is simple

  • use something like react-deep-force-update to hydrate the render tree
  • render the tree back, swapping the old components, and the new ones on the fly.

Results
The good one - ANY component could be Hot-Reloaded. You can use complex HOC or decorators and still have RHL working🏆
The bad one - all components should be wrapped by proxy. This actually does not affect performance at all, as long stand-in operates on pure js prototypical inheritance.

To activate reconciler - add reconciler prop to AppContainer.

PS: Not to be merged yet.

@theKashey theKashey requested a review from gregberge December 6, 2017 13:40
let firstInstances
let knownSignatures
let didUpdateProxy
import {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

patch.dev was completely rewritten. Things like error reporting about failed registrations or failed replacement were removed, as long they are obsolete with reconciler.

@gregberge
Copy link
Collaborator

Very good work! The idea is great! I think you have resolved the major issue of this project 😄.

export default class ComponentMap {
constructor(useWeakMap) {
if (useWeakMap) {
this.wm = new WeakMap()
Copy link
Collaborator

Choose a reason for hiding this comment

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

We could rely on WeakMap since we are targetting only modern browsers.

@@ -2,16 +2,20 @@ import React, { Component } from 'react'
import PropTypes from 'prop-types'
import deepForceUpdate from 'react-deep-force-update'
import { getGeneration } from './updateCounter'
import hydrate from './reconciler/traverse'
Copy link
Collaborator

Choose a reason for hiding this comment

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

hydrate / traverse

}
}

const deepForceTraverse = (instance, stack) => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

The goal of this function is to traverse a tree (stack) and swap all components is it right? I think the naming is not good.

}
}

export default function hydrate(instance) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

The goal is to traverse React tree and build an AST of it in order to traverse it and swap all swappable components, yes?

@theKashey
Copy link
Collaborator Author

Ideally this should be moved into react-mount, as a user defined class comparison function. And, as result we could even remove proxy, as long there will be no need to fake types.
A single line change in react internals can make things must simpler.
How to sell this idea to the React team?

@gregberge
Copy link
Collaborator

@gaearon can help us!

@gregberge
Copy link
Collaborator

@theKashey we can't remove proxy because we have to keep state and variables. So even if we have a custom comparison function proxies are still needed.

@gaearon
Copy link
Owner

gaearon commented Dec 6, 2017

Please avoid relying on React internal APIs if you can. We don't want to introduce more dependencies than already exist in deep-force-update.

I intend to work on officially supporting hot reloading in React in the beginning of next year. So it's best to coordinate our efforts.

@theKashey
Copy link
Collaborator Author

@gaearon - does this mean, that we should not proceed with this PR, as long it uses some React internals, or we could proceed, as long it uses the same internals as deep-force-update?

@gaearon
Copy link
Owner

gaearon commented Dec 6, 2017

If it uses the same exact internals it’s okay.

@theKashey
Copy link
Collaborator Author

All react internals were move into a single (and quite small) file. Also refactored logic a bit (just perform dead code elimination).

@theKashey
Copy link
Collaborator Author

@neoziro - just added a couple of tests. Coverage is not great, but enough for a start.
It is much more important to test this solution against some real edge cases, I am not quite aware of.

Waive if you are keen to merge this to the lerna branch.

@gregberge
Copy link
Collaborator

@theKashey Thanks! I could take a look on tuesday, not before.

@gaearon
Copy link
Owner

gaearon commented Dec 14, 2017

I want to note that I still don't really understand what this is doing, so I'd appreciate a more in-depth explanation of why this works, why it is better than the existing approach, and what are the potential downsides and things that could break.

That said I don't want to block you on my understanding :-) If you're sure this is solid and give it a lot of testing in large codebases (I'm sure plenty of people would be happy to volunteer trying an alpha) then let's roll with it.

@theKashey
Copy link
Collaborator Author

Ok, let me explain.

The biggest constraint of V3 is a literal unusability for lots of people. All these folks who use decorators, recompose, and quite happy about it - they are all off the boat.

V1 injects update code on the each-file-basic, thus allows it to perform point updates, and thus allows it to work with a complex code. But actually, it does not respect any complete code dependencies - module self-accept is a dangerous thing.

V3 performs the work much more gently, rendering from top to bottom, relying on registered classes to perform updates. This is the power, this is a weakness. It is just impossible to register everything, but you have to.

Reconciler is just another way to detect changes.
The idea is simply - you have a rendered React Three, and you are updating a single class. And you want only a single component to be updated (as V1 do).

Solution:

  1. (hydrate)Traverse the existing tree, to store component type positions. Like - I know that Button was inside Form, inside Page12. This part actually borrows code from react-deep-force-update.
  2. (hotReplacementRender)Traverse the new tree, to spot the changes to update proxies. I this part I have to explain in deep.

Reconsiler:

  1. To make reconciler works RHL have to wrap all components with proxy. In real - not all, but all stateless, and all classes with injected "REGENERATE_METHOD". As long standin-proxy did not affect performance - we can do it.
  2. Call render method on real component instance, obtained from hydrate.
  3. Traverse the result, comparing component types.
    3.1 In case of DOM node perform monkey patching (mergeInject) combining type from rendered child, and children from hydrated one.
    3.2 In case of function compare the new and the old one
    3.2.1 If types are equal - continue render
    3.2.1 If types are equal from proxy point of view (registed) - continue render
    3.2.1 If types are swappable - update proxy and continue render.

And swappable is the key of this solution.
In v3 swappable means that types have been exported from a same file and same name.
Here - it does mean - types were found in the same tree position, have the same display name and also some similarity in code.

similarity - full-text similarity, or 20% by Levenshtein distance (fuzzyCompare, up to O(n^2) ). For classes text similarity are tested on method level, allowing you add or remove one method, and change another. (has no effect on the registered types).

And there is a small trick here - reconsiler does not respect a possible change in props. It does not render the true react tree. It tries to re-render application as it was, to spot and swap changes.
If you are:

  • rendering a different component – it will don't enter that leaf
  • rendering component in a different order - it will swap types, and render them in the old order
  • rendering with different props, resulting a different nested tree - it will update as many proxies as it could, and the nested tree will be completely updated in any case.

So, this is not a real render, this is just another way to rehydrate hot updated code.
It is still important to have a registered types, to perform major updates upon them, but now RHL can stand any decorator or function composition between these registered types.
Hooray?

@theKashey theKashey added this to the v4 milestone Dec 18, 2017
Copy link
Collaborator

@gregberge gregberge left a comment

Choose a reason for hiding this comment

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

What is the behaviour if I remove an hoc:

// from
compose(one, two, three)(Component)
// to
compose(one, three)(Component)

@@ -99,6 +112,7 @@ AppContainer.propTypes = {
},
errorReporter: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
warnings: PropTypes.bool,
reconciler: PropTypes.bool,
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we should always activate reconciler. Why making it optional?

@@ -99,6 +112,7 @@ AppContainer.propTypes = {
},
errorReporter: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
warnings: PropTypes.bool,
Copy link
Collaborator

Choose a reason for hiding this comment

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

So we can drop this option isn't it?

@theKashey
Copy link
Collaborator Author

RE:

// from
compose(one, two, three)(Component)
// to
compose(one, three)(Component)

Reconciler will do it's best, and update all the proxies. But react-dom will not stand a tree change.
WE could invent a way, to rehydrate the old instances into a new components, but I'll prefer not to hack React (more deeply).

@theKashey theKashey merged commit 150f1d2 into gaearon:lerna Dec 21, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants