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

[core] Improve getReactElementRef() utils #43022

Merged
merged 14 commits into from
Sep 19, 2024

Conversation

sai6855
Copy link
Contributor

@sai6855 sai6855 commented Jul 22, 2024

check #42818 (comment) for more context

@sai6855 sai6855 added package: utils Specific to the @mui/utils package enhancement This is not a bug, nor a new feature labels Jul 22, 2024
@mui-bot
Copy link

mui-bot commented Jul 22, 2024

Netlify deploy preview

https://deploy-preview-43022--material-ui.netlify.app/

Bundle size report

Details of bundle changes (Toolpad)
Details of bundle changes

Generated by 🚫 dangerJS against b84c85d

Copy link
Member

@aarongarciah aarongarciah left a comment

Choose a reason for hiding this comment

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

Left some comments, but let's wait for @oliviertassinari since he suggested the improvements.

packages/mui-material/src/utils/getChildRef.js Outdated Show resolved Hide resolved
packages/mui-material/src/utils/getChildRef.js Outdated Show resolved Hide resolved
packages/mui-material/src/utils/getChildRef.js Outdated Show resolved Hide resolved
return null;
if (process.env.NODE_ENV !== 'production') {
if (!React.isValidElement(child)) {
console.error(
Copy link
Member

@oliviertassinari oliviertassinari Jul 22, 2024

Choose a reason for hiding this comment

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

I'm missing something. Do we have a concrete example where this warning helps and is not harming (creating confusion)?

Copy link
Contributor Author

@sai6855 sai6855 Jul 22, 2024

Choose a reason for hiding this comment

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

You are right, console.error doesn't provide any help if child is invalid react element. Throwing error instead of logging error seems to improve DX.

Code used to get below errors

 <Slide direction="up" in={checked} mountOnEnter unmountOnExit>
        <p>sample</p>
        <p>sample</p>
  </Slide>

when just console.error is used
image

When thowing error instead of console.error

image

Copy link
Member

@oliviertassinari oliviertassinari Jul 22, 2024

Choose a reason for hiding this comment

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

@sai6855 I think this example

<Slide direction="up" in={checked} mountOnEnter unmountOnExit>
  <p>sample</p>
  <p>sample</p>
</Slide>

supports the previous argument I was trying to develop in #42818 (comment): we increase the bundle size for end-users, so we degrade the UX. But on the opposite side, the error is still not actionable, so we don't improve the DX.
I conclude that it makes things worse.

Previously, the experience was (without TypeScript):

SCR-20240723-bery

which feels equal.

So this didff looks like a step forward to me. Simpler, less bundle size, faster runtime:

diff --git a/packages/mui-material/src/utils/getChildRef.js b/packages/mui-material/src/utils/getChildRef.js
index a1d0c2aec5..c24cd926d3 100644
--- a/packages/mui-material/src/utils/getChildRef.js
+++ b/packages/mui-material/src/utils/getChildRef.js
@@ -1,9 +1,4 @@
-import * as React from 'react';
-
 export default function getChildRef(child) {
-  if (!child || !React.isValidElement(child)) {
-    return null;
-  }
   // 'ref' is passed as prop in React 19, whereas 'ref' is directly attached to children in React 18
   // below check is to ensure 'ref' is accessible in both cases
   return child.props.propertyIsEnumerable('ref') ? child.props.ref : child.ref;

Now, if we want to avoid the confusion for developers, to not have an error thrown, but only a console error message, I could see us doing. This feels like a better DX:

diff --git a/packages/mui-material/src/Slide/Slide.js b/packages/mui-material/src/Slide/Slide.js
index d669e811d6..5b3a7a71ed 100644
--- a/packages/mui-material/src/Slide/Slide.js
+++ b/packages/mui-material/src/Slide/Slide.js
@@ -119,6 +119,10 @@ const Slide = React.forwardRef(function Slide(props, ref) {
     ...other
   } = props;

+  // Invalid children provided, bailout
+  if (!React.isValidElement(children)) {
+    return children;
+  }
+
   const childrenRef = React.useRef(null);
   const handleRef = useForkRef(getChildRef(children), childrenRef, ref);

diff --git a/packages/mui-material/src/utils/getChildRef.js b/packages/mui-material/src/utils/getChildRef.js
index a1d0c2aec5..c24cd926d3 100644
--- a/packages/mui-material/src/utils/getChildRef.js
+++ b/packages/mui-material/src/utils/getChildRef.js
@@ -1,9 +1,4 @@
-import * as React from 'react';
-
 export default function getChildRef(child) {
-  if (!child || !React.isValidElement(child)) {
-    return null;
-  }
   // 'ref' is passed as prop in React 19, whereas 'ref' is directly attached to children in React 18
   // below check is to ensure 'ref' is accessible in both cases
   return child.props.propertyIsEnumerable('ref') ? child.props.ref : child.ref;

Now, there will also be the React 19 prop-types feedback problem for people now using types or using any:

SCR-20240723-bfup

There, I think that we simply need to create new utils, inspired by facebook/react#28328, and do:

diff --git a/packages/mui-material/src/Slide/Slide.js b/packages/mui-material/src/Slide/Slide.js
index d669e811d6..f316556579 100644
--- a/packages/mui-material/src/Slide/Slide.js
+++ b/packages/mui-material/src/Slide/Slide.js
@@ -1,6 +1,8 @@
 'use client';
 import * as React from 'react';
 import PropTypes from 'prop-types';
+import getDisplayName from '@mui/utils/getDisplayName';
+import checkPropTypes from 'prop-types/checkPropTypes';
 import { Transition } from 'react-transition-group';
 import chainPropTypes from '@mui/utils/chainPropTypes';
 import HTMLElementType from '@mui/utils/HTMLElementType';
@@ -82,11 +84,20 @@ export function setTranslateValue(direction, node, containerProp) {
   }
 }

+function muiCheckPropTypes(Component, props,) {
+  if (process.env.NODE_ENV === 'production') {
+    return;
+  }
+  return checkPropTypes(Component.propTypes, props, 'prop', getDisplayName(Component));
+}
+
 /**
  * The Slide transition is used by the [Drawer](/material-ui/react-drawer/) component.
  * It uses [react-transition-group](https://github.com/reactjs/react-transition-group) internally.
  */
-const Slide = React.forwardRef(function Slide(props, ref) {
+const Slide = React.forwardRef(function SlideIn(props, ref) {
+  muiCheckPropTypes(Slide, props);
+

cc @mui/code-infra

Copy link
Member

@Janpot Janpot Jul 23, 2024

Choose a reason for hiding this comment

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

This feels like a better DX:

I second that.

There, I think that we simply need to create new utils

Sure, I perhaps would consider moving the condition around muiCheckPropTypes(Slide, props); to shave off a few more bytes and prevent an unnecessary empty function call.

+const Slide = React.forwardRef(function SlideIn(props, ref) {
+  if (process.env.NODE_ENV !== 'production') {
+    muiCheckPropTypes(Slide, props);
+  }
+

Do we have a CI check that verifies certain utilities don't make it into production bundles?

Copy link
Member

@oliviertassinari oliviertassinari Jul 23, 2024

Choose a reason for hiding this comment

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

I perhaps would consider moving the condition around muiCheckPropTypes(Slide, props); to shave off a few more bytes and prevent an unnecessary empty function call.

Fair enough.

It's also missing a React.version check to not warn twice in React 18 and lower.

Copy link
Member

Choose a reason for hiding this comment

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

Opportunity moved to #43138

@oliviertassinari oliviertassinari added the React 19 support PRs required to support React 19 label Jul 22, 2024
@oliviertassinari oliviertassinari changed the title [material-ui] refactor getChildRef util [material-ui] Improve getChildRef() Jul 22, 2024
@oliviertassinari
Copy link
Member

oliviertassinari commented Aug 1, 2024

@sai6855 The changes make sense to me now. We are back to a local maximum 👍.

If we want to get to the next local maximum, I would see:

diff --git a/packages/mui-material/src/Slide/Slide.js b/packages/mui-material/src/Slide/Slide.js
index d669e811d6..5b3a7a71ed 100644
--- a/packages/mui-material/src/Slide/Slide.js
+++ b/packages/mui-material/src/Slide/Slide.js
@@ -119,6 +119,10 @@ const Slide = React.forwardRef(function Slide(props, ref) {
     ...other
   } = props;

+  // Invalid children provided, bailout
+  if (!React.isValidElement(children)) {
+    return children;
+  }
+
   const childrenRef = React.useRef(null);
   const handleRef = useForkRef(getChildRef(children), childrenRef, ref);

combined with #43138, but better to be iterative.

@github-actions github-actions bot added the PR: out-of-date The pull request has merge conflicts and can't be merged label Aug 1, 2024
@aarongarciah
Copy link
Member

@sai6855 as part of the React 19 effort, @DiegoAndai recently moved getChildRef to mui-utils under a new name: getReactNodeRef. Can you rebase the PR and apply the potential changes there?

@github-actions github-actions bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged label Aug 9, 2024
sai6855 added a commit to sai6855/material-ui that referenced this pull request Sep 15, 2024
@sai6855 sai6855 marked this pull request as draft September 15, 2024 10:26
@sai6855
Copy link
Contributor Author

sai6855 commented Sep 15, 2024

Summary of changes done in this PR

  1. Removed !element || !React.isValidElement(element) check here, as it was decided it's better DX to move error up to component level here
  2. Added tests for getReactElementRef https://github.com/mui/material-ui/pull/43022/files#diff-55a56a78280026badefee15f47b8823274fe80e1ca3f4a8426c3ffb53d57c1a1 and https://github.com/mui/material-ui/pull/43022/files#diff-54bc148947b0c3f0a3e6872123e20b9615bf2940c1189ce27124597a5233746d
  3. Renamed getReactNodeRef to getReactElementRef
  4. Updated usage of getReactElementRef in Portal component here and here
  5. Updated logic to check for ref in different react versions here d05cdaf based on [core] Improve getReactElementRef() utils #43022 (comment)

@sai6855 sai6855 marked this pull request as ready for review September 15, 2024 12:24
Copy link
Member

@aarongarciah aarongarciah left a comment

Choose a reason for hiding this comment

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

Looks good.

I don't have a strong opinion on using propertyIsEnumerable vs check the React major. Although I tend to prefer to check features over versions, the version check works well, too. If someone has a strong opinion, speak now 😄

@aarongarciah
Copy link
Member

Thank you @sai6855!

@aarongarciah aarongarciah merged commit 3c83c7d into mui:master Sep 19, 2024
19 checks passed
@oliviertassinari oliviertassinari changed the title [material-ui] Improve getReactElementRef() utils [core] Improve getReactElementRef() utils Oct 2, 2024
@oliviertassinari oliviertassinari added the core Infrastructure work going on behind the scenes label Oct 2, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
breaking change core Infrastructure work going on behind the scenes enhancement This is not a bug, nor a new feature package: utils Specific to the @mui/utils package React 19 support PRs required to support React 19
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants