How to make a reusable Material UI tabs component in React - reactjs

I am working on a React project, and I divided some of the pages into tabs using the MUI Tab component, so I can have multiple components in one page and render each component accordingly, so I have created the Tabs component. However, I can only see one first index tab.
Reusable tab MUI component:
export default function useTabs(props) {
const { children, value, index, label, ...other } = props;
const [selectedValue, setSelectedValue] = React.useState(0);
const handleChange = (event, newValue) => {
setSelectedValue(newValue);
};
return (
<Box sx={{ width: "100%" }}>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<Tabs
value={selectedValue}
onChange={handleChange}
className={classes.tab}
textColor="primary"
indicatorColor="primary"
>
<Tab label={label} {...a11yProps(0)} className={classes.tabText} />
{/* <Tab className={classes.tabText} label={label} {...a11yProps(1)} /> */}
</Tabs>
</Box>
<TabPanel value={selectedValue} index={index}>
{children} // Rendering tab children here, but getting only the first component
</TabPanel>
</Box>
);
}
Here is how I am using it:
// Import the reusable component
import Tab from "../common/Tabs";
export default function JobsRecruitments() {
return (
<>
<Tab label="tab name" index={0}>
<MyComponent />
</Tab>
</>
)
}

I don't think it's a good idea to use children if you want reusable tabs. That's because your tabs have to be consistent: if you have 3 child nodes, you should also have 3 tab labels. If you're using children, you can easily lose track of that.
However, if you want to make a reusable tab component, I would suggest passing an array of objects containing the tab label and Component as a property to your custom Tab component. Here's an example of what I mean:
export default function BasicTabs({ tabs }) {
const [value, setValue] = React.useState(0);
const handleChange = (event, newValue) => {
setValue(newValue);
};
return (
<Box sx={{ width: "100%" }}>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<Tabs
value={value}
onChange={handleChange}
aria-label="basic tabs example"
>
{tabs.map(({ label }, i) => (
<Tab label={label} key={i} />
))}
</Tabs>
</Box>
{tabs.map(({ Component }, i) => (
<TabPanel value={value} index={i} key={i}>
{Component}
</TabPanel>
))}
</Box>
);
}
And then you can use this component like this:
import Tabs from "./MyTabs";
const tabs = [
{
label: "Tab 1",
Component: <div>Hello, I am tab 1</div>
},
{
label: "Tab 2",
Component: <div>Hello, I am tab 2</div>
},
{
label: "Tab 3",
Component: (
<div>
<h1>Tab with heading</h1>
<p>Hello I am a tab with a heading</p>
</div>
)
}
];
export default function App() {
return (
<div>
<Tabs tabs={tabs} />
</div>
);
}
Here's the Codesandbox with the full example

Related

Get Child components constants/state in parent component in React

I have a component which is a MUI TAB like this
<Box sx={{ width: "100%" }}>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<Tabs
value={value}
onChange={handleChange}
aria-label="basic tabs example"
>
<Tab label="Item One" {...a11yProps(0)} />
<Tab label="Item Two" {...a11yProps(1)} />
<Tab label="Item Three" {...a11yProps(2)} />
</Tabs>
</Box>
<TabPanel value={value} index={0}>
<Child1 />
</TabPanel>
<TabPanel value={value} index={1}>
<Child2 />
</TabPanel>
<TabPanel value={value} index={2}>
Item Three
</TabPanel>
<Box>
<Button onClick={submittab}>Submit</Button>
</Box>
</Box>
As mentioned, there are 2 child components with some textfields in it.
What I am trying to achieve is to read the child data in parent and submit it to an API as a single object.
Please help
Below the Sandbox
https://codesandbox.io/s/naughty-shadow-u2icz1?file=/demo.tsx:1233-1962
Please note I am working in typescript
Here is my child component1
import { FormLabel, TextField } from "#mui/material";
import react, { useState } from "react";
function Child1() {
const clientObj = {
user: {
name: "",
age: ""
}
};
const [client, setClient] = useState(clientObj);
const handlechange = (event: { target: { name: any; value: any } }) => {
const { name, value } = event.target;
setClient((prevState) => ({
...prevState,
context: {
...prevState.user,
[name]: value
}
}));
};
return (
<>
<FormLabel>Name</FormLabel>
<TextField
name="name"
id="text-id"
value={client.user.name}
onChange={handlechange}
/>
<FormLabel>Age</FormLabel>
<TextField
name="name"
id="text-id"
value={client.user.name}
onChange={handlechange}
/>
</>
);
}
export default Child1;
You could use a state in your parent component that will be passed as props to your children, so that they can use the parent's update state function.
Parent code:
const [state, setState] = useState()
<Child parentState={{state, setState}} />
Children code:
export function Child({parentState}) {
const {parentStateValue, setParentState} = parentState
const childTitle = 'Acme'
return <button onClick={setParentState(e => {...e, childTitle })} >childTitle</button>
}
the ideal would be to provide keys to your children so that you can manage each of their data independently in the state.

React: Using a same component twice in a page removes the data in first component

I have two tabs in my react app with each tab having a spreadsheet.
I have called the same spreadsheet tag in both the tabs. when i enter data in the first tab and move to second tab, the data in the second tab is getting replaced with the default value which i am setting in the useState.
import * as React from 'react';
import PropTypes from 'prop-types';
import Tabs from '#mui/material/Tabs';
import Tab from '#mui/material/Tab';
import Typography from '#mui/material/Typography';
import Box from '#mui/material/Box';
import Spreadsheet from './components/Spreadsheet.js'
function TabPanel(props) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
{value === index && (
<Box sx={{ p: 3 }}>
<Typography>{children}</Typography>
</Box>
)}
</div>
);
}
TabPanel.propTypes = {
children: PropTypes.node,
index: PropTypes.number.isRequired,
value: PropTypes.number.isRequired,
};
function a11yProps(index) {
return {
id: `simple-tab-${index}`,
'aria-controls': `simple-tabpanel-${index}`,
};
}
export default function BasicTabs() {
const [value, setValue] = React.useState(0);
const handleChange = (event, newValue) => {
setValue(newValue);
};
return (
<Box sx={{ width: '100%' }}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={value} onChange={handleChange} aria-label="basic tabs example">
<Tab label="Item One" {...a11yProps(0)} />
<Tab label="Item Two" {...a11yProps(1)} />
<Tab label="Item Three" {...a11yProps(2)} />
</Tabs>
</Box>
<TabPanel value={value} index={0}>
Item One
<div id='div_spreadsheet1'>
<Spreadsheet id="spreadsheet1"> </Spreadsheet>
</div>
</TabPanel>
<TabPanel value={value} index={1}>
Item Two
<div id='div_spreadsheet12'>
<Spreadsheet id="spreadsheet2"> </Spreadsheet>
</div>
</TabPanel>
<TabPanel value={value} index={2}>
Item Three
</TabPanel>
</Box>
);
}
EDIT:
Adding the Spreadsheet.js code
import Spreadsheet from "react-spreadsheet";
import { useState } from "react";
export default function Sheet(){
const [data, setData] = useState([
[{ value: "" }, { value: "1" }],
]);
return(
<div>
<h4>SpreadSheet</h4>
<Spreadsheet data={data} onChange={setData} />
</div>
)
};
How to restrict this from happening. Please help.

React Material UI - Tabs - How to disable lazy loading of tabs?

I am creating some react tabs based off of the example shown here for Material UI: https://material-ui.com/components/tabs/.
The problem I am encountering is that my tabs are lazy loading the linegraph components instead of performing all the queries on initial page load. Does anyone have some suggestions of where I can optimize this to make each tab load immediately on page load?
function TabPanel(props) {
const { children, value, index} = props;
return (
<Typography>
{value === index && <Box p={3}>{children}</Box>}
</Typography>
);
}
class SimpleTabs extends Component {
constructor(props) {
super(props)
console.log(props.color)
this.state = {
value: 0
};
}
handleChange = (event, newValue) => {
this.setState({value: newValue});
};
render() {
return (
<div>
<AppBar position="static">
<Tabs value={this.state.value} onChange={this.handleChange}>
<Tab label="Item One"/>
<Tab label="Item Two"/>
<Tab label="Item Three"/>
</Tabs>
</AppBar>
<TabPanel value={this.state.value} index={0}>
<LineGraph className={styles.graphBox} name={"Guest Count"} url={'http://127.0.0.1:5000/***/***'} initialFilterData={this.props.initialFilterData} filterData={this.props.filterData}/>
</TabPanel>
<TabPanel value={this.state.value} index={1}>
<LineGraph className={styles.graphBox} name={"Total Sales"} url={'http://127.0.0.1:5000/***/***'} initialFilterData={this.props.initialFilterData} filterData={this.props.filterData}/>
</TabPanel>
<TabPanel value={this.state.value} index={2}>
<LineGraph className={styles.graphBox} name={"Avg Check Size"} url={'http://127.0.0.1:5000/***/***'} initialFilterData={this.props.initialFilterData} filterData={this.props.filterData}/>
</TabPanel>
</div>
)}
}
export default SimpleTabs;
Add hidden={value !== index} to the Typography in TabPanel and remove the value === index && condition inside the Typography so that the children of all the tab panels are rendered immediately but hidden (except for the currently selected one).
Example:
function TabPanel(props) {
const { children, value, index, ...other } = props;
return (
<Typography
component="div"
role="tabpanel"
hidden={value !== index}
{...other}
>
<Box p={3}>{children}</Box>
</Typography>
);
}

Using Grid with SortableJS

I am using SortableJS for React and I am trying to implement horizontal Grid, so that the final result would look like this:
I managed to do that, however the sort function does not work at all. I think it has something to do with the tag attribute. When I try tag={Grid} I get following error:
Sortable: el must be an HTMLElement, not [object Object]
Any ideas how can I implement Grid to the tag attribute? Here is my code:
<Grid container>
<Sortable options={{ fallbackOnBody: true, group: "items", handle: reorderHandle }} tag={Grid} onChange={handleChange}>
{items.map((item, index) =>
<Paper key={index} data-id={index} className={classes.item} elevation={0}>
<ButtonHelper {...getHandleClass(editable)} key={index} icon={
<Badge badgeContent={index + 1} color="default">
{editable && <ReorderIcon />}
</Badge>}
/>
<Grid item xs={4} key={index}>
<div className={classes.gridItem}>
<GalleryInput className={classes.image} style={{ width: '300px' }} label="image" source={`${source}[${index}].image`} />
<br></br>
<TextInput label="desc" source={`${source}[${index}].desc`} />
{editable && <ButtonHelper icon={<RemoveIcon />} onClick={handleRemove(index)} className={classes.left} />}
</div>
</Grid>
</Paper>
)}
</Sortable>
</Grid>
Thank you for any help.
Since you are using a custom component, the tag prop accepts it as a forwarRef component only.
So you need to update in that way.
Sample Snippet
// This is just like a normal component, but now has a ref.
const CustomComponent = forwardRef<HTMLDivElement, any>((props, ref) => {
return <div ref={ref}>{props.children}</div>;
});
export const BasicFunction: FC = props => {
const [state, setState] = useState([
{ id: 1, name: "shrek" },
{ id: 2, name: "fiona" }
]);
return (
<ReactSortable tag={CustomComponent} list={state} setList={setState}>
{state.map(item => (
<div key={item.id}>{item.name}</div>
))}
</ReactSortable>
);
};

Material-UI custom JSS Styles removed on Component Re-render

I have a temporary Drawer with Tabs inside. When the Drawer is open and the color theme is toggled, the custom styles for the component in the tab are removed, breaking the styling. If I close the Drawer or toggle between tabs, it fixes the issue. This is only happening in two of the tabs, not all of them. See the pictures below.
For simplicity, I'll show the code for the Check Results tab. The Cog icon is being stripped of its styling and defaulting to the left.
...
const styles: StyleRulesCallback = () => ({
buttonDiv: {
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'flex-end',
},
});
...
type Props = StateProps & DispatchProps & WithStyles<typeof styles>;
class CheckResultsPanel extends React.Component<Props> {
...
render() {
this.selectedKeys = [];
const tree = this.renderTree();
const { classes } = this.props;
return (
<div id="checkResultsPanel">
<div className={classes.buttonDiv}>
<IconButton onClick={this.designSettingsClick} aria-label="Design Settings">
<SettingsIcon />
</IconButton>
</div>
<Divider />
{tree}
</div>
);
}
The components in question are being imported to the Drawer like so:
<div className="right-sidebar-container">
<div id="loadCaseObjectTabs" className={classes.root}>
<AppBar position="static">
<Tabs value={this.state.topValue} onChange={this.topHandleChange} fullWidth>
<Tab label="Objects" />
<Tab label="Load Cases" />
</Tabs>
</AppBar>
{topValue === 0 ? <div className={classes.tabs}><NavigationPanel /></div> : undefined}
{topValue === 1 ? <div className={classes.tabs}><LoadCasesPanel /></div> : undefined}
</div>
<div id="checkResultsPropertiesTabs" className={classes.root}>
<AppBar position="static">
<Tabs value={bottomTabIndices[this.props.bottom]} onChange={this.bottomHandleChange} fullWidth>
<Tab label="Check Results" />
<Tab label="Properties" />
</Tabs>
</AppBar>
{this.props.bottom === 'checkResults' ? <div className={classes.tabs}><CheckResultsPanel /></div> : undefined}
{this.props.bottom === 'properties' ? <div className={classes.tabs}><PropertiesPanel /></div> : undefined}
</div>
</div>
We discovered the issue. Apologies for the vague example. The issue was caused by optimization we implemented. The component was only set to update if certain props were changed and the classes were not being passed through this logic. When the themes were toggled, it changed all the styling for the application but since the component did not update the styles were not being applied.
shouldComponentUpdate(nextProps: Props) {
...
if (this.props.classes !== nextProps.classes) return true;
...
}

Resources