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

[ML] Improvements for urlState hook. #70576

Merged
merged 16 commits into from
Jul 8, 2020

Conversation

walterra
Copy link
Contributor

@walterra walterra commented Jul 2, 2020

Summary

Makes two improvements to the urlState hook (also known as appState in some places):

  • There was always a risk to run into a race condition because setUrlState could refer to a stale version of the state to act upon, for example if two calls were done in parallel. This is now fixed by using a local state copy of what we get from useLocation(). This allows us to use the callback version of useState's set function so we can make sure we always modify the latest state.
  • Calls to history.push() are now gated by a check if the change actually referred to the corresponding instance of urlState (either _g or _a), this should reduce the updates resulting re-renders.

The two changes should make the use of setUrlState more safe against the pitfalls (race conditions/stale updates/lots of rerenders) we previously faced.

Checklist

For maintainers

@walterra walterra added :ml refactoring v8.0.0 release_note:skip Skip the PR/issue when compiling release notes v7.9.0 labels Jul 2, 2020
@walterra walterra requested a review from a team as a code owner July 2, 2020 10:43
@walterra walterra self-assigned this Jul 2, 2020
@elasticmachine
Copy link
Contributor

Pinging @elastic/ml-ui (:ml)

// Only push to history if something related to the accessor of this
// url state instance is affected (e.g. a change in '_g' should not trigger
// a push in the '_a' instance).
if (!isEqual(getUrlState(locationSearch)[accessor], getUrlState(search)[accessor])) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd suggest storing the result of getUrlState in some state value or observable and update it based on URL changes. It will let use to avoid retrieving the value from URL string on each render.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe the naming was just not as obvious, but that PR already does that: locationSearch refers to what we get from React Router's useLocation, search is the state value we keep. getUrlState() is a pure function that just parses the search string so we can compare the namespace/accessor (_g/_a), it's not touching the URL directly.

I pushed some more commits with renames to make this more obvious:

  • renamed getUrlState to parseUrlState
  • renamed locationSearch to locationSearchString
  • renamed search to searchString

Also added a useMemo at the end of the hook so we return a memoized version of the parsed url state.

Copy link
Contributor

Choose a reason for hiding this comment

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

yes, this commit 06b0235 introduced useMemo with a parsed object, this is what I meant 👍

);

return [getUrlState(search)[accessor], setUrlState];
const urlState = useMemo(() => parseUrlState(searchString)[accessor], [searchString]);
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it worth to mention in a comment for this hook that it does not react on the accessor parameter update

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, the accessor should never be changed, I updated the code to enforce that so it only ever considers the value passed on first call: 3500f11#diff-42f8ed66c3f1b64589d16f1b4c9d1aa2R63-R65

Copy link
Contributor

@darnautov darnautov left a comment

Choose a reason for hiding this comment

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

LGTM! Great enhancement

// url state instance is affected (e.g. a change in '_g' should not trigger
// a push in the '_a' instance).
if (
!isEqual(parseUrlState(locationSearchString)[accessor], parseUrlState(searchString)[accessor])
Copy link
Contributor

Choose a reason for hiding this comment

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

looks like this effect should depend on two variables, you are missing locationSearchString

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 added more comments in 3500f11 to explain why locationSearchString isn't necessary for comparison in this case.

Copy link
Contributor

@peteharverson peteharverson left a comment

Choose a reason for hiding this comment

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

Tested and LGTM

Copy link
Contributor

@alvarezmelissa87 alvarezmelissa87 left a comment

Choose a reason for hiding this comment

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

LGTM ⚡

@walterra
Copy link
Contributor Author

walterra commented Jul 3, 2020

@elasticmachine merge upstream

@walterra
Copy link
Contributor Author

walterra commented Jul 6, 2020

@darnautov @alvarezmelissa87 @peteharverson

The functional tests surfaced that there were still issues with possible race conditions and render loops.
The problem was that multiple instances of useUrlState would manage their own state and could end up with different stale state when doing an update.
I solved this now by moving the logic to update the params string to a React context so there is ever only one instance of the code that manages url state updates. The hook useUrlState uses that context and keeps the same API surface, so the overall code in components can stay the same. The only difference is that I added the corresponding provider to the root of the ML plugin.

In the long run we should try to migrate away from HashRouter to BrowserRouter, this would simplify how we can manage this because it has better support for url params.

Copy link
Contributor

@alvarezmelissa87 alvarezmelissa87 left a comment

Choose a reason for hiding this comment

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

Code LGTM ⚡

Copy link
Contributor

@peteharverson peteharverson left a comment

Choose a reason for hiding this comment

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

Tested latest edits and LGTM

@walterra
Copy link
Contributor Author

walterra commented Jul 8, 2020

@elasticmachine merge upstream

@walterra
Copy link
Contributor Author

walterra commented Jul 8, 2020

@elasticmachine merge upstream

@walterra
Copy link
Contributor Author

walterra commented Jul 8, 2020

@elasticmachine merge upstream

@kibanamachine
Copy link
Contributor

💚 Build Succeeded

Build metrics

✅ unchanged

History

To update your PR or re-run it, just comment with:
@elasticmachine merge upstream

@walterra walterra merged commit 91377b2 into elastic:master Jul 8, 2020
@walterra walterra deleted the ml-improve-url-state branch July 8, 2020 20:58
walterra added a commit to walterra/kibana that referenced this pull request Jul 8, 2020
Makes two improvements to the urlState hook (also known as appState in some places):

- There was always a risk to run into a race condition because setUrlState could refer to a stale version of the state to act upon, for example if two calls were done in parallel. This is now fixed by using a local state copy of what we get from useLocation(). This allows us to use the callback version of useState's set function so we can make sure we always modify the latest state.
- Calls to history.push() are now gated by a check if the change actually referred to the corresponding instance of urlState (either _g or _a), this should reduce the updates resulting re-renders.

The two changes should make the use of setUrlState more safe against the pitfalls (race conditions/stale updates/lots of rerenders) we previously faced.
gmmorris added a commit to gmmorris/kibana that referenced this pull request Jul 9, 2020
* master: (39 commits)
  [APM] Add warning to notify user about legacy ML jobs (elastic#71030)
  updates consumer to siem (elastic#71117)
  Index pattern creation flow - fix spelling (elastic#71192)
  [Security Solution][Endpoint] User Manifest Cleanup + Artifact Compression (elastic#70759)
  [SECURITY] Rearrange rule name's column in Alert Table (elastic#71020)
  [SECURITY] Alerts back to Detections (elastic#71142)
  [Security Solution][Exceptions Builder] - Fixes operator selection bug (elastic#71178)
  [SIEM][Detection Engine] Speeds up value list imports by enabling streaming of files.
  [APM] Update ML job ID in data telemetry tasks (elastic#71044)
  [Resolver] Remove `currentPanelView` selector (elastic#71154)
  add meta.managed to index templates (elastic#71135)
  Clarify trial subscription levels (elastic#70900)
  [Security Solution] fix panel links (elastic#71148)
  skip flaky suite (elastic#69632)
  skip suite failing ES Promotion (elastic#71018)
  [ML] DF Analytics: add results field to wizard and show regression stats (elastic#70893)
  [SIEM] update wordings (elastic#71119)
  [SECURITY SOLUTION] Rename to hosts and administration (elastic#70913)
  [ML] Improvements for urlState hook. (elastic#70576)
  Removing uptime guide (elastic#71124)
  ...
walterra added a commit that referenced this pull request Jul 9, 2020
Makes two improvements to the urlState hook (also known as appState in some places):

- There was always a risk to run into a race condition because setUrlState could refer to a stale version of the state to act upon, for example if two calls were done in parallel. This is now fixed by using a local state copy of what we get from useLocation(). This allows us to use the callback version of useState's set function so we can make sure we always modify the latest state.
- Calls to history.push() are now gated by a check if the change actually referred to the corresponding instance of urlState (either _g or _a), this should reduce the updates resulting re-renders.

The two changes should make the use of setUrlState more safe against the pitfalls (race conditions/stale updates/lots of rerenders) we previously faced.

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
:ml refactoring release_note:skip Skip the PR/issue when compiling release notes v7.9.0 v8.0.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants