-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Site Editor: Improve loading experience (v3) #50222
Changes from all commits
e04b373
b0efb74
6228108
a355fba
bbdfe6d
8961bc8
d6a1842
7a314d1
73e8836
c5467c9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -131,3 +131,18 @@ export function isResolving( state, selectorName, args ) { | |
export function getCachedResolvers( state ) { | ||
return state; | ||
} | ||
|
||
/** | ||
* Whether the store has any currently resolving selectors. | ||
* | ||
* @param {State} state Data state. | ||
* | ||
* @return {boolean} True if one or more selectors are resolving, false otherwise. | ||
*/ | ||
export function hasResolvingSelectors( state ) { | ||
return [ ...Object.values( state ) ].some( ( selectorState ) => | ||
[ ...selectorState._map.values() ].some( | ||
Comment on lines
+143
to
+144
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The spread operators would be needed for results of The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you for the feedback @mcsf! I've addressed it in #50865. A couple of notes to add:
Alternatively, we could open a PR in the Edit: ha, Jarda beat me to it. Spot on btw! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note that the standard What we'd like to do is to add There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the answers, both of you. Right, I didn't take the time to understand the data structure behind
To be clear, I think I'm fine with just removing the copy around There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TBH spread and Regardless, I'm happy to make this 0.1% more readable 👍 Updated in the PR. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I usually prefer |
||
( resolution ) => resolution[ 1 ]?.status === 'resolving' | ||
) | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,10 @@ | ||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { useMemo } from '@wordpress/element'; | ||
import { useEffect, useMemo, useRef, useState } from '@wordpress/element'; | ||
import { useSelect, useDispatch } from '@wordpress/data'; | ||
import { Notice } from '@wordpress/components'; | ||
import { EntityProvider } from '@wordpress/core-data'; | ||
import { EntityProvider, store as coreStore } from '@wordpress/core-data'; | ||
import { store as preferencesStore } from '@wordpress/preferences'; | ||
import { | ||
BlockContextProvider, | ||
|
@@ -153,12 +153,41 @@ export default function Editor() { | |
// action in <URlQueryController> from double-announcing. | ||
useTitle( hasLoadedPost && title ); | ||
|
||
if ( ! hasLoadedPost ) { | ||
return <CanvasSpinner />; | ||
} | ||
const { hasResolvingSelectors } = useSelect( ( select ) => { | ||
return { | ||
hasResolvingSelectors: select( coreStore ).hasResolvingSelectors(), | ||
}; | ||
} ); | ||
const [ loaded, setLoaded ] = useState( false ); | ||
const timeoutRef = useRef( null ); | ||
|
||
useEffect( () => { | ||
if ( ! hasResolvingSelectors && ! loaded ) { | ||
clearTimeout( timeoutRef.current ); | ||
|
||
/* | ||
* We're using an arbitrary 1s timeout here to catch brief moments | ||
* without any resolving selectors that would result in displaying | ||
* brief flickers of loading state and loaded state. | ||
* | ||
* It's worth experimenting with different values, since this also | ||
* adds 1s of artificial delay after loading has finished. | ||
*/ | ||
timeoutRef.current = setTimeout( () => { | ||
setLoaded( true ); | ||
}, 1000 ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Curious to know what this arbitrary timeout is for? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are brief moments where there aren't any resolving selectors, but a few milliseconds later, there are resolving selectors present. This arbitrary timeout is there to ensure that we're marking the editor as loaded only if there haven't been any resolving selectors for an entire second. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This reads a bit like an anti-pattern to me and a bad UX for users. Now every user has to wait at least a second to load the editor, no matter how fast their device is. Could there also be cases when the user's device is so slow that the 1-second delay isn't sufficient to run all the remaining tasks? Could we figure out the correct timing to subscribe to the load event under the hood? This might be a bit more difficult to do but I think it'll be worth it 😆. At the very least, let's add some comments to explain the timeout. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In terms of blocks, it seems like it's difficult to know what's going to load next. The next resource/block to load is revealed only once the previous resource/block has been loaded, parsed and rendered. My assumption is that's what this timeout is for. The two ideas I had:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I don't claim for the 1s to be perfect. I claim it to be much better than what we had before, and much better than if we didn't have it all (in which case we'd be seeing bad flickering). Yes, 1s might be insufficient for very slow devices, but we're making a compromise here and I'm happy to experiment with timing separately. Regardless, I think this is much better than what we had before, so it's worth going with it for the time being while experimenting with better timing.
I'm open to suggestions. From my understanding, there isn't a good way to predict or subscribe to that, nor to figure out the perfect timing without a lot of experimentation and speculation. @talldan has explained that well in the above comment. In the meantime, I've added a comment to explain the arbitrary timeout: c7fed44
Both of those seem pretty complex to me, and I'm not sure it's worth investing in an additional complex parsing mechanism or a complex loading tracking mechanism, given that we're aiming to start async loading blocks at some point in the near future (with the upcoming work on ES modules and import maps adoption in the WordPress core). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agree with all your points @tyxla 👍 Great job on getting this merged. 🎉 |
||
|
||
return () => { | ||
clearTimeout( timeoutRef.current ); | ||
}; | ||
} | ||
}, [ loaded, hasResolvingSelectors ] ); | ||
|
||
const isLoading = ! loaded || ! hasLoadedPost; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Potentially this logic to compute the isLoading const could be extracted to a custom hook There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While I don't see the immediate need for that, I'm happy to go ahead and extract it when it can actually be reused somewhere. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it is mostly a suggestion to keep that logic outside the Editor component (timeout...) But I don't feel strongly. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is a good idea, I'll follow up in another PR. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Addressed in #50511 |
||
|
||
return ( | ||
<> | ||
{ isLoading ? <CanvasSpinner /> : null } | ||
{ isEditMode && <WelcomeGuide /> } | ||
<EntityProvider kind="root" type="site"> | ||
<EntityProvider | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -105,6 +105,7 @@ | |
left: 0; | ||
bottom: 0; | ||
width: 100%; | ||
overflow: hidden; | ||
|
||
& > div { | ||
color: $gray-900; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Depending how confident we are with this selector (it seems fine to me), we could consider making it private to start with.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To me, one of the positive sides of metadata selectors is that they are always publicly available. That being said, I wonder if you think there's a particular need for this specific one to be private.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not for me. I think it's fine for this one to be public. I was suggesting it in case we missed confidence.