-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Components: refactor Navigation
to pass exhaustive-deps
#41612
Conversation
Size Change: 0 B Total Size: 1.25 MB ℹ️ View Unchanged
|
Navigation
to pass exhaustive-deps
Navigation
to pass exhaustive-deps
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for working on this, @chad1008 !
I tested this in Storybook, and it seems to break the "controlled" behavior of the component:
- Open the "Controlled State" example
- Click on either the "Open the Nested Sub-Menu menu" or the "Navigate to Child 2 item" buttons
- Nothing happens (expected: the component navigates programmatically to another menu)
Personally I try to avoid wrapping variables in useRef
, as it makes the code harder and harder to follow — although I agree that, prior to this PR, this component was already written in a way that I found a bit convoluted (including a few variables around the "active" menu as a prop, internal state, and context value).
The exhaustive-deps
ESLint warnings are usually a sign that the current code patterns are not a good fit for the specific use case, and that the code could be refactored in a way that doesn't violate the exhaustive-deps
ESLint rule without needing workarounds that feel "hacky".
I actually opened #41668 to improve the unit tests for |
#41668 got merged, let's rebase this PR to see if the additional unit tests can help us to catch any potential regression 🤞 Update: they do! |
682fcd2
to
d2c2893
Compare
d2c2893
to
a1ac5de
Compare
Good catch. I see what I missed here: the This new fix addresses that, so the Controlled state works again, and I've confirmed the effect is firing the expected number of times. I also went ahead and combined these two values into a single ref. I felt like that made it more readable and clear what the ref was for, but I'm not sure how you feel about that pattern. Happy to split them up if that's preferred!
Thanks for that insight! I'll admit when I first encountered wrapping variables in I looked into a more complete refactor to resolve these issues another way but didn't get too far, so the current fix still employs |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can confirm that the component seems to work well after the latest changes, although I personally think that the current fix doesn't add much value to the component, while making the code more complex.
At this point, also given the fact that we'd like to move away from this component in the future, I'd prefer adding a comment to ignore ESLint's warning for that line, an an associated comment explaining why the ignore rule is in place (and maybe a link to this PR too).
@@ -50,9 +55,15 @@ export default function Navigation( { | |||
} | |||
}, [] ); | |||
|
|||
// Used to prevent excessive useEffect fires when navigation is being controlled by parent component | |||
const controlledMenuUpdate = useRef( { setActiveMenu, menu } ); | |||
useLayoutEffect( () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any particular reason why you opted for useLayoutEffect
instead of useEffect
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to make sure it's always up to date.. If I'd used a useEffect
here, and then some other future change tried to use this hook from a useLayoutEffect
, that would be a stale reference because layoutEffects
fire first.
Updating the ref with useLayoutEffect
here should ensure it's always up to date, regardless of what kind of effect tries to call it later.
Sounds good! I'll update the PR shortly! |
a1ac5de
to
c8bf77b
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM 🚀
Thank you for your patience, @chad1008 ! The time spent in this PR was definitely useful, even if we didn't end up applying your initial refactor.
…seEffect` handling menu `activeMenu` updates
Co-authored-by: Mitchell Austin <mr.fye@oneandthesame.net>
1a2d0a1
to
df2b594
Compare
Thanks for all the helpful feedback (and for that rebase!) @ciampo! |
What?
Updates the
Navigation
component tosatisfyignore theexhaustive-deps
eslint ruleWhy?
Part of the effort in #41166 to apply
exhuastive-deps
to the Components packageHow?
Update: Because we ultimately prefer
Navigator
toNavigation
, and fixing this component looks like it'd take either a heavier refactor or excessive reliance onuseRef
, we're option to disable the rule in this case rather than refactor the component.Missing dependency:menu
The component has an effect that checks to see if theactiveMenu
prop has been changed, which would currently only happen if it came from the parent component. The effect does this by checking theactiveMenu
prop against the currentmenu
state. If they don't match,menu
is updated toactiveMenu
. This effectively allows the parent component to control what menu or submenu is being displayed.We can't addmenu
as an effect dependency because ifmenu
was just changed (triggering a re-render and our effect) we know it no longer matchesactiveMenu
. This means the effect will updatemenu
back to once again matchactiveMenu
.To fix this, we can update a ref of the currentmenu
state value, and have the effect check against that instead. We no longer needmenu
in the dep array forexhaustive-deps
, so we won't be firing the effect on every submenu navigation that updates themenu
state.To keepmenu
andmenuRef
in state, we can use anotheruseEffect
call. The dependency onmenu
in this case will limit the ref updates for us.Missing dependency:setActiveMenu
AddingsetActivemenu
menu as a dep here is no good. We'd need to wrap it in auseCallback
and then its own dependencies would update too often, redeclaring the function and triggering the effect on all menu navigations. Instead, if we use a Ref of the function we eliminate the need for a dep, while still giving the effect access to the functionality.Testing Instructions
npx eslint --rule 'react-hooks/exhaustive-deps: warn' packages/components/src/navigation/index.js