-
-
Notifications
You must be signed in to change notification settings - Fork 32.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
[MenuList] Convert to a function component #14865
Conversation
* Provide access to MenuList internals via actions property * Changed Menu to interact with MenuList only via exposed actions * Adjusted Menu and MenuList tests accordingly
@material-ui/core: parsed: -0.13% 😍, gzip: +0.06% Details of bundle changes.Comparing: 395d925...defd2d3
|
I've seen the errors from the checks and will make those adjustments sometime tomorrow. Let me know if there are any code style changes I should make at the same time. |
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 like this a lot. Makes it very obvious what methods are public by never exposing any other instance variable and makes testing easier at the same time.
@@ -6,66 +6,95 @@ import ReactDOM from 'react-dom'; | |||
import warning from 'warning'; | |||
import ownerDocument from '../utils/ownerDocument'; | |||
import List from '../List'; | |||
import getScrollbarSize from 'dom-helpers/util/scrollbarSize'; | |||
|
|||
function MenuList({ actions, children, className, onBlur, onKeyDown, disableListWrap, ...other }) { |
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.
We destructure in the function body. Just helps with formatting and many props.
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.
@eps1lon and @oliviertassinari -- I'll of course adhere to the standards agreed on by the core team, but I'm curious what the formatting issues are with many props. Hoping to see the formatting ugliness that happens, I switched List.js
to destructure props in the function declaration. After running prettier, it looked nearly identical to the previous version -- just without the const
at the beginning or the = props;
at the end. The indentation was completely identical. My personal preference is to destructure in the function declaration to avoid props
being available as an alternate way to access the same variables. I also think it would be a benefit to see the defaults at the declaration of the variable rather than scrolling down to defaultProps
. When the defaults are handled via plain JavaScript features, my IDE is also smarter about being able to instantly show me the default for the variable (a keyboard shortcut at any point of usage will show me the declaration). I'm just having a hard time seeing any downsides to destructuring in the function declaration.
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'm personally using destructuring as few as possible as a general rule. I see two advantages. Number one, it's easier to grep. I can delete code more easily as I have more grep surface. The ability to delete code is one of my top concern when writing it. However, it's less efficient with React components. Advantage number two, in a JavaScript codebase, without TypeScript or JSDoc, you maintain the semantics of the argument, otherwise, it's implicit. However, it's less a concern in React components as it's standardized.
Regarding the defaultProps
API. React might get ride of it reactjs/rfcs#107. There is an issue with react-docgen for the generation of the Markdown. @eps1lon has more context than me on it.
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'm don't have any strong preference either way. If we change the approach, my concern n°1 is consistency: do it one way everywhere, not a mix on a migration we never complete.
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.
Number one, it's easier to grep.
I initially agreed with that but with modern JS /function [A-Z]/
should be sufficient. I think this works just as well. For arrow function components const [A-Z].* = (
should do.
you maintain the semantics of the argument
In general yes but for function components the argument semantics are pre-determined. There won't be a function component that starts with context
or ref
. Always props
.
The reasons why I'm arguing for this is
My personal preference is to destructure in the function declaration to avoid props being available as an alternate way to access the same variables.
Especially in a hooks world the immediate destructuring won't even allow accessing props.someProp
in a hook. No need for an angry linter to yell at you. Second concern why destructuring should be preferred: Minification. Minification of object properties is pretty unstable as far as I know and conservative minifaction won't touch them. However destructuring helps minification:
// with destructuring
-const { somePrettyLongName } = props;
+const { somePrettyLongName: a } = props;
-const isItReallyLong = somePrettyLongName.length > 10;
+const b = a.length > 10;
-return <div className={somePrettyLongName} />;
+return <div className={a} />;
// without
const isItReallyLong = props.somePrettyLongName.length > 10;
return <div className={props.somePrettyLongName} />;
I'm OK with being inconsistent with argument destructuring here since function components aren't supposed to be treated as functions anyway. Calling them directly is rather rare.
If React follows through with the deprecation of defaultProps
we will have to adapt anyway. Enabling react-docgen
to parse the function body for default props is probably trivial but it's time spent nonetheless.
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.
@eps1lon 👍
); | ||
|
||
return React.cloneElement(child, { | ||
tabIndex: index === currentTabIndex ? 0 : -1, |
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.
Do we need to change the tab index? Could we keep -1 (menu), 0 (dropdown) or nothing (comboxbox) depending on the mode? If we might be able to can kill the currentTabIndex
state.
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 intend on tackling that next, but I want to take some time to layout all the different scenarios related to tab index in a separate issue and make sure I know what we want the behavior to be in each. It isn't clear to me yet whether or not the dropdown case can be that simple.
…scrollbar size * scrollbar size is 0px with jsdom, but various sizes in Karma tests with different browsers
* Moved resetTabIndex out of the component so it doesn't need to be an effect dependency * Removed no-longer-used (due to moving functionality to MenuList) imports from Menu
* Deconstruct props in function body * Remove redundant findDOMNode calls * Document StrictMode status of findDOMNode calls
* Unable to test this locally
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.
Let us know if you need any help to move the pull request forward :)
* Unable to test this locally
* Stub clientHeight by stubbing the getter (stubbing the value does not work on Chrome 41)
* Change test to use ref instead of wrapper.getDOMNode()
@eps1lon I believe all of your review points have been addressed. Let me know if any further changes are necessary. |
On a side note, I'm seeing intermittent Karma test errors in animate.test.js. It happened once in CI and I saw it once locally when executing the browserstack Karma tests. |
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.
On a side note, I'm seeing intermittent Karma test errors in animate.test.js. It happened once in CI and I saw it once locally when executing the browserstack Karma tests.
I have noticed it too, it's annoying. The test fail doesn't give us much information.
- Is the logic broken on Safari 10? I have tried the module on the Tabs demo page, it seems to work, plus it's flaky. So no.
- Does it return an error? We don't check the first argument. We could add an assertion
- Is this causing by a test leak?
@oliviertassinari Do you know how long ago you first noticed it? |
We recently switched from node 6 to 8 in CI. Might be some test that hits timing issues. |
@ryancogswell I have always seen our karma tests flaky. The SwipeableDrawer fails from time to time too. |
}, | ||
})); | ||
|
||
React.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.
We might need a different strategy here. We can't use any useLayoutEffect
for our components since this will disqualify them trigger a warning for server-side rendering with the current version of react. See facebook/react#14927. Might be a legitimate use case for useLayoutEffect
in SSR though.
I feel like the individual components shouldn't hold the selected
state but the MenuList
. As far as I can tell currentTabIndex
and components with selected
currently fight for the state ownership which might come with a whole host of problems in ConcurrentMode.
Maybe we can use #14585 and only handle imperative focus
here. Still leaves the issue of useLayoutEffect
on the server. Presenting useLayoutEffect
as a focus manager is much more presentable IMO than focus manager + finding children that match a predicate.
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.
My next chunk of work will involve evaluating the use of currentTabIndex
in detail and changing the approach. I would prefer to not change that notably in this pull request.
I think changing this useLayoutEffect
to useEffect
is doable. The main case to make sure I'm handling robustly is the auto-focus case for Menu -- I need to ensure that currentTabIndex
is correct when the focus
logic executes. This may require a ref for knowing whether or not we have already executed resetTabIndex
for mount and if focus
gets called first then go ahead and execute resetTabIndex
(and then skip doing it in the effect).
Of course, assuming Menu
eventually gets converted to a function component, the useLayoutEffect
question will come up again since that would be the most natural way to convert its componentDidMount
which executes the auto-focus logic.
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.
After looking at this more closely, I realized this was completely harmless to change from useLayoutEffect
to useEffect
. The tab index is not used for the auto-focus functionality, so it only matters once the user starts hitting the tab key, and for that purpose it is perfectly fine to wait for useEffect
.
The additional items @eps1lon mentioned in the follow-up review have now been addressed. |
Provide access to MenuList internals via actions property
Changed Menu to interact with MenuList only via exposed actions
Adjusted Menu and MenuList tests accordingly
No intended changes in behavior. This is prep work for #8191 and a follow-up to #14757.