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
Update useLayoutEffect dependencies to ensure TabLine resizes #3688
Conversation
@@ -227,7 +227,7 @@ function TabLine(props: TabLineProps) { | |||
setStyle(styleObj); | |||
} | |||
|
|||
}, [direction, setStyle, selectedTab, orientation, scale, selectedKey]); | |||
}, [direction, setStyle, selectedTab, selectedTab?.offsetWidth, selectedTab?.offsetLeft, orientation, scale, selectedKey]); |
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 think this may not update in all cases, only if we get lucky that the size update is tied to a state update. I think what we really want to do is attach a resizeObserver
An example of this changing outside of a state update is a font change
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.
Ahh, yep. I do remember seeing a comment on the issue about it not updating after a font change or FOUT type of thing, I just forgot to solve for it.
I'll take a look at the useResizeObserver
util and se what I can do 👍
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.
@snowystinger Okay, this should be more resilient now. Moved the main functionality into a callback, and pass that to an observer as well as keeping a useLayoutEffect
to catch the regular state changes.
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.
Thanks, will look again soon. We're in the midst of release planning and some other things at work, so apologies if it takes a few days
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.
Apologies for the delay. Tested locally, behavior looks good to me, the small comment below isn't a blocking one.
My only concern was orientation: 'vertical'
and whether the tab label would wrap in specific cases, but that case doesn't seem to happen with our current Tabs implementation.
|
||
useLayoutEffect(() => { | ||
onResize(); | ||
}, [onResize, direction, scale, selectedKey, orientation, selectedTab?.offsetLeft]); |
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.
Think we can get rid of some of these extra deps since they are already in the callback's dep array
}, [onResize, direction, scale, selectedKey, orientation, selectedTab?.offsetLeft]); | |
}, [onResize, scale, selectedKey, selectedTab?.offsetLeft]); |
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 don't think so—from my understanding, the useCallback
deps gives us a memoized callback function with non-stale values. And the useLayoutEffect
deps ensure we call that resize function any time its deps change.
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.
@LFDanLu is correct, the onResize function will change anytime its dep array changes (as you said, it's memoized based on that dep array), which will mean that it changes in this dependency array and cause the useLayoutEffect to run
however, I have a different concern. we still have offsetLeft in this dependency array, which means we're getting lucky and render happens to be running after some change in the browser rendered dom. we should never have a dom rendered measurement as a direct reference in our dependency arrays, it's only safe to read dom values during effects. Here's an overly simplified version of why, https://codesandbox.io/s/rough-butterfly-98ny0r?file=/src/App.js
Instead, we need to be figuring out what actually changed (a resize event?) and hooking up to that to get offsetLeft into state. This will ensure a re-render and therefore the line will be placed correctly. We may need to move the state up a component in order to accomplish this. I haven't looked into it, just noticed it when I scanned the review.
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.
Ah, yep! Not sure how I looked right over onResize being in the second dep array, haha.
I think I'm following your other concern. Can take another look sometime soon, though likely not until after holiday.
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.
That's fine, no rush. I've been trying to find time to look into it as well. Thanks for all the work you've done so far
Can probably take similar approach to font loading here e8829a8#diff-6fa8be0fd81806f4ca7cebb681f725674ae8cbebd3187fea8fd8939cac53fca7R120 |
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 finally got some time to look at this. I managed to remove the need for our resize observer.
This doesn't address the font, but a user can do that in their implementation by waiting to render until the font is loaded. I do want to provide a utility to help with this in the future, but I think it can be handled separately from this PR.
if (tabDimensions.length !== prevTabPositions.current.length || tabDimensions.some((box, index) => box?.left !== prevTabPositions.current[index]?.left && box?.right !== prevTabPositions.current[index]?.right)) { | ||
setTabPositions(tabDimensions); | ||
prevTabPositions.current = tabDimensions; |
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.
Works well enough, but I wonder if it would instead be sufficient to trigger an arbitrary state change when children
changes and provide that state to the context in place of tabLineState
. It would cause a couple of extra onResize
calls down in TabLine I guess if children changed but the exact sizing of each resulting tab didn't...
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.
Right, I thought of doing that, but this is the thing we actually care about changing. And as you point out, some children may change without changing the dimensions, so it saves us some potential extra renders.
Adds two additional dependencies to the
useLayoutEffect
inside ofTabLine
that will ensure it's width is updated properly when the text inside the tab item changes.Closes #2004
✅ Pull Request Checklist:
📝 Test Instructions:
I attempted to write a new test to check that the inline style for
width
is updated correctly, however because the value is calculated using element offsets, those values always end up at0
in a non-browser environment. So I added a new Storybook story instead to visualize the bugfix.Scenario 1
Tabs -> Tab 1 controlled TabList item
TabLine
grows/shrinks as expectedScenario 2
Tabs -> Tab 1 controlled TabList item
TabLine
for Tab 2 moves horizontally to maintain alignment beneath Tab 2