[GH-ISSUE #180] Reconciler rerendering unchanged elements? #115

Closed
opened 2026-05-05 11:44:43 -06:00 by gitea-mirror · 14 comments
Owner

Originally created by @mischnic on GitHub (Oct 7, 2018).
Original GitHub issue: https://github.com/kusti8/proton-native/issues/180

Hot reloading is nearly working (https://github.com/mischnic/proton-hot-cli), but there is an issue when reloading a Window component.
On macOS, the window position resets to the bottom left corner and on Windows, a new window is created on every update. This is because a new libui.UiWindow is created every time an update occurs (even though the props didn't change). A workaround is putting all component inside of the window into a new file, so that the component containing the Window never gets reloaded itself.

This is how it works internally:

import Example from "./app.js";

class MyApp extends Component {
	render() {
		return (
			<App>
				<Window title="Notes" size={{ w: 500, h: 350 }} margined>
					<Box padded>
						<Example/>
					</Box>
				</Window>
			</App>
		);
	}
}

render(<MyApp/>);

gets turned into


const _Example = require("./app.js");
const reactProxy = require("react-proxy");

const Example = function () {
	if (_Example && _Example.___component) {
		const proxy = _react_proxy.createProxy(_Example.___component);

		module.hot.accept(require.resolve("./app.js"), function () {
			// update proxy and forceUpdate
			const x = require("./app.js")["default"];
			const mountedInstances = proxy.update(x.___component);
			const forceUpdate = _react_proxy.getForceUpdate(React);
			mountedInstances.forEach(forceUpdate);
		});

		return proxy.get();
	} else {
		// not recognized as Component
		return _Example;
	}
}();

class MyApp extends Component {
	render() {
		return (
			<App>
				<Window title="Notes" size={{ w: 500, h: 350 }} margined>
					<Box padded>
						<Example/>
					</Box>
				</Window>
			</App>
		);
	}
}

(() => {
	class Wrapper extends React.Component {
		render() {
			return <HotApp/>;
		}

	}

	if (shouldUpdate) {
		const mountedInstances = module.hot.data.proxy.update(Wrapper);
		mountedInstances.forEach(i => i.forceUpdate());
	} else /* first run */{
		let proxy = reactProxy.createProxy(Wrapper);
		render(React.createElement(proxy.get()));
	}
})();
Originally created by @mischnic on GitHub (Oct 7, 2018). Original GitHub issue: https://github.com/kusti8/proton-native/issues/180 Hot reloading is nearly working (https://github.com/mischnic/proton-hot-cli), but there is an issue when reloading a `Window` component. On macOS, the window position resets to the bottom left corner and on Windows, a new window is created on every update. This is because a new `libui.UiWindow` is created every time an update occurs (even though the props didn't change). A workaround is putting all component inside of the window into a new file, so that the component containing the `Window` never gets reloaded itself. This is how it works internally: ```jsx import Example from "./app.js"; class MyApp extends Component { render() { return ( <App> <Window title="Notes" size={{ w: 500, h: 350 }} margined> <Box padded> <Example/> </Box> </Window> </App> ); } } render(<MyApp/>); ``` gets turned into ```jsx const _Example = require("./app.js"); const reactProxy = require("react-proxy"); const Example = function () { if (_Example && _Example.___component) { const proxy = _react_proxy.createProxy(_Example.___component); module.hot.accept(require.resolve("./app.js"), function () { // update proxy and forceUpdate const x = require("./app.js")["default"]; const mountedInstances = proxy.update(x.___component); const forceUpdate = _react_proxy.getForceUpdate(React); mountedInstances.forEach(forceUpdate); }); return proxy.get(); } else { // not recognized as Component return _Example; } }(); class MyApp extends Component { render() { return ( <App> <Window title="Notes" size={{ w: 500, h: 350 }} margined> <Box padded> <Example/> </Box> </Window> </App> ); } } (() => { class Wrapper extends React.Component { render() { return <HotApp/>; } } if (shouldUpdate) { const mountedInstances = module.hot.data.proxy.update(Wrapper); mountedInstances.forEach(i => i.forceUpdate()); } else /* first run */{ let proxy = reactProxy.createProxy(Wrapper); render(React.createElement(proxy.get())); } })(); ```
Author
Owner

@mischnic commented on GitHub (Oct 12, 2018):

I've reduce the code a more (full repo: https://github.com/mischnic/proton-reload-issue - just run npm start):

Changing the button text inside the MyApp component recreates the window, although only the button text changed.

import React, { Component } from "react";
import { App, Window, render, Box, Text, Button } from "proton-native";

class MyApp extends Component {
	constructor(props){
		super(props);
		this.state = {text: "t"};
	}

	render(){
		return <App>
			<Window>
				<Box>
					<Text>
						{this.state.text}
					</Text>
					<Button onClick={()=>this.setState({text: this.state.text+"!!!"})}>
						Do!
					</Button>
				</Box>
			</Window>
		</App>;
	}
}

// when this file is hot reloaded, it gets executed again

let e;
class Wrapper extends Component {
	constructor(props){
		super(props);
		this.state = {component: MyApp};
		e = this;
	}

	render(){
		const X = this.state.component;
		return <X/>;
	}
}

if (module.hot.data && module.hot.data.proxy) {
	// we are running an update
	module.hot.data.proxy.setState({component: MyApp});
} else {
	// first run
	render(<Wrapper/>);
}

// Please reload me
module.hot.accept();
// Make proxy available in updated module
module.hot.dispose(data => {
	data.proxy = e || (module.hot.data && module.hot.data.proxy);
});
<!-- gh-comment-id:429458048 --> @mischnic commented on GitHub (Oct 12, 2018): I've reduce the code a more (full repo: https://github.com/mischnic/proton-reload-issue - just run `npm start`): Changing the button text inside the MyApp component recreates the window, although only the button text changed. ```jsx import React, { Component } from "react"; import { App, Window, render, Box, Text, Button } from "proton-native"; class MyApp extends Component { constructor(props){ super(props); this.state = {text: "t"}; } render(){ return <App> <Window> <Box> <Text> {this.state.text} </Text> <Button onClick={()=>this.setState({text: this.state.text+"!!!"})}> Do! </Button> </Box> </Window> </App>; } } // when this file is hot reloaded, it gets executed again let e; class Wrapper extends Component { constructor(props){ super(props); this.state = {component: MyApp}; e = this; } render(){ const X = this.state.component; return <X/>; } } if (module.hot.data && module.hot.data.proxy) { // we are running an update module.hot.data.proxy.setState({component: MyApp}); } else { // first run render(<Wrapper/>); } // Please reload me module.hot.accept(); // Make proxy available in updated module module.hot.dispose(data => { data.proxy = e || (module.hot.data && module.hot.data.proxy); }); ```
Author
Owner

@mischnic commented on GitHub (Oct 13, 2018):

This works fine though (without the window inside MyApp):

class MyApp extends Component {
	constructor(props) {
		super(props);
		this.state = { text: "t" };
	}

	render() {
		return (
			<Box>
				<Text>{this.state.text}</Text>
				<Button
					onClick={() =>
						this.setState({ text: this.state.text + "!!!" })
					}
				>
					Do!
				</Button>
			</Box>
		);
	}
}

class Wrapper extends Component {
	constructor(props) {
		super(props);
		this.state = { component: MyApp };
	}

	render() {
		const X = this.state.component;
		return (
			<App>
				<Window margined>
					<X />
				</Window>
			</App>
		);
	}
}
<!-- gh-comment-id:429528092 --> @mischnic commented on GitHub (Oct 13, 2018): This works fine though (without the window inside MyApp): ```jsx class MyApp extends Component { constructor(props) { super(props); this.state = { text: "t" }; } render() { return ( <Box> <Text>{this.state.text}</Text> <Button onClick={() => this.setState({ text: this.state.text + "!!!" }) } > Do! </Button> </Box> ); } } class Wrapper extends Component { constructor(props) { super(props); this.state = { component: MyApp }; } render() { const X = this.state.component; return ( <App> <Window margined> <X /> </Window> </App> ); } } ```
Author
Owner

@kusti8 commented on GitHub (Oct 16, 2018):

Huh, interesting. It works fine for me on Linux. I don't have access to mac right now, but I'll try it out on Windows.

<!-- gh-comment-id:430414314 --> @kusti8 commented on GitHub (Oct 16, 2018): Huh, interesting. It works fine for me on Linux. I don't have access to mac right now, but I'll try it out on Windows.
Author
Owner

@mischnic commented on GitHub (Oct 17, 2018):

It works fine for me on Linux

Really? Ubuntu in VirtualBox (so behaves just like macOS - the window position resets):

oct-17-2018 22-16-12

<!-- gh-comment-id:430773529 --> @mischnic commented on GitHub (Oct 17, 2018): > It works fine for me on Linux Really? Ubuntu in VirtualBox (so behaves just like macOS - the window position resets): ![oct-17-2018 22-16-12](https://user-images.githubusercontent.com/4586894/47113902-8b414400-d25a-11e8-92a6-e4e8f04fc384.gif)
Author
Owner

@kusti8 commented on GitHub (Oct 18, 2018):

Yeah that's my bad. I was using a tiling WM so it appeared different. AFAIK, the reconciler looks right. What I think might be happening is that when the state changes, it sees it as an entirely new object, so it re-renders everything rather than diffing it. I don't know if this same thing happens with React/React-Native because there's no concept of windows there.

<!-- gh-comment-id:431167238 --> @kusti8 commented on GitHub (Oct 18, 2018): Yeah that's my bad. I was using a tiling WM so it appeared different. AFAIK, the reconciler looks right. What I think might be happening is that when the state changes, it sees it as an entirely new object, so it re-renders everything rather than diffing it. I don't know if this same thing happens with React/React-Native because there's no concept of windows there.
Author
Owner

@mischnic commented on GitHub (Oct 19, 2018):

What I think might be happening is that when the state changes, it sees it as an entirely new object, so it re-renders everything rather than diffing it. I don't know if this same thing happens with React/React-Native because there's no concept of windows there.

I'll have to check whether React removes and readds a html element in a situation like this...

<!-- gh-comment-id:431257276 --> @mischnic commented on GitHub (Oct 19, 2018): > What I think might be happening is that when the state changes, it sees it as an entirely new object, so it re-renders everything rather than diffing it. I don't know if this same thing happens with React/React-Native because there's no concept of windows there. I'll have to check whether React removes and readds a html element in a situation like this...
Author
Owner

@mischnic commented on GitHub (Oct 19, 2018):

I'll have to check whether React removes and readds a html element in a situation like this...

Yes, it does 😞.

class MyApp extends React.Component {
	render(){
		return <p>Hi!</p>;
	}
}

I guess it's because to React, it looks as if a completely different component is being rendered (because MyApp_old !== MyApp_new ).

<!-- gh-comment-id:431415656 --> @mischnic commented on GitHub (Oct 19, 2018): > I'll have to check whether React removes and readds a html element in a situation like this... Yes, it does 😞. ```jsx class MyApp extends React.Component { render(){ return <p>Hi!</p>; } } ``` I guess it's because to React, it looks as if a completely different component is being rendered (because `MyApp_old !== MyApp_new` ).
Author
Owner

@mischnic commented on GitHub (Oct 19, 2018):

it sees it as an entirely new object, so it re-renders everything rather than diffing it

But why even? A different component could render a very similar tree and diffing that would be more performant than just replacing.

<!-- gh-comment-id:431434337 --> @mischnic commented on GitHub (Oct 19, 2018): > it sees it as an entirely new object, so it re-renders everything rather than diffing it But why even? A different component could render a very similar tree and diffing that would be more performant than just replacing.
Author
Owner

@mischnic commented on GitHub (Oct 23, 2018):

Why does the diffing algorithm decide to remove and replace the b node?

const React = require('react');
const ReactDOM = require('react-dom');

class MyApp extends React.Component {
	render(){
		return <b>
			<p>Hi!!</p>
		</b>;
	}
}

class MyApp2 extends React.Component {
	render(){
		return <b>
			<p>Alternative</p>
		</b>;
	}
}

class Wrapper extends React.Component {
	constructor(props){
		super(props);
		this.state = {
			toggle: false
		}
	}

	render(){
		return <div>
			<button onClick={()=>this.setState({toggle: !this.state.toggle})}>
				Toggle!
			</button>
			{
				this.state.toggle ? <MyApp2/>  : <MyApp/>
			}
		</div>;
	}
}

ReactDOM.render(<Wrapper />, document.body.appendChild(document.createElement('div')));
<!-- gh-comment-id:432302340 --> @mischnic commented on GitHub (Oct 23, 2018): Why does the diffing algorithm decide to remove and replace the `b` node? ```jsx const React = require('react'); const ReactDOM = require('react-dom'); class MyApp extends React.Component { render(){ return <b> <p>Hi!!</p> </b>; } } class MyApp2 extends React.Component { render(){ return <b> <p>Alternative</p> </b>; } } class Wrapper extends React.Component { constructor(props){ super(props); this.state = { toggle: false } } render(){ return <div> <button onClick={()=>this.setState({toggle: !this.state.toggle})}> Toggle! </button> { this.state.toggle ? <MyApp2/> : <MyApp/> } </div>; } } ReactDOM.render(<Wrapper />, document.body.appendChild(document.createElement('div'))); ```
Author
Owner

@mischnic commented on GitHub (Oct 31, 2018):

https://reactjs.org/docs/reconciliation.html:

Whenever the root elements have different types, React will tear down the old tree and build the new tree from scratch.
The algorithm will not try to match subtrees of different component types. If you see yourself alternating between two component types with very similar output, you may want to make it the same type. In practice, we haven’t found this to be an issue.

Not sure how to circumvent this. Tricking React into thinking that the type didn't change....

<!-- gh-comment-id:434820965 --> @mischnic commented on GitHub (Oct 31, 2018): https://reactjs.org/docs/reconciliation.html: > Whenever the root elements have different types, React will tear down the old tree and build the new tree from scratch. > The algorithm will not try to match subtrees of different component types. If you see yourself alternating between two component types with very similar output, you may want to make it the same type. In practice, we haven’t found this to be an issue. Not sure how to circumvent this. Tricking React into thinking that the type didn't change....
Author
Owner

@mischnic commented on GitHub (Nov 3, 2018):

Dan Abramov on Twitter:

... Seems like a general problem with current hot reloading solutions for React (which is why we have class proxying hacks). Should get much easier with Hooks. I intend to work on fixing this

<!-- gh-comment-id:435613218 --> @mischnic commented on GitHub (Nov 3, 2018): Dan Abramov on Twitter: > ... Seems like a general problem with current hot reloading solutions for React (which is why we have class proxying hacks). Should get much easier with Hooks. I intend to work on fixing this
Author
Owner

@kusti8 commented on GitHub (Nov 3, 2018):

Cool. React made a blog post when they added hot reloading about how they did it and it was pretty convoluted. I can't find it right now.

<!-- gh-comment-id:435617021 --> @kusti8 commented on GitHub (Nov 3, 2018): Cool. React made a blog post when they added hot reloading about how they did it and it was pretty convoluted. I can't find it right now.
Author
Owner

@mischnic commented on GitHub (Nov 3, 2018):

I think I read most of these while developing proton-hot-cli. But there is one big issue: local state in reloaded components is always lost.

<!-- gh-comment-id:435617805 --> @mischnic commented on GitHub (Nov 3, 2018): I think I read most of these while developing proton-hot-cli. But there is one big issue: local state in reloaded components is always lost.
Author
Owner

@kusti8 commented on GitHub (Jan 19, 2020):

Proton Native V2 is now released! If the issue still occurs in the new update, please open a new issue.

<!-- gh-comment-id:576029403 --> @kusti8 commented on GitHub (Jan 19, 2020): Proton Native V2 is now released! If the issue still occurs in the new update, please open a new issue.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: github-starred/proton-native#115
No description provided.