Created
May 1, 2025 21:18
-
-
Save jamiebuilds/40efe2ec78276d150ffa1a66f4b0d7ed to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!doctype html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
<title>react-child-rendering-before-parent</title> | |
</head> | |
<body> | |
<div id="root"></div> | |
<script type="module" src="./index.jsx"></script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useSyncExternalStore } from "react"; | |
import { render } from "react-dom"; | |
class CounterStore { | |
#counter = 0; | |
#listeners = new Set(); | |
getSnapshot = () => { | |
return this.#counter; | |
}; | |
subscribe = (listener) => { | |
this.#listeners.add(listener); | |
return () => this.#listeners.delete(listener); | |
}; | |
increment = () => { | |
this.#counter += 1; | |
this.#listeners.forEach((listener) => listener()); | |
}; | |
} | |
const counterStore = new CounterStore(); | |
function shouldChildRender(counter) { | |
return counter % 2 === 0; | |
} | |
function Child() { | |
const value = useSyncExternalStore(counterStore.subscribe, counterStore.getSnapshot); | |
console.log(`child render ${value}`); | |
if (!shouldChildRender(value)) { | |
throw new Error("child should not have rendered"); | |
} | |
return <div>child: {value}</div>; | |
} | |
function Parent() { | |
const counter = useSyncExternalStore(counterStore.subscribe, counterStore.getSnapshot); | |
console.log(`parent render ${counter}`); | |
function onClick() { | |
// setTimeout breaks out of the automatic batching inside onClick and | |
// causes <Child> to be re-rendered before <Parent> (which is a bug) | |
// and then <Child> throws an error when shouldChildRender() returns false. | |
setTimeout(() => { | |
// Note: Wrapping this with `unstable_batchedUpdates` fixes the issue. | |
counterStore.increment(); | |
}, 1); | |
} | |
return ( | |
<> | |
<button onClick={onClick}>Dispatch</button> | |
<div>parent: {counter}</div> | |
{shouldChildRender(counter) && <Child />} | |
</> | |
); | |
} | |
render(<Parent />, document.getElementById("root")); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"private": true, | |
"name": "react-child-rendering-before-parent", | |
"version": "0.0.0", | |
"scripts": { | |
"start": "parcel index.html" | |
}, | |
"dependencies": { | |
"react": "^18.3.1", | |
"react-dom": "^18.3.1" | |
}, | |
"devDependencies": { | |
"parcel": "^2.14.4" | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment