Do you want to request a feature or report a bug?
Feature
What is the current behavior?
I have played a bit with Concurrent Mode and the Suspense API.
Really exiting features and I look forward to use them in a stable release. Thank you for everything you are doing!
Regarding the Suspense component, could it be nice to have a property (both in Concurrent Mode and in "normal/synchronous" mode) which would allow us to set the minimum duration of the Suspense fallback UI in case the fallback UI ever gets rendered?
What is the expected behavior?
Let me do an example. Try clicking on the Next button in this codesandbox:
https://codesandbox.io/s/cold-monad-ifr29.
You will see that the Suspense fallback UI is rendered and stays in the tree just for a little moment (~200ms) because both promises resolve in 1200ms, while useTransition has a timeoutMs of 1 second.
In my opinion, this is a bit unpleasant to the eye.
Wouldn't it be nicer if we could tell the Suspense component something like "If you ever render the fallback, show it for at least N millisec."? E.g.:
...
function ProfilePage({ resource }) {
return (
<Suspense fallback={<h1>Loading profile...</h1>}
// If the fallback ever gets rendered,
// it will be shown for at least 1500 millisec.,
// even if the promise resolves right after rendering the fallback.
fallbackMinDurationMs={1500}>
<ProfileDetails resource={resource} />
<Suspense fallback={<h1>Loading posts...</h1>}>
<ProfileTimeline resource={resource} />
</Suspense>
</Suspense>
);
}
...
Consider an animated spinner used as a fallback of Suspense, if it happens that the promise resolves just a few milliseconds after rendering the fallback like above, the spinner will be rendered and suddenly disappear, without completing its animation cycle and showing an incomplete animation.
Whereas, if we could keep the spinner in the tree for at least fallbackMinDurationMs millisec. once rendered, we could improve its appearance in such cases.
The Suspense component responsible for rendering the fallback would have to wrap the caught Promise in a promise which would look something like this:
function maxDelayFallbackPromise({
promise,
timeoutMs, // ---> This would be the value of `useTransition`'s `timeoutMs`
onFallback = () => {}, // ---> This code would run in case `timeoutMs` exceeds (i.e. when `Suspense`'s fallback UI is rendered)
fallbackMinDurationMs
} = {}) {
// Generate a unique identifier, like a string, a number, in order to identify which promise resolves first...
const uniqueIdentifier = `promise_value_${Math.random()}`
return Promise.race([
promise,
timeout(timeoutMs).then(() => uniqueIdentifier)
]).then(value => {
if (value === uniqueIdentifier) {
onFallback()
return minDelayPromise(promise, fallbackMinDurationMs)
}
else {
return value
}
})
}
Where timeout and minDelayPromise are:
function timeout(delayMs) {
return new Promise(resolve => setTimeout(resolve, delayMs))
}
function minDelayPromise(promise, minDelay) {
return Promise.all([
promise,
timeout(minDelay)
]).then(([value]) => {
return value
})
}
This could also apply to the isPending flag of useTransition...
Do you think such a feature could improve the UX in such cases?
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.
Ping to unmark as stale.
I think there's a few undocumented useTransition options that do what you want, but I'm not sure if we're gonna replace those with something else.
@gaearon Thank you for the feedback, Dan. I look forward to having the possibility to use those options when the final Suspense API is finally released.
Please don't close it, it's worth tracking.
@gaearon OK :)
We can manage that in promise fetch function
I think this can do it
function delayPromise(promise, delay) {
return new Promise((resolve, reject) => {
let status = 'pending';
let result;
promise
.then((_result) => {
if (timeout) {
result = _result;
status = 'success';
return;
}
resolve(_result);
})
.catch((_result) => {
if (timeout) {
result = _result;
status = 'error';
return;
}
reject(_result);
});
let timeout = setTimeout(() => {
if (status === 'success') {
resolve(result);
} else if (status === 'error') {
reject(result);
}
timeout = null;
}, delay);
});
}
and use that like this
let postsPromise = delayPromise(fetchPosts(), 1500);
const resource = wrapPromise(postsPromise);
@hosseinmd Haven't tried it yet, but I don't think your code achieves the same behaviour.
You are starting the timeout concurrently as soon as you create the Promise responsible for fetching the data, whereas in my example the timeout gets created only as soon as the Suspense fallback UI is rendered, it won't be even be created if the promise resolves before the timeoutMs of useTransition has been reached.
Yes, you are right.
This code avoid hidden fallback until 1500 ms. timeout start from create promise resource. But you like to start that from UI rendered.
Related to this feature request - what if the child component rendered with Suspense was given an additional prop representing the amount of time the fallback component was visible? That way the child component can own the logic for minimum fallback component duration time, such as:
const FALLBACK_MIN_DURATION_MS = 1500;
const ChildComponent = ({ fallbackVisibilityDuration }) => {
const [showFallback, setShowFallback] = useState(false);
useEffect(() => {
if (fallbackVisibilityDuration < FALLBACK_MIN_DURATION_MS) {
setShowFallback(true);
setTimeout(() => { setShowFallback(false); }, (FALLBACK_MIN_DURATION_MS - fallbackVisibilityDuration));
}
});
return showFallback ? <h1>Loading...</h1> : <h1>Foo</h1>;
}
@PatNeedham This way ChildComponent has too many responsibilities from my point of view.
I think that a child component which suspends does not have to handle its fallback's min visibility duration, it's not its responsibility, that's the point of Suspense in the first place.
I can imagine just setting internal state after fallback gets called and than after return just check if it was rendered for concrete amount of time... if yes render children of suspense...
@damikun Could you provide an example? Thank you!
Most helpful comment
Please don't close it, it's worth tracking.