Closed tofumatt closed 2 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 useSelect
s, and by doing that here we would be able to know that it actually works.
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 👍🏻
@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".
@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
orfalse
—false
if the parent component/context has NEVER been on-screen, buttrue
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 stillreturn 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 👍
@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:
@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.
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 🙂
Thanks @tofumatt!
- There should be a mechanism to "reset" the
useInViewSelect
state for any hook that has had itsuseInView
hookreturn true
at least once. The best way to implement this would be to have eachuseInViewSelect
hook create its own instanceID and store the "useInView()
has returnedtrue
at least once since reset" flag for its instance ID. All of these instance IDs that have returnedtrue
once should be stored in an array in theCORE_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:
inView
value that we would provide via context and store it in a state variableinView
state to true
(as in ever in view once) only if true
, using the inView
intersection observer entry value as a dependency of that effect (that way when it later became false
, the state would remain true)inView
from state as the in view context valueuseEffect
that's based on the current date range as its only dependency – when that changes, it should set the state-managed inView
value to the real current value from the I.O. entry (essentially resetting it, but without forcing a re-render if the value hasn't changed)
forceUpdate = () => setValue( inViewBust, num + 1 )
where num
would be a simple integer to bust the cache with)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!
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 🙂
Per https://github.com/google/site-kit-wp/issues/4120#issuecomment-947788625 that option name might change, but otherwise this LGTM 👍
IB ✅
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.
Verified:
@tofumatt Per what we discussed yesterday, the implementation in #4347 isn't really accurate. It changes a couple of useSelect
s to useInViewSelect
that don't trigger API calls, while it doesn't actually change the useSelect
s 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
@felixarntz back to you for another pass 👍
@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
.
@felixarntz agh, I must have missed that in my testing because Analytics wasn't connected. Submitting a PR to fix it now 👍
Feature Description
We should add a new hook named
useInViewSelect
that triggers auseSelect
only when the nearest element in an "In-View Context Provider" (likely aWidgetAreaRenderer
) 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 usinguseInView( { resetWhenOffScreen: false } )
.Do not alter or remove anything below. The following sections will be managed by moderators only.
Acceptance criteria
useInViewSelect
hook created inassets/js/hooks/useInViewSelect.js
.useSelect
hook.useInView( { resetWhenOffScreen: false } )
hook to either:useInViewSelect(...args)
through touseSelect(...args)
(ifuseInView()
istrue
)() => undefined
to theuseSelect
hook ifuseInView()
isfalse
useInViewSelect
hook from #4096 for all of its API-based selector calls, replacinguseSelect
for those.Implementation Brief
useInViewSelect( mapSelect, deps )
useInView
introduced in #4120 (not to be confused with the current/old hook fromreact-intersection-observer
which should be removed now) which will return a boolean based on the context provider's element (the widget area) is in viewmapSelect
function should be wrapped withuseCallback
using the givendeps
since we can't change the length ofdeps
passed touseSelect
which is essentially used in the same way, we just do it ahead of time hereuseSelect( mapSelect )
(note no deps passed)inView
returnstrue
, pass the givenfunc
asmapSelect
with the givendeps
inView
returnsfalse
, pass a function that always returnsundefined
(this should be defined as a constant rather than an a new function every time to avoid triggering the select unnecessarily)WP Dashboard
WPDashboardApp
to work similar to the changes inWidgetAreaRenderer
made in #4120, essentially usinguseIntersection
, and wrapping the WP dashboard widget with anInViewProvider
and setting theref
on thediv.googlesitekit-wp-dashboard
useSelect
hooks inWPDashboardWidgets
to useuseInViewSelect
insteadnull
if selected values returnundefined
to prevent nested components from renderingTest Coverage
useInViewSelect
QA Brief
/wp-admin
on a site that has Site Kit setup/google-site-kit/v1/
are made when at the top of the page/google-site-kit/v1/
.Changelog entry
useInViewSelect
hook that allows to call a specific selector only when in view.