Skip to content
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

Refactor Nav creation hooks and associated code #38824

Closed
wants to merge 6 commits into from

Conversation

getdave
Copy link
Contributor

@getdave getdave commented Feb 15, 2022

Description

Continues work on #37190.

This PR looks to address the inconsistencies around the code paths (especially hooks) around the creation of classic menus into block based menus.

Currently many of the hooks involved are very opaque and despite being async, provide little or not information about the resolution status of the async requests involved. This causes UI issues such as that which is experienced when selecting a classic menu from the dropdown and observing how the UI appears "locked" whilst the async request resolves.

This PR takes a step towards solving these problems. Principally it exposes information about the resolution status of the useConvertClassicMenu hook which allows the UI to respond according.

Testing Instructions

Screenshots

Types of changes

Checklist:

  • My code is tested.
  • My code follows the WordPress code style.
  • My code follows the accessibility standards.
  • I've tested my changes with keyboard and screen readers.
  • My code has proper inline documentation.
  • I've included developer documentation if appropriate.
  • I've updated all React Native files affected by any refactorings/renamings in this PR (please manually search all *.native.js files for terms that need renaming or removal).
  • I've updated related schemas if appropriate.

@getdave getdave self-assigned this Feb 15, 2022
@getdave getdave added the [Block] Navigation Affects the Navigation Block label Feb 15, 2022
@@ -495,6 +528,13 @@ function Navigation( {
setRef( id );
onClose();
} }
onSelectClassicMenu={ ( menu ) => {
onClose();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling onClose here allows the dropdown to close and the block UI to update whilst the async conversion process happens.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to be a little careful about focus management here.

@@ -495,6 +528,13 @@ function Navigation( {
setRef( id );
onClose();
} }
onSelectClassicMenu={ ( menu ) => {
onClose();
convertClassicMenuToBlocks(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By passing the conversion code down into the dropdown we avoid errors thrown when the dropdown unmounts and an async request triggered by that component resolves. We do this by handling in the parent (which is not unmounted when the option is clicked).

@@ -676,6 +716,7 @@ function Navigation( {
/>
) }
{ ! hasResolvedCanUserCreateNavigation ||
isResolvingClassicMenuConversion ||
( ! isEntityAvailable && ! isPlaceholderShown && (
<PlaceholderPreview isLoading />
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to look again at loading states because I don't yet see this. Perhaps because of the call to await createNavigationMenu (which again has no resolution status indicators).

@@ -55,4 +59,12 @@ export default function useConvertClassicMenu( onFinish ) {
},
[ hasResolvedMenuItems, createFromMenu ]
);

return {
dispatch,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't love dispatch here. We could use run or similar?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

convert?

Copy link
Contributor

@talldan talldan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropping an early review, though I realise this is still a draft.

I'm pretty familiar with this code as I had to write this last minute patch for 5.9 - #38168.

I didn't do anything drastic in that PR, but it did introduce the useConvertClassicMenu hook, which to be honest, I don't love. It was just moving the existing code into something reusable rather than improving the code quality.

IMO, I think we should probably explore more drastic changes than we do in this PR and instead try to get rid of useConvertClassicMenu.

Here's what I think is the problem. Hooks that work with or return data are generally 'long lived', they'll be active for entire lifecycle of a component. Converting a menu though is a one-time action that doesn't require data to be kept around.

There are two bits of data that useConvertClassicMenu persists when it doesn't need to - the classic menu name, and the classic menu items (which it fetches once it has a menu id). I think that's problematic, because what are we supposed to do with this data after a conversion has happened? We could reset it, but then we're just doing garbage collection manually.

Another option is to instead make classic menu conversion a normal async function instead of a hook. Give the function a menu id, it maybe calls apiFetch instead of useSelect to get the menu items and returns blocks. The only thing we lose is caching of the classic menu items in the store, but I don't think classic menu conversion is a common or regular interaction so it's an ok trade-off.

It may also be good to get some design input into the loading states. I don't know how we can adequately convey that clicking a classic menu is an asynchronous operation.

@@ -55,4 +59,12 @@ export default function useConvertClassicMenu( onFinish ) {
},
[ hasResolvedMenuItems, createFromMenu ]
);

return {
dispatch,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

convert?

Comment on lines 238 to 244
const {
dispatch: convertClassicMenuToBlocks,
blocks: classicMenuBlocks,
name: classicMenuName,
isResolving: isResolvingClassicMenuConversion,
hasResolved: hasResolvedClassicMenuConversion,
} = useConvertClassicMenu();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like a lot of overhead to run this on every render of edit when converting classic menus is a rare interaction for the user. It'll result in the block always fetching all classic menus.

if (
hasResolvedClassicMenuConversion &&
classicMenuName &&
classicMenuBlocks?.length
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I have an empty classic menu, nothing happens when clicking the menu item. (it's the first thing I noticed when testing as all my classic test menus are empty 😄 )

@@ -495,6 +528,13 @@ function Navigation( {
setRef( id );
onClose();
} }
onSelectClassicMenu={ ( menu ) => {
onClose();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to be a little careful about focus management here.

hasResolvedClassicMenuConversion,
] );

const isStillLoading =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this was already in trunk, but I think a good improvement would be a rename of this variable:

Suggested change
const isStillLoading =
const isLoading =

@getdave
Copy link
Contributor Author

getdave commented Feb 16, 2022

Dropping an early review, though I realise this is still a draft.

I'm pretty familiar with this code as I had to write this last minute patch for 5.9 - #38168.

I didn't do anything drastic in that PR, but it did introduce the useConvertClassicMenu hook, which to be honest, I don't love. It was just moving the existing code into something reusable rather than improving the code quality.

IMO, I think we should probably explore more drastic changes than we do in this PR and instead try to get rid of useConvertClassicMenu.

Here's what I think is the problem. Hooks that work with or return data are generally 'long lived', they'll be active for entire lifecycle of a component. Converting a menu though is a one-time action that doesn't require data to be kept around.

There are two bits of data that useConvertClassicMenu persists when it doesn't need to - the classic menu name, and the classic menu items (which it fetches once it has a menu id). I think that's problematic, because what are we supposed to do with this data after a conversion has happened? We could reset it, but then we're just doing garbage collection manually.

Another option is to instead make classic menu conversion a normal async function instead of a hook. Give the function a menu id, it maybe calls apiFetch instead of useSelect to get the menu items and returns blocks. The only thing we lose is caching of the classic menu items in the store, but I don't think classic menu conversion is a common or regular interaction so it's an ok trade-off.

It may also be good to get some design input into the loading states. I don't know how we can adequately convey that clicking a classic menu is an asynchronous operation.

TBH I've been going round and round with this trying to decide what to do. I thought I'd push this up and at least then it would be easier to discuss. Glad you 👀 and left thoughts - thank you.

Let's consider the more drastic changes you suggest. I am keen that we are able to convey when something is "working" so we'll need to keep that in mind.

@getdave
Copy link
Contributor Author

getdave commented Feb 16, 2022

I don't know how we can adequately convey that clicking a classic menu is an asynchronous operation.

For what it's worth I don't think we can or should attempt to do this within the dropdown / menu item itself. Instead we

  1. Click the menu item.
  2. Immediately close the menu - this is expected UX else it feels like the UI is freezing/locking up.
  3. Trigger a "loading" UI in the block itself.
  4. Once menu conversion is resolved, disable the loading state.

The block shouldn't care how the data is being loaded. All it needs to know is that the data isn't available and thus it should be in a "loading" state.

@adamziel
Copy link
Contributor

adamziel commented Feb 16, 2022

Another option is to instead make classic menu conversion a normal async function instead of a hook. Give the function a menu id, it maybe calls apiFetch instead of useSelect to get the menu items and returns blocks. The only thing we lose is caching of the classic menu items in the store, but I don't think classic menu conversion is a common or regular interaction so it's an ok trade-off.

We can lean on select even if that's an async function if we use the registry:

const registry = useRegistry();
const callback = useCallback(() => {
    setIsConverting( true );
    try{
        await convertClassicMenu( registry );
        showSuccessNotice();
    } catch(e) {
        showErrorNotice(e);
    } finally {
        setIsConverting( false );
    }
}, [registry])

and then we can do things like registry.resolveSelect( coreStore ).getEntityRecords( /* ... */ )

@getdave
Copy link
Contributor Author

getdave commented Feb 16, 2022

Another option is to instead make classic menu conversion a normal async function instead of a hook. Give the function a menu id, it maybe calls apiFetch instead of useSelect to get the menu items and returns blocks. The only thing we lose is caching of the classic menu items in the store, but I don't think classic menu conversion is a common or regular interaction so it's an ok trade-off.

We can lean on select even if that's an async function if we use the registry:

const registry = useRegistry();
const callback = useCallback(() => {
    setIsConverting( true );
    try{
        await convertClassicMenu( registry );
        showSuccessNotice();
    } catch(e) {
        showErrorNotice(e);
    } finally {
        setIsConverting( false );
    }
}, [registry])

and then we can do things like registry.resolveSelect( coreStore ).getEntityRecords( /* ... */ )

😮 Thanks Adam!

I have another PR where I've changed course to use "one big async function". I'll push that up and then post a link here so we can discuss if/how to migrate over to using registry and friends as @adamziel suggests.

@getdave
Copy link
Contributor Author

getdave commented Feb 16, 2022

@getdave
Copy link
Contributor Author

getdave commented Feb 21, 2022

Closing in favour of #38858

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Block] Navigation Affects the Navigation Block
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants