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

Bug: confused with react 18 render twice #24425

Closed
mankeheaven opened this issue Apr 22, 2022 · 8 comments
Closed

Bug: confused with react 18 render twice #24425

mankeheaven opened this issue Apr 22, 2022 · 8 comments
Labels
Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug

Comments

@mankeheaven
Copy link

React version:18.0.0

with 18

import ReactDOM from "react-dom/client";
const root = ReactDOM.createRoot(
	document.getElementById("root") as HTMLElement
);
root.render(
	<React.StrictMode>
		<App />
	</React.StrictMode>
);

with 17

import ReactDOM from "react-dom";
ReactDOM.render(
  <React.StrictMode>
		<App />
	</React.StrictMode>,
	document.getElementById("root") as HTMLElement
);

and my app demo

import { useEffect } from "react";
import "./App.css";
import "xterm/css/xterm.css";

import { Terminal } from "xterm";
import { io } from "socket.io-client";
import { FitAddon } from "xterm-addon-fit";

const ids = {} as any;
function getTerminal(id: string) {
	if (!ids[id]) {
		ids[id] = new Terminal();
		return ids[id];
	} else {
		return ids[id];
	}
}

let chunk = "";

function App() {
	console.log("here");
	useEffect(() => {
		console.log("run effect");
		const term = getTerminal("t1") as Terminal;
		const el = document.getElementById("xterm-container") as HTMLDivElement;
		const socket = io("ws://localhost:3000");
		const fitAddon = new FitAddon();
		term.loadAddon(fitAddon);
		term.open(el);
		fitAddon.fit();

		window.addEventListener("resize", (s) => {
			fitAddon.fit();
		});

		term.write("welcome\r\n");

		socket.on("to", function (msg: string) {
			console.log("receive", msg);
			term.write(msg + "\r\n");
		});

		term.onData((arg1: string) => {
			console.log("data-", arg1, chunk);
			if (arg1.charCodeAt(0) === 13) {
				term.write("\r\n");
				console.log("data", arg1);
				if (!!chunk) {
					if (chunk === "clear") {
						term.clear();
						chunk = "";
					} else {
						socket.emit("chat message", chunk);
					}
				}
				chunk = "";
			} else if (arg1.charCodeAt(0) === 127 || arg1.charCodeAt(0) === 8) {
				chunk = chunk.slice(0, chunk.length - 1);
				console.log(chunk);
				term.write("\b \b");
			} else {
				term.write(arg1);
				chunk += arg1;
			}
		});

		// return () => {
		// 	term.dispose()
		// }
	}, []);

	return (
		<div className="App">
			<header className="App-header">terminal test</header>
			<div id="xterm-container"></div>
		</div>
	);
}

export default App;

image
image

code is ok with react17

I'm very confused, may I get your help?

@mankeheaven mankeheaven added the Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug label Apr 22, 2022
@mankeheaven
Copy link
Author

how to create a xterm once if render twice?

@Baribj
Copy link

Baribj commented Apr 22, 2022

React 18 renders your component twice in development mode. This is done to detect problems with purity. Your component has to be pure. You shouldn't change any variables that were created before your component.

See react docs: https://beta.reactjs.org/learn/keeping-components-pure

In your case, you defined chunk outside the scope of the component and changed it inside the component.

Remember, given the same input, react component should always produce the same exact output. In your code, this is not the case, as when the component re-renders, it could give different output depending on the current value of chunk.

So, by rendering your whole component twice, react makes impure issues easier to detect. Which is exactly what's happening in your code here.

Also, please don't forget to remove your event listener in the return statement of the useEffect. Otherwise, if the component is unmounted and remounted again, you will just keep adding event listeners causing performance issues down the line.

@mankeheaven
Copy link
Author

mankeheaven commented Apr 22, 2022

Get it. thanks, but it's difficult for most people to write absolutely pure components in a large application.

It's a strongly mental burden.

And can i have much more demos with your best practices or faq?

I think it's a long way to write pure components with most people.

I rewrite my code and I sure it's more rigorous, here it's my resolution, render twice but create one terminal with useRef.

import { useCallback, useEffect, useRef } from "react";
import "./App.css";
import "xterm/css/xterm.css";

import { Terminal } from "xterm";
import { io } from "socket.io-client";
import { FitAddon } from "xterm-addon-fit";

const ids = {} as any;
function getTerminal(id: string) {
	if (!ids[id]) {
		ids[id] = new Terminal();
		return ids[id];
	} else {
		return ids[id];
	}
}

const socket = io("ws://localhost:3000");

function useTerm() {
	const chunk = useRef("");
	const once = useRef(true);

	function createTerm(id: string) {
		if (!once.current) {
			return getTerminal(id) as Terminal;
		}
		once.current = false;
		const el = document.getElementById("xterm-container") as HTMLDivElement;
		const term = getTerminal(id) as Terminal;

		term.open(el);

		const fitAddon = new FitAddon();
		term.loadAddon(fitAddon);
		fitAddon.fit();

		window.addEventListener("resize", (s) => {
			fitAddon.fit();
		});

		term.write("welcome\r\n");

		socket.on("to", function (msg: string) {
			console.log("receive", msg);
			term.write(msg + "\r\n");
		});

		term.onData((arg1: string) => {
			console.log("data-", arg1, chunk.current);
			if (arg1.charCodeAt(0) === 13) {
				term.write("\r\n");
				console.log("data", arg1);
				if (!!chunk.current) {
					if (chunk.current === "clear") {
						term.clear();
						chunk.current = "";
					} else {
						socket.emit("chat message", chunk.current);
					}
				}
				chunk.current = "";
			} else if (arg1.charCodeAt(0) === 127 || arg1.charCodeAt(0) === 8) {
				chunk.current = chunk.current.slice(0, chunk.current.length - 1);
				console.log(chunk);
				term.write("\b \b");
			} else {
				term.write(arg1);
				chunk.current += arg1;
			}
		});
		return term;
	}

	const termCb = useCallback(createTerm, []);

	return {
		createTerm: termCb,
	};
}

function App() {
	console.log("here");

	const { createTerm } = useTerm();

	useEffect(() => {
		console.log("run effect");

		const term = createTerm("id1");
		console.log("term", term);
	}, [createTerm]);

	return (
		<div className="App">
			<header className="App-header">terminal test</header>
			<div id="xterm-container"></div>
		</div>
	);
}

export default App;


React 18 with strict mode is a good way to write pure component, I get a lot, thank you for your work

@mankeheaven
Copy link
Author

reactwg/react-18#19

reactwg/react-18#18

someone else can see this to solve your problem

@gaearon
Copy link
Collaborator

gaearon commented Apr 22, 2022

To be clear, this behavior only happens in development in Strict Mode. But we recommend to keep it on because it tests whether your component is resilient to remounting with existing state: https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html#updates-to-strict-mode

We plan to add a feature to React that would let a component preserve state between unmounts. So to verify that your component works with it, we mount it, unmount and immediately remount with existing state. The behavior you see is the behavior that would happen in this case. So cleaning up effects and making them symmetric is a good idea.

I am not sure the solution you wrote in #24425 (comment) is the best. I think you want to ensure there is a symmetry in your effects: whatever gets created, also gets destroyed in cleanup. And then the next time effect runs, the terminal is created again. So ideally what should happen is: (1) terminal is created, (2) that terminal is destroyed, (3) terminal is created again. And then it would not be visible because the first terminal was silently created and destroyed. This would simulate what should happen if the component remounts.

Indeed, reactwg/react-18#18 should provide more guidance.

@gaearon
Copy link
Collaborator

gaearon commented Jun 16, 2022

We wrote a new page documenting this in detail on the Beta website.

https://beta.reactjs.org/learn/synchronizing-with-effects#how-to-handle-the-effect-firing-twice-in-development

Hope this is helpful. Sorry some links from there are 404 because other pages are still being written.

@prathamesh-dukare
Copy link

Awesome Explanation , Issue is cleared

@immakdas
Copy link

For me this caused Firefox to crush the application in production mode. Chrome just fine

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug
Projects
None yet
Development

No branches or pull requests

5 participants