I'm using Materal-UI's Tabs and I'm facing a problem where every time I click on a tab, it doesn't start executing the functions from the start. Instead all the functions are done in all the tabs at the same time which is not the required thing I need. How can I change it so that when I click on that tab it starts executing the functions inside from the start?
I have made an example of what I mean in the codesandbox below showing 3 tabs with a timer of 5 secs, where all the functions in the tab are executed at the same time which is not what is desired and clicking on the tab doesn't restart it
CodeSandBox Code: https://codesandbox.io/s/material-demo-forked-18xwt
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 p={3}>
<Typography>{children}</Typography>
</Box>
)}
</div>
);
}
TabPanel.propTypes = {
children: PropTypes.node,
index: PropTypes.any.isRequired,
value: PropTypes.any.isRequired
};
function a11yProps(index) {
return {
id: `simple-tab-${index}`,
"aria-controls": `simple-tabpanel-${index}`
};
}
const useStyles = makeStyles((theme) => ({
root: {
flexGrow: 1,
backgroundColor: theme.palette.background.paper
}
}));
export default function SimpleTabs() {
const classes = useStyles();
const [value, setValue] = React.useState(0);
const [text1, setText1] = React.useState("Wait 1");
const [text2, setText2] = React.useState("Wait 2");
const [text3, setText3] = React.useState("Wait 3");
const handleChange = (event, newValue) => {
setValue(newValue);
};
//
function tab1() {
setTimeout(function () {
setText1("Done 1");
}, 5000);
}
//
function tab2() {
setTimeout(function () {
setText2("Done 2");
}, 5000);
}
//
function tab3() {
setTimeout(function () {
setText3("Done 3");
}, 5000);
}
return (
<div className={classes.root}>
<AppBar position="static">
<Tabs
value={value}
onChange={handleChange}
aria-label="simple tabs example"
>
<Tab label="Item One" {...a11yProps(0)} />
<Tab label="Item Two" {...a11yProps(1)} />
<Tab label="Item Three" {...a11yProps(2)} />
</Tabs>
</AppBar>
<TabPanel value={value} index={0}>
{text1}
{tab1()}
</TabPanel>
<TabPanel value={value} index={1}>
{text2}
{tab2()}
</TabPanel>
<TabPanel value={value} index={2}>
{text3}
{tab3()}
</TabPanel>
</div>
);
}
Issues
You are doing state updates as side-effect in the "render" function return, the return should be pure.
All the tab panels mount at the same time, and remain mounted.
You don't react to the tab index changing, so no functions are re-ran.
Solution
Use an useEffect hook to "restart" callbacks.
Each callback should "reset" state.
Save the timeout reference so any running timers can be cleared when resetting.
Updated Component
export default function SimpleTabs() {
const classes = useStyles();
const [value, setValue] = useState(0);
const [text1, setText1] = useState("Wait 1");
const [text2, setText2] = useState("Wait 2");
const [text3, setText3] = useState("Wait 3");
const timer1Ref = useRef();
const timer2Ref = useRef();
const timer3Ref = useRef();
const handleChange = (event, newValue) => {
setValue(newValue);
};
useEffect(() => {
switch (value) { // <-- invoke specific callback associated with tab
case 0:
tab1();
break;
case 1:
tab2();
break;
case 2:
tab3();
break;
default:
// ignore
}
}, [value]); // <-- trigger when tab index value updates
function tab1() {
clearTimeout(timer1Ref.current);
setText1("Wait 1"); // <-- reset state
timer1Ref.current = setTimeout(function () {
setText1("Done 1");
}, 5000);
}
function tab2() {
clearTimeout(timer2Ref.current);
setText2("Wait 2");
timer2Ref.current = setTimeout(function () {
setText2("Done 2");
}, 5000);
}
function tab3() {
clearTimeout(timer3Ref.current);
setText3("Wait 3");
timer3Ref.current = setTimeout(function () {
setText3("Done 3");
}, 5000);
}
return (
<div className={classes.root}>
<AppBar position="static">
<Tabs
value={value}
onChange={handleChange}
aria-label="simple tabs example"
>
<Tab label="Item One" {...a11yProps(0)} />
<Tab label="Item Two" {...a11yProps(1)} />
<Tab label="Item Three" {...a11yProps(2)} />
</Tabs>
</AppBar>
<TabPanel value={value} index={0}>
{text1}
{/* {tab1()} */} // <-- Don't update state from return!!
</TabPanel>
<TabPanel value={value} index={1}>
{text2}
{/* {tab2()} */}
</TabPanel>
<TabPanel value={value} index={2}>
{text3}
{/* {tab3()} */}
</TabPanel>
</div>
);
}
Note: This is a lot of really repetitive code, i.e. not very DRY. Try to abstract the common behavior into a single function.
Related
I'm working on react library to create to display list of events dynamically with react material ui tab. Im' getting following error. How I can check following props is not undefined before render Tab component.
Eg:
{Object.keys(props.schedule).map((name: string, index: number) => {
return <Tab label={name} {...a11yProps(index)} />;
})}
Error:
Uncaught TypeError: Cannot convert undefined or null to object
The above error occurred in the <BasicTabs> component:
channelDetail.tsx
function ChannelDetail() {
const { channelId } = useParams();
const dispatch = useAppDispatch();
const { schedule }: Channels = useAppSelector(selectChannelById); // please check sample response
useEffect(() => {
dispatch(getChannelById(channelId));
}, [dispatch, channelId]);
return(
<ChannelTab schedule={schedule} />
)
}
channelTab.tsx
export default function BasicTabs(props: any) {
const [value, setValue] = React.useState(0);
const handleChange = (event: React.SyntheticEvent, newValue: number) => {
setValue(newValue);
};
return (
<Box sx={{ width: "100%" }}>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<Tabs
value={value}
onChange={handleChange}>
{Object?.keys(props?.schedule).map((name: string, index: number) => {
return <Tab label={name} {...a11yProps(index)} />;
})}
</Tabs>
</Box>
<TabPanel value={value} index={0}>
Item One
</TabPanel>
<TabPanel value={value} index={1}>
Item Two
</TabPanel>
<TabPanel value={value} index={2}>
Item Three
</TabPanel>
</Box>
);
}
Sample response for schedule
const schedule = {
'today': [
{ title: "Harry Potter", time: "2pm" },
{ title: "Rampage", time: "4pm" }
],
'tommorow': [
{ title: "Die Hard", time: "3pm" },
{ title: "Rambo", time: "6pm" }
]
};
This is either very simple or I am doing it completely wrong. I am a novice so please advise.
I am trying to show different components inside different tabs using Material UI using array map. The tabs are showing fine but the components do not render. Basically if the array label is 'Welcome', the tab name should be 'Welcome' and the Welcome component should show up and so on. Please help!
return (
<Box sx={{ width: '100%' }}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={value} onChange={handleChange} aria-label="basic tabs example">
{fetchedCategories.map((category) => (
<Tab key={category.label} label={category.label} />
))}
</Tabs>
</Box>
{fetchedCategories.map((category, index) => {
const Component=myComponents[category.label];
})}
{fetchedCategories.map((category, index) => (
<TabPanel key={category.label} value={value} index={index}>
<Component label={category.label} />
</TabPanel>
))}
</Box>
);
Here is my props & Component function:
interface ComponentProps {
label: string;
value?: number;
}
function Component (props: ComponentProps)
{
const {label, value} = props;
return myComponents[label];
}
const myComponents = {
'Welcome': Welcome,
'Salad/Soup': Welcome
}
Try something like:
function Component({ label, value }: ComponentProps) {
const [Comp, setComponent] = useState(<div />);
React.useEffect(() => {
const LabelComp = myComponents[label];
if (label && LabelComp) {
setComponent(<LabelComp value={value} />); // <-- if you want to pass value to you component
}
}, [value, label]);
return Comp;
}
And you will use it like:
const App = () => {
return <Component label={"myComponentLabel"} value={"some value"} />;
};
I am building a an app that has a map and side-tabs.
Every time a user clicks on the map a marker appears and the coordinates are stored in a used state array.
I want every time a new marker appears to show it as a list or an accordion item in my side-tabs.
My side-tabs component and my addmarker component have the App as a parent.
How can I pass the usestate array from my addmarker component to my sidebar component every time I click on the map ?
ADD MARKER COMPONENT
function AddMarker(callbackFunction){
const [coord, setPosition] = useState([]);
const map = useMapEvents({
click: (e) => {
setPosition([...coord,e.latlng])
const mark = e
//console.log(mark)
//setInfo(`${e.latlng}`)
},
SIDE-BAR COMPONENT
export default function VerticalTabs() {
const [value, setValue] = React.useState(0);
const handleChange = (event, newValue) => {
setValue(newValue);
};
return (
<Box
sx={{ flexGrow: 1, bgcolor: 'background.paper', display: 'flex', height: 224 }}
>
<Tabs
orientation="vertical"
value={value}
onChange={handleChange}
aria-label="Vertical tabs"
sx={{ borderRight: 1, borderColor: 'divider' }}
>
<Tab label="Waypoints" {...a11yProps(0)} />
<Tab label="Sorting" {...a11yProps(1)} />
</Tabs>
<TabPanel value={value} index={0}>
</TabPanel>
<TabPanel value={value} index={1}>
Sorting
</TabPanel>
</Box>
);
}
APP.JS
function App() {
return (
<div className="App" >
<Sidetabs/>
<MapContainer center={[40.44695, -345.23437]} zoom={3}>
..............
<AddMarker />
</MapContainer>
</div>
)
}
Here, always lifting one step up, helps everytime, i have lifted your state up, and now both children have access to it, https://reactjs.org/docs/lifting-state-up.html
the similar implementation could be look like this,
as both siblings are nothing but children to parent App
function App() {
const [coord, setPosition] = useState([]);
return (
<div className="App" >
<Sidetabs coord={coord} setPosition={setPosition}/>
<MapContainer center={[40.44695, -345.23437]} zoom={3}>
<AddMarker coord={coord} setPosition={setPosition}/>
</MapContainer>
</div>
)
}
then extract out using props,
function AddMarker(props){
const {coord, setPosition} = props;
}
I need to create bunch of Tab nodes in a Tabs. I thought that map a array would be easier to manage it. But I was kind of don't know how to make it works with MATERIAL UI Taps components.
My target is when I click the tab, the TabPanel supposed to show the correct components pending on the index.
The Tabs part works just fine, and it will be siwtch components properly if I keep the TabPanel one by one. But it won't be work if I map the array to create the TabPanel.
Please advise how to fix it.
//TODO set the router for each tab, wondering if it could be done in an array and map it
const tab_item = [
{
index: 1,
label: 'Purchase',
path: '/Linx_Homeline/Purchase',
tabPanel_comp:<LawyerPurchase />
},
{
index: 2,
label: 'Refinance',
path: '/Linx_Homeline/Refinance',
tabPanel_comp:<Refinance />
},
// {},
]
function TabPanel(props) {
const { children, value, index } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`wrapped-tabpanel-${index}`}
>
{value === index && (
<Box p={3}>
<Typography component={'div'}>{children}</Typography>
</Box>
)}
</div>
);
}
// TabPanel.propTypes = {
// // children: PropTypes.node,
// index: PropTypes.any,
// value: PropTypes.any,
// };
const useStyles = makeStyles((theme) => ({
root: {
backgroundColor: theme.palette.background.paper,
},
item: {
minWidth: '0px'
}
}));
export default function TabsWrappedLabel() {
const classes = useStyles();
const [value, setValue] = React.useState(false);
const updateNotes = useContext(NotesUpdate);
const history = useHistory();
const handleChange = (event, newValue) => {
setValue(newValue);
};
const clean_notes_push = (item) => {
//comments
updateNotes.setCondition('');
updateNotes.setFunNotes('');
updateNotes.setBusNotes('');
history.push(item.path);
}
return (
<div className={classes.root}>
<AppBar position="static">
<Tabs
value={value}
onChange={handleChange}
variant='fullWidth'
TabIndicatorProps={{ style: { background: '#00ff33' } }}
>
{tab_item.map((item) => ( // The Tab works fine here.
<Tab
wrapped
key={item.index}
index={item.index}
label={item.label}
onClick={() => (clean_notes_push(item))}
/>
))}
</Tabs>
</AppBar>
{/* <TabPanel value={value} index={0}> // It works if I put the TabPanel one by one, but I'm trying to map the tab_item array to generate them, problem is I don't know how to make it works.
<LawyerPurchase/>
</TabPanel>
<TabPanel value={value} index={1}>
Item Two
</TabPanel>
<TabPanel value={value} index={2}>
Item Two
</TabPanel>
*/}
{tab_item.map((item) => ( // Not working here, not even generate a TabPanel
<TabPanel
key={item.index}
value={value}
index={1}
>
{item.tabPanel_comp}
</TabPanel>
))}
</div>
);
}
You may need to update the tab_item object by:
//TODO Declare the function to render the component in a tab pane
const tab_item = [
{
index: 1,
label: 'Purchase',
path: '/Linx_Homeline/Purchase',
tabPanel_comp: () => <LawyerPurchase /> // function returns the component
},
{
index: 2,
label: 'Refinance',
path: '/Linx_Homeline/Refinance',
tabPanel_comp: () => <Refinance />
},
]
And replace the TabPanel render map function by:
{tab_item.map((item) => ( // Not working here, not even generate a TabPanel
<TabPanel
key={item.index}
value={value}
index={1}
>
{item.tabPanel_comp()} //calls the function to render the component
</TabPanel>
))}
I'm using Material UI's nested/select ItemList component to dynamically generates any number of dropdown menu items based on how many items belong in this header as you can maybe tell from the mapping function. On another file 1 layer above this one, I am again mapping and generating multiple of these DropDownMenus, is it possible for these components to communicate to each other?
This is the file in question
const useStyles = makeStyles((theme) => ({
root: {
width: '100%',
maxWidth: 330,
backgroundColor: theme.palette.background.paper,
},
nested: {
paddingLeft: theme.spacing(4),
}
}));
export default function DropDownMenu(props) {
const classes = useStyles();
const [open, setOpen] = React.useState(true);
let unitName = props.unit[0];
let chapterList = props.unit.slice(1);
const [selectedIndex, setSelectedIndex] = React.useState(1);
const handleListItemClick = (index) => {
console.log("ItemClicked");
console.log(index);
setSelectedIndex(index);
};
const handleClick = () => {
setOpen(!open);
};
const selectMenuItem = (chapter, index) => {
props.chooseChapter(chapter)
handleListItemClick(index)
}
let dropDownUnit = chapterList.map((chapter, index) => {
return (
<ListItem button
className={classes.selected}
selected={selectedIndex === index}
onClick={() => selectMenuItem(chapter, index)}
key={index}>
<ListItemText primary={chapter} />
</ListItem>
)
})
return (
<List
component="nav"
aria-labelledby="nested-list-subheader"
subheader={
<ListSubheader component="div" id="nested-list-subheader">
</ListSubheader>
}
className={classes.root}
>
<ListItem button onClick={handleClick}>
<ListItemText primary={unitName} />
{!open ? <ExpandLess /> : <ExpandMore />}
</ListItem>
<Collapse in={!open} timeout="auto" unmountOnExit>
<List component="div" disablePadding className={classes.selected}>
{dropDownUnit}
</List>
</Collapse>
</List>
);
}
Psudo Style - What I'm trying to accomplish
<DropDownMenu>
<MenuItem> // Suppose this is selected
<MenuItem>
<DropDownMenu>
<MenuItem> // onClick --> Select this and deselect all other selected buttons
You can have a parent of these components such that the parent will keep the state of who is active. That way you can pass that state & the state setter as props so that everyone will know who is active
export default function App() {
const [selectedItem, setSelectedItem] = React.useState();
return (
<>
<DropDownMenu
selectedItem={selectedItem} // pass down as props
setSelectedItem={setSelectedItem} // pass down as props
unit={...}
chooseChapter={function () {}}
/>
...
On the children, simply refactor the Call To Action (in this case onClick) to set the state using the passed down props. Pay attention to the selected prop of ListItem, we now use the state we have passed down from the parent
let dropDownUnit = chapterList.map((chapter, index) => {
return (
<ListItem
button
className={classes.selected}
selected={props.selectedItem === chapter}
onClick={() => props.setSelectedItem(chapter)}
key={index}
>
<ListItemText primary={chapter} />
</ListItem>
);
});