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] Warn when defaultValue changes #19070

Merged
merged 17 commits into from
Jan 9, 2020

Conversation

m4theushw
Copy link
Member

Fixes #18956

This PR adds support to update the value when the defaultValue prop is changed after initial rendering. New default values are only propagated when the component is not dirty, to preserve the values the user may already have selected.

@mui-pr-bot
Copy link

mui-pr-bot commented Jan 4, 2020

@material-ui/core: parsed: +0.13% , gzip: +0.22%
@material-ui/lab: parsed: +0.23% , gzip: +0.31%

Details of bundle changes.

Comparing: 89cbce9...19aeedd

bundle Size Change Size Gzip Change Gzip
Autocomplete ▲ +1.1 kB (+0.85% ) 131 kB ▲ +351 B (+0.86% ) 41.2 kB
useAutocomplete ▲ +1.07 kB (+8.10% ) 14.3 kB ▲ +308 B (+6.32% ) 5.18 kB
Checkbox ▲ +1.03 kB (+1.26% ) 83.2 kB ▲ +334 B (+1.28% ) 26.4 kB
Switch ▲ +1.03 kB (+1.27% ) 82.4 kB ▲ +329 B (+1.28% ) 26.1 kB
Radio ▲ +1.03 kB (+1.24% ) 84.3 kB ▲ +319 B (+1.21% ) 26.7 kB
SpeedDialAction ▲ +1.03 kB (+0.88% ) 118 kB ▲ +300 B (+0.81% ) 37.5 kB
Slider ▲ +1.03 kB (+1.36% ) 76.8 kB ▲ +326 B (+1.36% ) 24.4 kB
RadioGroup ▲ +1.02 kB (+1.61% ) 64.7 kB ▲ +92 B (+0.46% ) 20.1 kB
TextField ▲ +1.02 kB (+0.82% ) 125 kB ▲ +296 B (+0.82% ) 36.5 kB
TablePagination ▲ +1.02 kB (+0.72% ) 142 kB ▲ +291 B (+0.70% ) 41.8 kB
Select ▲ +1.02 kB (+0.89% ) 116 kB ▲ +283 B (+0.83% ) 34.5 kB
Tooltip ▲ +1.02 kB (+1.01% ) 102 kB ▲ +296 B (+0.92% ) 32.4 kB
ExpansionPanel ▲ +767 B (+1.07% ) 72.4 kB ▲ +268 B (+1.19% ) 22.8 kB
@material-ui/core ▲ +458 B (+0.13% ) 359 kB ▲ +216 B (+0.22% ) 98.6 kB
@material-ui/lab ▲ +420 B (+0.23% ) 184 kB ▲ +169 B (+0.31% ) 55 kB
docs.main ▲ +327 B (+0.05% ) 615 kB ▲ +129 B (+0.07% ) 196 kB
TreeView ▲ +270 B (+0.41% ) 66.9 kB ▲ +113 B (+0.54% ) 21.1 kB
@material-ui/core[umd] ▲ +144 B (+0.05% ) 314 kB ▲ +73 B (+0.08% ) 90.8 kB
Typography ▲ +5 B (+0.01% ) 63.9 kB ▼ -1 B (-0.00% ) 20.1 kB
SwipeableDrawer ▲ +4 B (0.00% ) 92.1 kB ▲ +43 B (+0.15% ) 29 kB
InputBase ▲ +4 B (+0.01% ) 70.8 kB ▲ +2 B (+0.01% ) 22.3 kB
ButtonBase ▲ +4 B (+0.01% ) 74.2 kB ▲ +1 B (0.00% ) 23.4 kB
Paper ▲ +4 B (+0.01% ) 62.6 kB -- 19.6 kB
SvgIcon ▲ +4 B (+0.01% ) 63.3 kB -- 19.8 kB
Drawer ▲ +3 B (0.00% ) 84.7 kB ▼ -1 B (-0.00% ) 25.9 kB
Modal ▲ +2 B (+0.01% ) 14.3 kB ▲ +22 B (+0.44% ) 5.03 kB
Dialog ▲ +2 B (0.00% ) 83 kB ▼ -13 B (-0.05% ) 26 kB
Popover ▲ +2 B (0.00% ) 83 kB ▲ +5 B (+0.02% ) 25.8 kB
Grow ▲ +2 B (+0.01% ) 23.9 kB ▲ +1 B (+0.01% ) 8.21 kB
IconButton ▲ +2 B (0.00% ) 76.4 kB ▲ +1 B (0.00% ) 23.9 kB
Menu ▲ +2 B (0.00% ) 88.7 kB ▲ +1 B (0.00% ) 27.4 kB
Box ▼ -1 B (-0.00% ) 71 kB ▲ +19 B (+0.09% ) 21.7 kB
@material-ui/system ▼ -1 B (-0.01% ) 14.5 kB ▲ +10 B (+0.25% ) 4.05 kB
MenuItem ▲ +1 B (0.00% ) 78.4 kB ▲ +4 B (+0.02% ) 24.6 kB
Fab ▲ +1 B (0.00% ) 77 kB ▲ +2 B (+0.01% ) 24.1 kB
Fade ▲ +1 B (0.00% ) 23.3 kB ▼ -2 B (-0.03% ) 7.99 kB
ListItem ▲ +1 B (0.00% ) 77.3 kB ▲ +2 B (+0.01% ) 24.3 kB
Backdrop ▲ +1 B (0.00% ) 68 kB ▲ +1 B (0.00% ) 21.1 kB
Chip ▲ +1 B (0.00% ) 82.8 kB ▲ +1 B (0.00% ) 25.5 kB
Collapse ▲ +1 B (0.00% ) 68.1 kB ▲ +1 B (0.00% ) 21.2 kB
FilledInput ▲ +1 B (0.00% ) 73.8 kB ▲ +1 B (0.00% ) 23 kB
FormControl ▲ +1 B (0.00% ) 64.6 kB ▲ +1 B (0.00% ) 20.2 kB
Input ▲ +1 B (0.00% ) 72.7 kB ▲ +1 B (0.00% ) 22.8 kB
MenuList ▲ +1 B (0.00% ) 66.2 kB ▲ +1 B (0.00% ) 20.8 kB
OutlinedInput ▲ +1 B (0.00% ) 74.2 kB ▲ +1 B (0.00% ) 23.2 kB
StepIcon ▲ +1 B (0.00% ) 64.9 kB ▼ -1 B (-0.00% ) 20.3 kB
Button ▲ +1 B (0.00% ) 79.9 kB -- 24.6 kB
FormLabel ▲ +1 B (0.00% ) 63.7 kB -- 19.8 kB
InputLabel ▲ +1 B (0.00% ) 65.6 kB -- 20.3 kB
List ▲ +1 B (0.00% ) 62.6 kB -- 19.6 kB
Grid -- 65.3 kB ▼ -15 B (-0.07% ) 20.6 kB
ButtonGroup -- 83.4 kB ▼ -8 B (-0.03% ) 25.6 kB
@material-ui/styles -- 51.3 kB ▲ +7 B (+0.05% ) 15.5 kB
AvatarGroup -- 62.5 kB ▲ +7 B (+0.04% ) 19.7 kB
NativeSelect -- 77 kB ▲ +7 B (+0.03% ) 24.4 kB
TableBody -- 62.4 kB ▼ -7 B (-0.04% ) 19.6 kB
Alert -- 84 kB ▲ +5 B (+0.02% ) 26.4 kB
ToggleButton -- 76.4 kB ▲ +5 B (+0.02% ) 24.3 kB
DialogContent -- 62.5 kB ▼ -4 B (-0.02% ) 19.7 kB
DialogContentText -- 64.3 kB ▲ +3 B (+0.01% ) 20.3 kB
ExpansionPanelSummary -- 78.3 kB ▲ +3 B (+0.01% ) 24.8 kB
GridListTile -- 64 kB ▲ +3 B (+0.01% ) 20.1 kB
MobileStepper -- 68.1 kB ▲ +3 B (+0.01% ) 21.4 kB
SpeedDial -- 86.3 kB ▲ +3 B (+0.01% ) 27.3 kB
StepButton -- 82.5 kB ▼ -3 B (-0.01% ) 26.2 kB
StepContent -- 69.2 kB ▼ -3 B (-0.01% ) 21.8 kB
TableCell -- 64.3 kB ▲ +3 B (+0.01% ) 20.3 kB
AppBar -- 64.2 kB ▲ +2 B (+0.01% ) 20.2 kB
Breadcrumbs -- 68.2 kB ▼ -2 B (-0.01% ) 21.5 kB
Card -- 63.1 kB ▲ +2 B (+0.01% ) 19.8 kB
CardContent -- 62.2 kB ▲ +2 B (+0.01% ) 19.6 kB
CircularProgress -- 64.4 kB ▲ +2 B (+0.01% ) 20.4 kB
DialogActions -- 62.3 kB ▲ +2 B (+0.01% ) 19.6 kB
Divider -- 62.8 kB ▼ -2 B (-0.01% ) 19.8 kB
ExpansionPanelActions -- 62.3 kB ▼ -2 B (-0.01% ) 19.6 kB
Icon -- 63 kB ▲ +2 B (+0.01% ) 19.8 kB
ListItemSecondaryAction -- 62.3 kB ▼ -2 B (-0.01% ) 19.6 kB
Popper -- 28.7 kB ▲ +2 B (+0.02% ) 10.3 kB
Slide -- 25.3 kB ▲ +2 B (+0.02% ) 8.72 kB
StepConnector -- 63 kB ▼ -2 B (-0.01% ) 19.9 kB
TableSortLabel -- 77.6 kB ▲ +2 B (+0.01% ) 24.5 kB
Tabs -- 85.7 kB ▲ +2 B (+0.01% ) 27.3 kB
ToggleButtonGroup -- 63.5 kB ▼ -2 B (-0.01% ) 20 kB
TreeItem -- 74 kB ▼ -2 B (-0.01% ) 23.5 kB
useMediaQuery -- 2.5 kB ▼ -2 B (-0.19% ) 1.05 kB
Avatar -- 65.5 kB ▼ -1 B (-0.00% ) 20.7 kB
CardActionArea -- 75.3 kB ▲ +1 B (0.00% ) 23.8 kB
CardActions -- 62.3 kB ▼ -1 B (-0.01% ) 19.6 kB
CardMedia -- 62.6 kB ▼ -1 B (-0.01% ) 19.8 kB
CssBaseline -- 57.8 kB ▼ -1 B (-0.01% ) 18.2 kB
ExpansionPanelDetails -- 62.2 kB ▲ +1 B (+0.01% ) 19.6 kB
FormControlLabel -- 65.8 kB ▲ +1 B (0.00% ) 20.7 kB
FormHelperText -- 63.5 kB ▲ +1 B (0.00% ) 20 kB
Hidden -- 66.2 kB ▼ -1 B (-0.00% ) 20.8 kB
InputAdornment -- 65.3 kB ▼ -1 B (-0.00% ) 20.6 kB
ListItemAvatar -- 62.4 kB ▼ -1 B (-0.01% ) 19.6 kB
ListItemIcon -- 62.4 kB ▲ +1 B (+0.01% ) 19.6 kB
ListSubheader -- 63 kB ▲ +1 B (+0.01% ) 19.9 kB
NoSsr -- 2.19 kB ▲ +1 B (+0.10% ) 1.04 kB
Portal -- 2.9 kB ▼ -1 B (-0.08% ) 1.3 kB
Rating -- 70.7 kB ▲ +1 B (0.00% ) 22.8 kB
RootRef -- 4.21 kB ▼ -1 B (-0.06% ) 1.64 kB
Snackbar -- 75.4 kB ▲ +1 B (0.00% ) 23.7 kB
SnackbarContent -- 63.8 kB ▼ -1 B (-0.00% ) 20.2 kB
Step -- 62.9 kB ▼ -1 B (-0.01% ) 19.8 kB
StepLabel -- 68.8 kB ▲ +1 B (0.00% ) 21.7 kB
Tab -- 76.6 kB ▲ +1 B (0.00% ) 24.3 kB
TableContainer -- 62.2 kB ▲ +1 B (+0.01% ) 19.6 kB
TextareaAutosize -- 5.09 kB ▲ +1 B (+0.05% ) 2.14 kB
Toolbar -- 62.6 kB ▼ -1 B (-0.01% ) 19.7 kB
AlertTitle -- 64.4 kB -- 20.4 kB
Badge -- 65.6 kB -- 20.5 kB
BottomNavigation -- 62.7 kB -- 19.7 kB
BottomNavigationAction -- 75.7 kB -- 24 kB
CardHeader -- 65.3 kB -- 20.6 kB
ClickAwayListener -- 3.85 kB -- 1.54 kB
colorManipulator -- 3.85 kB -- 1.52 kB
Container -- 63.5 kB -- 19.9 kB
DialogTitle -- 64.5 kB -- 20.3 kB
docs.landing -- 50.9 kB -- 13.5 kB
FormGroup -- 62.3 kB -- 19.6 kB
GridList -- 62.7 kB -- 19.8 kB
GridListTileBar -- 63.5 kB -- 20 kB
LinearProgress -- 65.6 kB -- 20.5 kB
Link -- 66.8 kB -- 21.2 kB
ListItemText -- 65.2 kB -- 20.6 kB
Skeleton -- 63.2 kB -- 20.1 kB
SpeedDialIcon -- 64.8 kB -- 20.4 kB
Stepper -- 65.1 kB -- 20.6 kB
styles/createMuiTheme -- 16.5 kB -- 5.85 kB
Table -- 62.8 kB -- 19.8 kB
TableFooter -- 62.4 kB -- 19.6 kB
TableHead -- 62.4 kB -- 19.6 kB
TableRow -- 62.8 kB -- 19.8 kB
Zoom -- 23.4 kB -- 8.11 kB

Generated by 🚫 dangerJS against 19aeedd

@oliviertassinari
Copy link
Member

oliviertassinari commented Jan 4, 2020

@m4theushw Thanks for looking at the issue

  • Why should we go down this direction?
  • If we do go down this direction, we should be consistent and update the logic of all the other components.
  • At this point, we likely want to write an abstraction around it, the same pattern is repeated over and over.
  • Regarding the change on its own, I don't think that we need to use an effect. It would be better with a straight update (with a conditional logic).

@oliviertassinari oliviertassinari added the core Infrastructure work going on behind the scenes label Jan 4, 2020
@oliviertassinari oliviertassinari changed the title [Autocomplete] Add support to update defaultValue prop after initial rendering [core] Add support to update defaultValue prop after initial rendering Jan 4, 2020
@oliviertassinari oliviertassinari changed the title [core] Add support to update defaultValue prop after initial rendering [core] Add support to update defaultValue prop Jan 4, 2020
@m4theushw
Copy link
Member Author

If we do go down this direction, we should be consistent and update the logic of all the other components.

Yes, I didn't know this pattern was repeated. <Slider /> and <SelectInput /> also have a defaultValue prop.

At this point, we likely want to write an abstraction around it, the same pattern is repeated over and over.

The other approach to solve this problem is a warning. I custom hook useWarnWhenPropChange could be created to warn when some prohibited props change.

Regarding the change on its own, I don't think that we need to use an effect. It would be better with a straight update (with a conditional logic).

Without an effect, how I'll know when the defaultValue prop has changed?

@oliviertassinari
Copy link
Member

oliviertassinari commented Jan 4, 2020

@m4theushw I had forgotten about facebook/react#12710. As you can see in https://codesandbox.io/s/sad-sun-jbt4f, people shouldn't expect to be able to update the defaultValue prop. So I strongly think that we should go down the warning path. @joshwooding has started to look into an abstraction for the controlled/uncontrolled logic in https://github.com/mui-org/material-ui/pull/18357/files#diff-9c7e0ccdda79ab710195dd1b74c7df2c. Do you want to push it further?

@oliviertassinari oliviertassinari changed the title [core] Add support to update defaultValue prop [core] Warn when defaultValue changes Jan 4, 2020
@oliviertassinari oliviertassinari added the PR: needs revision The pull request can't be merged. More details is available on the code review or fails in the CI label Jan 4, 2020
@m4theushw
Copy link
Member Author

@oliviertassinari Yes, I'll redo my PR to introduce a custom hook with the warning. Do you think is better to make this hook specifically for defaultValue prop or accept an array of prop names to watch?

@oliviertassinari
Copy link
Member

@m4theushw I haven't looked closely at how we could make it work. I think that it would be great if it could handle one prop with it's controlled and uncontrolled variations as well as the warning. For instance:

diff --git a/packages/material-ui/src/Slider/Slider.js b/packages/material-ui/src/Slider/Slider.js
index 93ab41f56..c267d818a 100644
--- a/packages/material-ui/src/Slider/Slider.js
+++ b/packages/material-ui/src/Slider/Slider.js
@@ -371,28 +371,10 @@ const Slider = React.forwardRef(function Slider(props, ref) {
   const [active, setActive] = React.useState(-1);
   const [open, setOpen] = React.useState(-1);

-  const { current: isControlled } = React.useRef(valueProp != null);
-  const [valueState, setValueState] = React.useState(defaultValue);
-  const valueDerived = isControlled ? valueProp : valueState;
-
-  if (process.env.NODE_ENV !== 'production') {
-    // eslint-disable-next-line react-hooks/rules-of-hooks
-    React.useEffect(() => {
-      if (isControlled !== (valueProp != null)) {
-        console.error(
-          [
-            `Material-UI: A component is changing ${
-              isControlled ? 'a ' : 'an un'
-            }controlled Slider to be ${isControlled ? 'un' : ''}controlled.`,
-            'Elements should not switch from uncontrolled to controlled (or vice versa).',
-            'Decide between using a controlled or uncontrolled Slider ' +
-              'element for the lifetime of the component.',
-            'More info: https://fb.me/react-controlled-components',
-          ].join('\n'),
-        );
-      }
-    }, [valueProp, isControlled]);
-  }
+  const [valueDerived, setValueState] = useControlled({
+    controlled: valueProp,
+    default: defaultValue,
+    name: 'Slider',
+  });

   const range = Array.isArray(valueDerived);
   const instanceRef = React.useRef();

@m4theushw
Copy link
Member Author

@oliviertassinari Nice, I'll work on your suggestion.

@oliviertassinari
Copy link
Member

It's taking shape, can't wait to review it :)

@m4theushw
Copy link
Member Author

@oliviertassinari Could you take a first look?

I noticed that isControlled is used in almost all components (except Tooltip) just to prevent calling setValue when it's uncontrolled. I think we could make this check in setValue. The hook could return an array, instead of an object, like below:

diff --git a/packages/material-ui/src/utils/useControlled.js b/packages/material-ui/src/utils/useControlled.js
index c76fb5a5d..4c86401d6 100644
--- a/packages/material-ui/src/utils/useControlled.js
+++ b/packages/material-ui/src/utils/useControlled.js
@@ -38,7 +38,13 @@ const useControlled = ({ controlled, default: defaultProp, name }) => {
     }, [JSON.stringify(defaultProp)]);
   }
 
-  return { value, setValue, isControlled };
+  const setValueIfUncontrolled = newValue => {
+    if (!isControlled) {
+      setValue(newValue);
+    }
+  };
+
+  return [value, setValueIfUncontrolled];
 };
 
 export default useControlled;

@oliviertassinari
Copy link
Member

oliviertassinari commented Jan 8, 2020

@m4theushw It looks awesome. I have added a test case and move the default prop change check away from production. Let us know if it sounds still look OK! Also, I have used a function over an arrow, it's more a convention we have in a the codebase (top-level => function, nested => arrow function).

The hook could return an array, instead of an object, like below:

This sounds great :). Do you want to make the changes? However, be careful with hook dependencies, the reference of the callback changes.

@oliviertassinari oliviertassinari removed the PR: needs revision The pull request can't be merged. More details is available on the code review or fails in the CI label Jan 8, 2020
Copy link
Member

@oliviertassinari oliviertassinari left a comment

Choose a reason for hiding this comment

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

I think that we have reached a point where it's good enough to be accepted, well done! But feel free to push further, we can always do better :).

Copy link
Member

@joshwooding joshwooding 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 the only concern I have is with the TreeView and handling expanded and selected using the hook but we can make changes then. Good work!

@oliviertassinari
Copy link
Member

oliviertassinari commented Jan 9, 2020

@joshwooding Thanks for having a look, I'm sure your prior effect has helped too :).

I suspect we will get feedback from the community on these changes, I suspect that a non-negligeable amount of project have cases where the default prop changes. I hope developers don't provide object with circular references, which would break JSON.stringify, but on the other hand, a strict referential comparison could raise false positives. Let's try and learn.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
core Infrastructure work going on behind the scenes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Autocomplete multiple defaultValue doesn't get updated
4 participants