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

Experiment: Client-side navigation of the Query Loop block using directives #44034

Closed
wants to merge 14 commits into from

Conversation

luisherranz
Copy link
Member

@luisherranz luisherranz commented Sep 9, 2022

What?

This is an experiment to implement client-side navigations in the Query Loop block.

It's an alternative to #38713 based on our experiments with Directives hydration.

Why?

To be able to implement patterns that improve the user experience.

How?

It fetches the HTML of the next page and diffs the DOMs using Preact's virtual DOM.

This is just a quick experiment. @DAreRodz and I are working on an implementation of this algorithm that can also hydrate interactive components:

It's also worth noting that this approach can work for any page navigation, not only those of the Query Loop block.

Testing Instructions

Create a post or page with a Query Loop and navigate using the Next or Previous buttons.

As long as the next/previous links are preserved in the DOM, the vDOM diffing is intelligent enough not to destroy them, and the event handlers keep working. But on the first and last page, the links disappear from the DOM and lose the event handler. I haven't done anything to reattach the event listeners because I want to test using directives instead.

EDIT: This is working fine after we switched to directives.

Screenshots or screencast

Screen.Capture.on.2022-09-09.at.16-40-32.mp4

Next steps

I'll probably use this to test more patterns:

  • Attach event handlers using a directive
  • Prefetch URL

@gziolo gziolo added the [Type] Technical Prototype Offers a technical exploration into an idea as an example of what's possible label Sep 12, 2022
This is currently broken as it won't prefetch the new links.
We need something similar to useEffect here.
@luisherranz luisherranz removed the request for review from ajitbohra September 12, 2022 17:52
@luisherranz luisherranz self-assigned this Sep 12, 2022
@luisherranz
Copy link
Member Author

I've added a few things:

  • Attach event handlers using a directive ✅

    That works great. I'm using wp-client-navigation and these directives can be added directly in PHP.

    <a href="..." wp-client-navitagion>...</a>

    It also accepts an object with options for prefetch and scroll. I don't know if that's the best DX, or if it'd be better to use only strings and multiple directives.

    <a href="..." wp-client-navitagion="{...}">...</a>
  • Prefetch and cache pages 🚧

    It works for the initial URLs only because the directive callback is executed only on vnode creation (mount). I'm not sure what would be the best DX here. Maybe something similar to useEffect: it runs on every update and you can return a cleanup function.

    Prefetch should have more strategies, but I won't implement them here.

  • Add the vdom of the initial page to the cache ✅

  • Support back and forward buttons ✅

@luisherranz
Copy link
Member Author

luisherranz commented Sep 13, 2022

The prefetch and the cache are working perfectly now ✅

I've switched to vnode-based hooks for the WordPress Directives. It's still low level and I didn't change the internal Preact names (diff, diffed) because they don't relate 1:1 to the component lifecycles we are used to. It feels better than the DOM-based hooks, but still too low level. Modifications to the vnode are reflected in the vdom, which is better for subsequent client-side navigations.

// The `wp-client-navigation` directive.
wpDirectives.clientNavigation = {
  onDiff: ({ props }) => {
    const {
      wp: { clientNavigation },
      href,
    } = props;

    // Prefetch the page if it is in the directive options.
    if (clientNavigation?.prefetch) {
      fetchPage(href);
    }

    // Don't do anything if it's falsy.
    if (!!clientNavigation) {
      props.onclick = async (event) => {
        event.preventDefault();

        // Fetch the page (or return it from cache).
        const vdom = await fetchPage(href);
        // Render the new page.
        render(vdom, rootFragment);

        // Update the URL.
        window.history.pushState({}, "", href);

        // Update the scroll, depending on the option. True by default.
        if (clientNavigation?.scroll === "smooth") {
          window.scrollTo({ top: 0, left: 0, behavior: "smooth" });
        } else if (clientNavigation?.scroll !== false) {
          window.scrollTo(0, 0);
        }
      };
    }
  },
};

I want to try wrapping the vnode with a component to be able to use render and effect and see how it feels.

I left a console.log in case somebody wants to understand how Preact Option Hooks work (vnode -> diff -> diffed -> unmount). I'll remove them later.

I've also added an animation to make sure that Preact is never removing DOM nodes when hydrating (or rehydrating).

@luisherranz
Copy link
Member Author

luisherranz commented Sep 14, 2022

Ok, this feels much better now. You can even use regular hooks.

// The `wp-client-navigation` directive.
directive("clientNavigation", (props) => {
  const { wp: { clientNavigation }, href } = props;

  useEffect(() => {
    // Prefetch the page if it is in the directive options.
    if (clientNavigation?.prefetch) {
      prefetch(href);
    }
  });

  // Don't do anything if it's falsy.
  if (!!clientNavigation) {
    props.onclick = async (event) => {
      event.preventDefault();

      // Fetch the page (or return it from cache).
      await navigate(href);

      // Update the URL.
      window.history.pushState({}, "", href);

      // Update the scroll, depending on the option. True by default.
      if (clientNavigation?.scroll === "smooth") {
        window.scrollTo({ top: 0, left: 0, behavior: "smooth" });
      } else if (clientNavigation?.scroll !== false) {
        window.scrollTo(0, 0);
      }
    };
  }
});

Remember, once it is defined, just need to add the directive to the HTML (in PHP or in the save.js):

<a wp-client-navigation href="/some-post">Some post</a>

We need to figure out the use cases that we need to cover to see if this API fits well or if we need to add more things (i.e., a ref to the DOM element and so on).

I've also refactored a bit the code into different files: directives, router and vdom.

@luisherranz
Copy link
Member Author

luisherranz commented Sep 20, 2022

I've started another client-side navigation experiment for WooCommerce's Product Query block. This one is replacing only the inner children of the Product Query block instead of the whole document.

@luisherranz luisherranz marked this pull request as draft September 20, 2022 16:08
@SantosGuillamot
Copy link
Contributor

For context: I took a look at the cst parameter in the query, that was causing issues with the prefetching. It seems it is just a fake argument as explained here. If I understood it correctly, it should be removed at some point.

@luisherranz luisherranz changed the title Experiment: Client-side navigation with pagination block and query loop using a virtual DOM Experiment: Client-side navigation of the Query Loop block using a virtual DOM Sep 27, 2022
@luisherranz luisherranz changed the title Experiment: Client-side navigation of the Query Loop block using a virtual DOM Experiment: Client-side navigation of the Query Loop block using directives Jan 11, 2023
@gziolo gziolo added the [Feature] Interactivity API API to add frontend interactivity to blocks. label Apr 2, 2023
@luisherranz
Copy link
Member Author

Superseded by #53812.

@gziolo gziolo deleted the experiment/vdom-client-transitions branch August 23, 2023 08:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Feature] Interactivity API API to add frontend interactivity to blocks. [Type] Technical Prototype Offers a technical exploration into an idea as an example of what's possible
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants