-
Notifications
You must be signed in to change notification settings - Fork 47.2k
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
React.Suspense provide a lifecycle so components can handle the display:none
removal
#14536
Comments
A new lifecycle doesn't feel like the right solution to me. In hooks implementations, the logic typically involved would be in It seems that this really needs to be treated more like state of the |
Perhaps someone on the React team has already put a lot of thought into this and knows what they plan to do and I'm just wasting brain cycles on it, but I want to follow-up on my earlier thoughts after some more consideration. I like the idea of using a context type built into React (e.g. React.SuspendedContext). This avoids needing a new hook or lifecycle to get at this information. Since this likely needs to be taken into account by any component that uses
I would actually prefer for the boolean to be the opposite of suspended since you would generally only care about doing something in the "not suspended" case, but I was having a hard time coming up with a variable name for the opposite that relates clearly to |
Can you please provide a minimal reproducing example? Without Material UI etc. Just React. |
(For future bug reports, that's generally a good idea since it lets avoid stalling on the issue when we're finally ready to look at it :-) |
What’s there to reproduce here? It’s not really a bug, but a feature request. If you want to animate a component for example there’s no way to know when it’s actually mounted and visible to the user to start the animation. If there’s a bug here ignore what I said, but in that case I’m wondering how this problem (in theory) would be solved with Suspense. |
A code sandbox demonstrating the problem you're running into (which necessitates the feature request) is what I'm asking for. |
@gaearon The code sandbox I provided in #14869 (https://codesandbox.io/s/40kw74nyk9) reproduces the problem without any other dependencies. |
I think this is only ever a problem if the component that didn't suspend tries to read its size. Since the I guess this was always a bug if you used these components inside a node that was initially Edit: Another codesandbox: https://codesandbox.io/s/qx4mv0q2vw. This is a bit constructed. For an actual example the material-ui tabs can be used. They display scroll buttons depending on scrollLeft or scrollRight. |
Thanks @ryancogswell. I think the problem is just that we unhide the parent node after inserting the children. If the order of DOM operations was swapped I think this would have worked. Does this sound right? |
Aren't the components that didn't suspend currently mounted immediately? In other words: Let A and B be children of Suspense. A suspends, B doesn't: B is immediately mounted but wrapped inside |
No. React never mounts trees in an inconsistent state. |
I think we're not talking about the same thing: <Suspense>
<SomethingThatSuspends />
<SomethingThatMountsImmediatelyAndNeedsLayout />
</Suspense> |
Actually, we do in sync mode. |
That actually fixes it. If I put everything in |
My bad. |
@sebmarkbage So in Concurrent Mode, if children within a Suspense have already been rendered and mounted and a state change occurs that causes a new lazy child to be introduced, will the existing children be unmounted and then remounted once the lazy child is loaded? If so, then it sounds like the behavior in Concurrent Mode would take care of this completely. In the React 16 Roadmap it indicates that Concurrent Mode will be opt-in. Is that just for React 16? Would it likely become the default for React 17? I'm just trying to figure out whether there is any need for an enhancement to address this other than the release of Concurrent Mode. If Concurrent Mode is going to remain opt-in beyond the next major version, then there may still be a need for an enhancement to address this issue when not using Concurrent Mode. It may also be sufficient to just add documentation for Suspense that warns about this specific limitation when using it without Concurrent Mode. It really depends on how you intend to support the "full" set of Suspense features (e.g. data fetching). Will the full Suspense features be available with and without Concurrent Mode (just with some differences in behavior)? Or will you have to opt-in to Concurrent Mode to use them? |
@ryancogswell For an update in concurrent mode, the existing children will be hidden without issuing any life-cycles and then once they unsuspend, they will be unhidden before the life-cycles fire. Adopting Concurrent Mode will be hard for a lot of people so it will remain opt-in for quite a while. |
I thought I was going to be able to still expose this as an issue in the update case in concurrent mode, but the timing of the effects in concurrent mode allows everything to work wonderfully! 🥇 Here's my modified sandbox using sync on the top and concurrent below with a "Toggle Header Size" button that demonstrates the issue for an update. Both the initial render and update demonstrate the issue in sync mode. Both work great in concurrent mode. Given that concurrent mode fully handles this, I don't think any separate enhancement is necessary, but given that many people won't be using concurrent mode any time soon, the documentation for React.lazy/Suspense should probably warn about this limitation when not using concurrent mode. |
It seems like the fix to this will be #15504. Note it requires that your tree is Strict Mode compliant. We can't fix this in legacy mode. |
I will be happy to give it a try once it's available for test. I will see if it fixes the bug reported on Material-UI side 😍. |
I think this is a bug. In non-lazy components, you can depend on render() producing something visible before What makes this worse is that |
Any update here?
Maybe it worth prevent Suspense child renders in progress of async request (using some props for |
Here is another use case that is really difficult right now: I'm building a chat UI that uses some lazy components in the "thread" view. I want the thread view to open to the most recent message at the bottom, but without an event to tell me when the thread is no longer suspended, I'm having to hack an event together with a MutationObserver. Is there a better way to do this? function App() {
...
return (
<Suspense>
<Thread />
</Suspense>
);
}
function Thread() {
let ref = useRef(null);
const scrollToBottom = useCallback(() => {
let current = ref.current;
if (current) current.scrollTop = current.scrollHeight;
}, []);
// Since the children of this component may or may not suspend, I have to handle both cases
useLayoutEffect(scrollToBottom, []); // children don't suspend
useCallbackOnUnsuspendEffect(ref, scrollToBottom); // children do suspend
return <div ref={ref}>...</div>;
}
function useCallbackOnUnsuspendEffect(ref, callback) {
return React.useEffect(() => {
let node = ref.current;
if (node == null) return;
const observer = new global.MutationObserver(mutations => {
if (mutations.find(m => m.attributeName === 'style')) {
callback();
}
});
observer.observe(node, { attributes: true });
return () => observer.disconnect();
}, [callback, ref]);
} This implementation is brittle for multiple reasons:
|
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contribution. |
Closing this issue after a prolonged period of inactivity. If this issue is still present in the latest release, please create a new issue with up-to-date information. Thank you! |
See my reply here: #18141 (comment). |
Had this same issue trying to trigger a transition on suspended component mount; ended up writing a wrapped import React, {
useEffect,
useState,
Suspense as ReactSuspense,
createContext,
useContext,
} from 'react';
import { node } from 'prop-types';
const SuspenseContext = createContext();
/*
* Suspense apparently renders a suspended tree before it resumes,
* hiding it with `display: none;`. This tweaked version lets child
* components use the `useSuspended` hook to know if they're in a
* rendered-while-suspended tree.
*/
export default function Suspense({ fallback, children }) {
const [suspended, setSuspended] = useState(true);
const Fallback = props => {
useEffect(() => {
setSuspended(true);
return () => setSuspended(false);
}, []);
return props.children;
};
return (
<ReactSuspense fallback={<Fallback>{fallback}</Fallback>}>
<SuspenseContext.Provider value={suspended}>
{children}
</SuspenseContext.Provider>
</ReactSuspense>
);
}
Suspense.propTypes = {
fallback: node,
children: node,
};
Suspense.defaultProps = {
fallback: null,
children: null,
};
export function useSuspended() {
return useContext(SuspenseContext);
} Then, you can do something like this: https://codesandbox.io/s/wild-feather-vynpq. Might need to be modified for situations with multiple nested |
As I understand from the conversation this is not fixable in sync mode (legacy mode), I would like to understand how is this fixed in concurrent mode? |
@gaurav5430 Trees are not mounted in an inconsistent state i.e. a sibling of a suspended component is simply not mounted instead of being mounted with |
Is there a way to use |
Please create a new issue and add some more details about what you're asking. It's not clear what behavior you want. |
Small update. We released 18 alpha for libraries authors to start testing. Notably, there is no concurrent "mode" anymore, instead you opt into features as you use them. So it's much easier to adopt. The new behavior is to delay effects from firing until the tree unsuspends, which solves a bunch of issues like in this thread. See mui/material-ui#14077 (comment) for a concrete example. |
React 18 is out with the change described in #14536 (comment). |
I hack an "myDomNode.style.setProperty = () => {};" within React 16.13 |
Do you want to request a feature or report a bug?
It's a feature.
What is the current behavior?
React.Suspense mounts its children with a
display: none
style if a promise is thrown. Once the thrown promise is resolved, React removes thedisplay: none
style.What is the expected behavior?
The children components have no easy way to know when the
display: none
style is removed by React. This is problematic when one child component needs to read from the DOM layout to correctly display its elements. Most people wait for thecomponentDidMount
callback to trigger, but because the element isdisplay: none
, it can't read any value from the DOM layout.The issue was discovered in mui/material-ui#14077. I believe that React should provide a lifecycle so the children components know when they are visible, that it's safe to do layout computations.
The best workaround I'm aware of it to use the Intersection Observer API but it requires a polyfill on IE 11 and Safari.
Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?
Version: 16.7.0-alpha.2
The text was updated successfully, but these errors were encountered: