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

Let the user settings hook just use the first defined value #42605

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

rafpaf
Copy link
Contributor

@rafpaf rafpaf commented May 13, 2024

This pull request adds a new option to to the useUserSetting hook: just use the first defined value. This helps avoid unnecessary re-renders, which avoids race conditions.

In the useUserSetting function, an additional parameter shouldUseFirstDefinedValue was added in this branch. If this parameter is true, only the first value retrieved from the API is used. This parameter is optional and set to false by default.

Copy link
Contributor Author

rafpaf commented May 13, 2024

This stack of pull requests is managed by Graphite. Learn more about stacking.

Join @rafpaf and the rest of your teammates on Graphite Graphite

@rafpaf rafpaf changed the title bring file over from other branch Improve the user settings hook May 13, 2024
@metabase-bot metabase-bot bot added the .Team/AdminWebapp Admin and Webapp team label May 13, 2024
@rafpaf rafpaf force-pushed the improve-user-settings-hook branch from a672a24 to 6628f19 Compare May 17, 2024 10:17
@rafpaf rafpaf marked this pull request as ready for review May 17, 2024 10:18
@graphite-app graphite-app bot requested a review from a team May 17, 2024 10:19
@graphite-app graphite-app bot added the no-backport Do not backport this PR to any branch label May 17, 2024
Copy link

graphite-app bot commented May 17, 2024

Graphite Automations

"Notify author when CI fails" took an action on this PR • (05/17/24)

1 teammate was notified to this PR based on Raphael Krut-Landau's automation.

"Don't backport" took an action on this PR • (05/17/24)

1 label was added and 1 reviewer was added to this PR based on Raphael Krut-Landau's automation.

Copy link

replay-io bot commented May 17, 2024

Status Complete ↗︎
Commit 6628f19
Results
⚠️ 1 Flaky
2526 Passed

Copy link
Contributor

@iethree iethree left a comment

Choose a reason for hiding this comment

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

  1. This really needs tests. This is essentially a caching problem which is really hard, and tests will help make sure it works as we expect, and clearly document those expectations.
  2. I'm not sure memoization like this is the right approach:

Here, we say, for the purposes of this hook, ignore all API values other than the first one. I think this is confusing to reason about, especially when multiple components are subscribing to the same setting.

I think what we really want here is aggressively optimistic updates - where we immediately update the value in the redux store and then don't update the redux store when the API request completes. That way we get completely synchronous behavior client side for these user settings, and hopefully save them server side, but it's no big deal if they don't. This also lets us avoid the weird back/forth race condition possibilities. I also think this should be the default behavior, maybe with the option to turn it off. WDYT?

Copy link
Contributor

@sloansparger sloansparger left a comment

Choose a reason for hiding this comment

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

I'd like to understand a bit more why this was needed. If useSetting is slow, we should fix it for everyone instead of opting in. If you have a singular use case for this, I'd opt for a more composable solution like:

const CheapToRerender = () => {
  const [setting] = useSetting('my-setting')
  const staleSetting = useFirstDefinedValue(setting);

  return (
     <ExpensiveToRerender thing={setting.attr} />
   )
}

Comment on lines +33 to +37
const memoizedValue = useMemo(
() => currentValue,
// eslint-disable-next-line react-hooks/exhaustive-deps -- Update only when currentValue first becomes defined
[currentValue === undefined],
);
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure this is what memoization is. If you provide a new key input (line 16) you'll get the previous key's output still. To me this is something like shouldUseFirstValue or shouldUseStaleValue.

return [currentValue, shouldDebounce ? debouncedSetter : setter];
return [
shouldMemoize ? memoizedValue : currentValue,
shouldDebounce ? debouncedSetter : setter,
Copy link
Contributor

Choose a reason for hiding this comment

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

If this were going to go through with this, I think there should be a setter that does something. You should be able to at the very least have your value update to the setter's value.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure I follow

Copy link
Contributor

Choose a reason for hiding this comment

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

const [value, set] = useSetting('thing', { shouldMemoize: true });
console.log(value) // 'a'

// somewhere else
set('b');

// next render
const [value, set] = useSetting('thing', { shouldMemoize: true });
console.log(value) // 'a' <-- still a even though we set it to b

@rafpaf
Copy link
Contributor Author

rafpaf commented May 17, 2024

@sloansparger

I'd like to understand a bit more why this was needed. If useSetting is slow, we should fix it for everyone instead of opting in. If you have a singular use case for this, I'd opt for a more composable solution like:

const CheapToRerender = () => {
  const [setting] = useSetting('my-setting')
  const staleSetting = useFirstDefinedValue(setting);

  return (
     <ExpensiveToRerender thing={setting.attr} />
   )
}

I've changed the prop to shouldUseFirstDefinedValue and it defaults to true.

@rafpaf
Copy link
Contributor Author

rafpaf commented May 17, 2024

@sloansparger

I'd like to understand a bit more why this was needed.

The prop makes it possible to remove a race condition in the Browse models filter: Avoid race condition in Browse models filter. The race condition could arise for any similar UI that tracks a value in the API, so a general solution seemed useful

@rafpaf rafpaf added this to the 0.50 milestone May 17, 2024
@rafpaf
Copy link
Contributor Author

rafpaf commented May 17, 2024

  1. This really needs tests. This is essentially a caching problem which is really hard, and tests will help make sure it works as we expect, and clearly document those expectations.

Agreed. Will write some tests.

  1. I'm not sure memoization like this is the right approach:

Here, we say, for the purposes of this hook, ignore all API values other than the first one. I think this is confusing to reason about, especially when multiple components are subscribing to the same setting.

I think what we really want here is aggressively optimistic updates - where we immediately update the value in the redux store and then don't update the redux store when the API request completes. That way we get completely synchronous behavior client side for these user settings, and hopefully save them server side, but it's no big deal if they don't. This also lets us avoid the weird back/forth race condition possibilities. I also think this should be the default behavior, maybe with the option to turn it off. WDYT?

I like this idea!

@rafpaf rafpaf removed this from the 0.50 milestone May 20, 2024
@rafpaf rafpaf changed the title Improve the user settings hook Let the user settings hook just use the first defined value May 20, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
no-backport Do not backport this PR to any branch .Team/AdminWebapp Admin and Webapp team
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants