Add support for re-entrant SSR stacks#13181
Conversation
64aa1a2 to
aa039da
Compare
Details of bundled changes.Comparing: e79366d...503ddd8 react-dom
Generated by 🚫 dangerJS |
bvaughn
left a comment
There was a problem hiding this comment.
This looks okay to me. Left a minor question or two.
| ' in div (at **)\n' + | ||
| ' in App (at **)', | ||
| ]); | ||
| }); |
| getStackAddendum = function(): null | string { | ||
| if (currentDebugStack === null) { | ||
| const currentDebugFrame = currentDebugFrames[currentDebugFrames.length - 1]; | ||
| if (currentDebugFrame === null) { |
There was a problem hiding this comment.
Just curious, but when would this happen? I don't see anywhere we push null to this stack.
There was a problem hiding this comment.
Oops. Shouldn't happen, but I'll change it to test for array length instead. It's for exceptional case where we accidentally call getStackAddendum too late.
| function pushFrame(stack: Array<Frame>, frame: Frame): void { | ||
| if (__DEV__) { | ||
| const returnFrame = | ||
| stack.length > 0 ? ((stack[stack.length - 1]: any): FrameDev) : null; |
There was a problem hiding this comment.
Do we need this explicit DEV-only pointer? Isn't it always the frame at index - 1?
There was a problem hiding this comment.
Yes, but then we need to get a reference to the frame. I could do it — it's just very confusing to think about what currentDebugStacks would mean.
I guess my solution is also confusing.
There was a problem hiding this comment.
Not sure I follow. We only use the debugReturn attribute in getStackAddendum() where we just start at the top frame in currentDebugFrames and walk back. Seems like we could just do that with a for loop?
There was a problem hiding this comment.
No, currentDebugFrames is not the same thing as frames :-(
That's why this is so confusing. I rewrote and pushed a more explicit solution which doesn't try to hide that we have stacks of stacks of stacks. I hope that will clarify.
There was a problem hiding this comment.
Huh. The change you pushed to getStackAddendum in 5e3f51e looks like what I was suggesting 😅 but it looks like some commits are missing now so I can't step backwards....
| popCurrentDebugFrame(); | ||
| } | ||
| } else { | ||
| // Be careful! Make sure this matches the DEV path above. |
5e3f51e to
0f8577d
Compare
|
I rewrote the PR to be close to the original code, so this is just fixing the bug now. I added a bunch of comments to explain why it works this way and what it's doing exactly. |
2eb4060 to
39568bb
Compare
| if (currentDebugStacks.length === 1) { | ||
| // We are entering a server renderer. | ||
| // Remember the previous (e.g. client) global stack implementation. | ||
| prevGetCurrentStackImpl = ReactDebugCurrentFrame.getCurrentStack; |
There was a problem hiding this comment.
Huh... does this work? Do we never push more than once before popping?
There was a problem hiding this comment.
Hmm... or is this just assuming that all pushes before the pop will be for the same renderer and will have the same previous stack impl? I find this more confusing initially 😆
There was a problem hiding this comment.
It's assuming that once you're inside ReactDOMServer call, you're only going to make other ReactDOMServer calls (at most), but not, e.g., ReactDOMClient or ReactART calls. Because it's synchronous. I think it's a safe assumption in 99% cases.
All ReactDOMServer calls share the same implementation. Happy to talk through it via VC — I'm worried this is getting difficult to explain like this.
There was a problem hiding this comment.
Gotcha. Yeah, that's what I was thinking from reading it. Was just unsure if that would ever cause problems.
There was a problem hiding this comment.
In the worst case you'll end up with no stacks. Which is exactly what's happening today with any nesting. :-)
|
@gaearon @bvaughn |
|
Reentrancy is a general computing term:
In the context of this PR, a "reentrant" call can be seen in the tests Dan added- where e.g. |
|
@bvaughn Thank you Brian! But I had read that many times and seen questions on stackoverflow are talking about that. Almost all of them think it's about multithreading because the keyword 'interrupted' and 'safely', but in javascript it's nonexistent(except worker). The only thing I can imagine is that generator could be 'interrupted' and 'be called again', but React doesn't use generator, it use its own stack implementation(Fiber), but I don't know how to associate it with these keywords. BTW, in javascript, we should call it 'recursion' rather than 'Reentrancy'? (Searched on google about the different in javascript but don't get answer) |
Recursive functions must also be reentrant, but not all reentrancy is recursive. A function is recursive if it calls itself. For example: function foo(shouldRecurse) {
console.log('foo start');
if (shouldRecurse) {
foo(false);
}
console.log('foo stop');
}
foo(true);This is not recursion, but it is an example of reentrancy: function foo(fn) {
console.log('foo start');
fn();
console.log('foo stop');
}
function bar() {
console.log('bar');
foo(baz);
}
function baz() {
console.log('baz');
}
foo(bar); |
|
@bvaughn Thank you Brian, love it ! Before I think the second example is also a recursive function because the foo function call it self, even though it's indirect. But seems I'm wrong, we must call itself in the foo function body, can't be indirect |
Fixes #10188.
This is an alternative for #13158.
This embraces that we keep a stack of stacks of stacks, and explains why.