google / site-kit-wp

Site Kit is a one-stop solution for WordPress users to use everything Google has to offer to make them successful on the web.
https://sitekit.withgoogle.com
Apache License 2.0
1.23k stars 286 forks source link

Create `useInViewSelect` hook #4096

Closed tofumatt closed 2 years ago

tofumatt commented 3 years ago

Feature Description

We should add a new hook named useInViewSelect that triggers a useSelect only when the nearest element in an "In-View Context Provider" (likely a WidgetAreaRenderer) is on-screen.

For more context refer to the section "Lazy-load rendering of widget contexts/areas not in view" in the design doc.

This should rely on the useInView hook created in #4120, specifically using useInView( { resetWhenOffScreen: false } ).


Do not alter or remove anything below. The following sections will be managed by moderators only.

Acceptance criteria

Implementation Brief

useInViewSelect( mapSelect, deps )

WP Dashboard

Test Coverage

QA Brief

Changelog entry

felixarntz commented 3 years ago

@tofumatt Maybe a good idea to actually test-drive this while implementing (rather than just implementing without using it anywhere initially) would be the WP dashboard widget - see https://github.com/google/site-kit-wp/issues/4001#issuecomment-923421368. I wonder if we should just merge these two tickets.

The main load is really implementing this hook, using it in WP dashboard widget would just be a matter of replacing useSelects, and by doing that here we would be able to know that it actually works.

tofumatt commented 3 years ago

Fair enough, we can test-drive it there for sure. I'd be happy to include replacing the usage inside the WP Dashboard as part of this issue though 👍🏻

felixarntz commented 3 years ago

@tofumatt Let's do that. Once that's part of the ACs here, you can close #4001 with a reference to here. We should then explicitly include that bit in the changelog here as well, maybe even change the issue title to "Create useInViewSelect hook and use it in WP dashboard widget".

aaemnnosttv commented 2 years ago

@tofumatt I think we need to update the ACs here which don't include the relevant changes to @felixarntz 's https://github.com/google/site-kit-wp/issues/4096#issuecomment-926885002 above (I've included in the IB though).

The hook should return either true or falsefalse if the parent component/context has NEVER been on-screen, but true as soon as it has been visible even once.

  • If the component appears on-screen, then is scrolled out-of-screen, the useInView hook should still return true.

I don't recall why we wanted this but I think we should avoid introducing it as it makes things quite a bit more complicated. The part I remember is that we needed a reset to be avoid the mass select that would be triggered on the dashboard (if everything had been in view once) when changing the date range. However, if we leave it as-is and don't implement in-view means "was ever in view", then this is no longer a concern because any useInViewSelect uses which were not in view would not trigger selects again. These would likely be back in their loading state though which shouldn't really matter if they're out of view as long as it doesn't result in a layout shift. As soon as they came back into view, they should of course render as before, or load/request new data if needed.

I've added the current IB without this which seems like a good place to start. If needed, I suggest we explore enhancing the behavior in a follow-up issue.

Back to @tofumatt for revising the ACs but otherwise I think this is ready for IBR 👍

felixarntz commented 2 years ago

@aaemnnosttv

I don't recall why we wanted this but I think we should avoid introducing it as it makes things quite a bit more complicated. The part I remember is that we needed a reset to be avoid the mass select that would be triggered on the dashboard (if everything had been in view once) when changing the date range.

The reason is that it will likely result in some clunky UX, I see specifically the following two problems if we don't do it like that:

tofumatt commented 2 years ago

@felixarntz covered it exactly—we don't want to be removing fully-rendered and loaded widgets/elements from the DOM entirely when they're scrolled out-of-view.

In the case of Google Charts like the Analytics ones, for instance, it's actually quite expensive to create those charts—they have a lot of event handlers, SVG/canvas computations/drawing, etc. Once they're rendered they don't consume too many resources, but loading/unloading them is quite expensive.

Doing this anytime they scroll out-of-view would be wasteful and result in a lot of needless DOM manipulation/Google Charts work.

So we want to include that "once in-view-once always return true" to this hook—I think it's a vital part of how it works.

tofumatt commented 2 years ago

I've kept the ACs as-discussed, and fleshed out the "reset" mechanism a bit more. Originally @aaemnnosttv and I discussed it and I wasn't sure about storing that "has been in view" state in the datastore, but upon further consideration I think it's the best, most flexible, and most straightforward approach. I've raised the estimate to account for this more fleshed-out implementation of the reset and its tests.

I think it's best to include those reset actions in this issue so we can test/build it more holistically. Happy to take this one on as I have an idea of how to approach it already 🙂

aaemnnosttv commented 2 years ago

Thanks @tofumatt!

  • There should be a mechanism to "reset" the useInViewSelect state for any hook that has had its useInView hook return true at least once. The best way to implement this would be to have each useInViewSelect hook create its own instanceID and store the "useInView() has returned true at least once since reset" flag for its instance ID. All of these instance IDs that have returned true once should be stored in an array in the CORE_UI datastore. This way we can easily reset all hooks by clearing this array when the UI should be "reset".

I'm not sure we need to build additional datastore infra to accommodate the reset functionality we want. Also, I don't think the has-been-in-view state should be unique to each useInViewSelect because the in-view state is provided by the higher context, so it would make more sense to manage this in a more centralized way.

I think we can encapsulate this in the WidgetAreaRenderer building on the foundation in #4120 like so:

I think that would solve all our needs without really requiring any changes to useInViewSelect itself while still allowing for a global reset via the datastore as an escape hatch if we really needed it.

Another benefit of this approach would be room for adding throttle/debounce-like functionality in the updating of the state-managed value which will likely be needed at some point as well.

Let me know what you think!

tofumatt commented 2 years ago

I had considered the "increment a value to 'cache-bust' the inViewSelect boolean" approach, and I think that part works okay, but the issue with your approach is that it ends up coupling the WidgetAreaRenderer and the hook to a degree. I wanted a system where the hook was independent, in case we wanted to use it in either finer-grained or broader <div>s in the future. It means it's easier to scaffold—it also means it's easier to understand externally.

Your proposed solution doesn't immediately strike me as more performant, but I feel like it's a bit more complicated. Maybe I'm missing something?

We could probably combine the approaches though, with only a reset data store action that incremented the "cache-busting" variable. But I thought this functionality warranted a dedicated selector as it's not "arbitrary" data and I thought it was a bit easier to understand...

The throttling/debouncing option with setState state-managed variables though is fair—keeping that out of the data store is handy.

I think we can combine the two approaches for something that combines the best of both worlds 🙂

aaemnnosttv commented 2 years ago

Per https://github.com/google/site-kit-wp/issues/4120#issuecomment-947788625 that option name might change, but otherwise this LGTM 👍

IB ✅

felixarntz commented 2 years ago

Per https://github.com/google/site-kit-wp/issues/4096#issuecomment-924477283, I've updated the ACs here to also encompass what was previously the ACs of #4001. The IB already accounts for that, so execution here can proceed as is.

wpdarren commented 2 years ago

QA Update: ✅

Verified:

Screenshot 2021-11-22 at 14 02 49

felixarntz commented 2 years ago

Approval ❌

@tofumatt Per what we discussed yesterday, the implementation in #4347 isn't really accurate. It changes a couple of useSelects to useInViewSelect that don't trigger API calls, while it doesn't actually change the useSelects that do trigger API calls. Specifically, the WPDashboardWidgets shouldn't need to be modified in this issue at all, but the sub-components (which are the ones triggering the API calls) like WPDashboardClicks, WPDashboardImpressions etc. should.

Can you open a follow-up PR for main that reverts the changes in WPDashboardWidgets and instead adds them for where API calls are triggered (getReport and isGatheringData selector calls in any WPDashboard* components)?

cc @eugene-manuilov @aaemnnosttv

aaemnnosttv commented 2 years ago

@felixarntz back to you for another pass 👍

felixarntz commented 2 years ago

Approval ❌

@tofumatt @aaemnnosttv The fix PR is mostly good, but one API request is still being made from the WP dashboard widget even when it is hidden, likely because the isGatheringData call in WPDashboardPopularPages is still using useSelect.

aaemnnosttv commented 2 years ago

@felixarntz agh, I must have missed that in my testing because Analytics wasn't connected. Submitting a PR to fix it now 👍

felixarntz commented 2 years ago

Approval ✅