Reputation: 45
I have started to receive some errors and warnings:
Uncaught Error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering
Uncaught Error: Hydration failed because the initial UI does not match what was rendered on the server.
Warning: validateDOMNesting(...): <button> cannot appear as a descendant of <button>.
Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.
Uncaught Error: Hydration failed because the initial UI does not match what was rendered on the server.
Warning: Expected server HTML to contain a matching <button> in <button>.
Warning: Failed prop type: MUI: You are providing an onClick event listener to a child of a button element. Prefer applying it to the IconButton directly.
Based on a search of similar issues, I think somehow there might be a <button>
inside a <button>
, but I cannot see where or how that could've happened!
The errors started when I moved some JSX code from a component into a static method of a class, which I would like to use as a UI factory for multiple components that all would use the same JSX code.
The following code was perfectly fine before the change:
AppsButton.jsx
import * as React from 'react';
import { styled, useTheme } from '@mui/material/styles';
import Box from '@mui/material/Box';
import IconButton from '@mui/material/IconButton';
import AppsIcon from '@mui/icons-material/Apps';
// define a function outputting the view
function AppsButtonView (inputs) {
// use the MUI theme
const theme = useTheme();
// define the view
const view = (
<Box
sx = {{
boxSizing: 'border-box',
width: `calc(${theme.spacing(6)} + 1px)`
}}
>
<IconButton
aria-label = "open apps drawer"
edge = "start"
onClick = {inputs.callback}
sx = {{
color: inputs.toggleState? "grey" : "inherit",
backgroundColor: inputs.toggleState? "white" : "inherit",
}}
>
<AppsIcon />
</IconButton>
</Box>
);
} // function
// export the function
export default AppsButtonView
... which is called by the containing AppBar component:
AppBar.jsx
import * as React from 'react';
import Box from '@mui/material/Box';
import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
//import * as AppBarContent from './/AppBar'; // todo work out how to import as module/package
import AppBarModel from "./AppBar.vm" // todo: include in above?
import AppsButton from "./AppsButton";
import HomeButton from "./HomeButton";
import Typography from '@mui/material/Typography';
import HelpButton from "./HelpButton";
import HelpDrawer from "./HelpDrawer";
import AppsDrawer from "./AppsDrawer";
// define a function outputting the view
function AppBarView (inputs) {
// get the app bar model
const appBarModel = AppBarModel();
// define the view
const view = (
<Box sx={{ display: 'flex' }}>
<AppBar
position="fixed"
sx={{
zIndex: (theme) => theme.zIndex.drawer + 1
}}
>
<Toolbar>
<AppsButton
callback = {appBarModel.appsButtonCallback}
toggleState = {appBarModel.appsDrawerModel.state}
/>
<HomeButton visibility = {inputs.showHomeButton} callback = {appBarModel.homeButtonCallback}/>
<Typography
variant = "h6"
noWrap component = "div"
sx = {{
flex: 1,
}}
>
{inputs.title}
</Typography>
<HelpButton
callback = {appBarModel.helpButtonCallback}
toggleState = {appBarModel.helpDrawerModel.state}
/>
</Toolbar>
</AppBar>
<HelpDrawer model = {appBarModel.helpDrawerModel}/>
<AppsDrawer model = {appBarModel.appsDrawerModel}/>
</Box>
);
// output the view
return view
} // function
// export the function
export default AppBarView
So the change I made was to introduce a UI Factory:
Factory.js
import { styled, useTheme } from '@mui/material/styles';
import Box from '@mui/material/IconButton';
import IconButton from '@mui/material/IconButton';
/*
Facility to output consistent UI components.
*/
class UIFactory {
/*
Returns a material UI icon button in a box.
*/
static getBoxIconButton(inputs) {
// use the MUI theme
const theme = useTheme();
// define the icon button UI
const iconButton = (
<Box
sx = {{
boxSizing: 'border-box',
width: `calc(${theme.spacing(6)} + 1px)`
}}
>
<IconButton
color = "inherit"
aria-label = {inputs.ariaLabel}
edge = "start"
onClick = {inputs.callback}
>
{inputs.icon}
</IconButton>
</Box>
);
// output the icon button
return iconButton;
} // getBoxIconButton
} // class
// export the class
export default UIFactory
... which is now called in updated the AppsButton component like so:
AppsButton.jsx
import * as React from 'react';
import AppsIcon from '@mui/icons-material/Apps';
import UIFactory from "../Factory";
// define a function outputting the view
function AppsButtonView (inputs) {
// make a button view
const buttonDetails = {
ariaLabel: "open apps drawer",
icon: (<AppsIcon />),
callback: inputs.callback,
};
const view = UIFactory.getBoxIconButton(buttonDetails)
// output the view
return view
} // function
// export the function
export default AppsButtonView
This same concept I successfully applied already with a drawer getting a of , etc. from the UI Factory.
So in my head I just moved the code to another file to be called from there instead, what am I missing here?
How is there a <button>
inside a <button>
with this new structure?
Upvotes: 2
Views: 1777
Reputation: 45
So the answer to this question is in the question itself. There's actually nothing wrong with the code posted here, which works fine without the hydration error as I found out when copying back from this question in order to reproduce and demonstrate to someone. The code in the question came from the code used in my case and in copying to the question, I lost the real issue (which I still don't know - probably something small and silly).
Maybe an answer would be: "use sandboxing".
Upvotes: 1