Conditionally Rendering Components From An Array of Components - reactjs

I have 3 cards I want to render on the screen all with a similar layout. What is this pattern called when we have a component as a value?
const steps = [
{
label: "Order details",
component: OrderDateStep,
},
{
label: "Driver details",
component: OrderDriverStep,
},
{
label: "Acknowledgements",
component: OrderAcknowledgementStep,
},
];
Additionally I keep running into an issue when these are conditionally rendered. I want to wait until stripe has initialised before displaying the form. However, I get an error Error: Rendered more hooks than during the previous render.. I know I can just add the different components but that isn't very scalable. Is there another way I can achieve this re-usable pattern without running into this issue with the number of hooks changing? Why does using step[x].component() change the number of hooks where just using the component does not?
{stripe && (
<Elements
stripe={stripe}
options={{
clientSecret: paymentIntent?.client_secret,
}}
>
{steps.map((step, index) => {
return (
<Box
key={step.label}
sx={{
mt: 3,
}}
>
<Box sx={{ my: 2 }}>
<Typography variant="h5">{step.label}</Typography>
</Box>
{step.component()}
</Box>
);
})}
<Box sx={{ display: "flex", justifyContent: "end" }}>
<Button variant="contained" onClick={submitForm}>
Submit
</Button>
</Box>
</Elements>
)}

If you want to make sure something is filled, or rendered, before displaying other data in react, you can just do
{
loadedVariable ?
<div>
......
</div>
:null
}
If your question is not fully answered by the point i get home i'll be happy to help you further.

Add one more conditionally into your render component to make sure that steps had filled:
{stripe && steps.length && (
<Elements
stripe={stripe}
options={{
clientSecret: paymentIntent?.client_secret,
}}
>
{steps.map((step, index) => {
return (
<Box
key={step.label}
sx={{
mt: 3,
}}
>
<Box sx={{ my: 2 }}>
<Typography variant="h5">{step.label}</Typography>
</Box>
{step.component()}
</Box>
);
})}
<Box sx={{ display: "flex", justifyContent: "end" }}>
<Button variant="contained" onClick={submitForm}>
Submit
</Button>
</Box>
</Elements>
)}

Related

mui-Autocomplete dropdown goes up when the state changed

I have a component which written with mui Autocomplete component. I have a expand-collapse ability in the component and ı keep the expanded collapsed values in one state.
When ı clicked to something that will change this state, my autocomplete re-renders as ı expect but dropdown list goes up. This is my main problem.
How can ı avoid this? Is there any prop or something to solve that ? I tried to manage with sholudComponent update but in that case, state changes but autocomplete not re-renders so it calses another problem.
Example gif belong here
I hold the expanded portfolios in expandedPortfolios state and when ı clicked arrow ıcon handle with handleExpand function and change the state so ı can manage which group will expanded or collapsed.
handleExpand = (portfolioName) => {
var portfolio = (this.state.portfoliosAsGroup || []).find(p => p.name == portfolioName);
if (portfolio) {
let portfolioId = portfolio.id;
let current = this.state.expandedPortfolios;
console.log(current);
let Ids = [];
if (current.includes(portfolioId)) {
console.log("filter");
Ids = current.filter((i) => i !== portfolioId);
} else {
console.log("push");
Ids.push(...current, portfolioId);
}
console.log(Ids);
this.setState({ expandedPortfolios: Ids, expand: !this.state.expand });
}
}
renderGroup={(params) => (
<>
{console.log(params)}
<div style={{ display: "flex", alignItems: "center", height: "20px", marginTop: '15px' }}>
{params.group != dropdownGroupTags.all && <IconButton size="small" onClick={() => this.handleExpand(params.group)}>
{(!(this.state.expandedPortfolios || []).includes(this.getPortfolioIdByName(params.group)) ?
<ExpandLessIcon /> : <ExpandMoreIcon />)}
</IconButton>}
{params.group != dropdownGroupTags.all ? <WarehouseIcon /> : <AccountBalanceIcon />}
<Button onClick={() => params.group != "Unportfolio" && this.handleCompanyChange(this.createCompanyGroupChangeText(params.group), params.group)}>
<Typography variant="subtitle1" sx={{ fontWeight: 'bold' }}>
{params.group}
</Typography>
</Button>
<div style={{
marginLeft: 'auto',
marginRight: '30px'
}}>
<Typography align="right" variant="subtitle1" sx={{ fontWeight: 'bold' }}>
{params.group != dropdownGroupTags.all && `${(params.children || []).length} Unit`}
</Typography>
</div>
</div>
{/* <Typography variant="subtitle1" sx={{fontWeight: 'bold'}}>{(params.children || []).length} Unit</Typography> */}
<br />
{(this.state.expandedPortfolios || []).includes(this.getPortfolioIdByName(params.group)) ? params.children : []}
</>
)}

Best way to create as many unique useState as cleanly as possible?

I am trying to figure out how to create as many useState as cleanly (keeping DRY principle in mind) as possible.
The problem I am trying to solve is that currently I have a menu list of items and its relative prices. The website lets the customer use a drop down menu to choose how many orders of that item he/she wants.
My question is that I know for one menu item, I can use useState to make something like
const [order, setOrder] = useState({});
where the key-value pair would be name and number of orders
Now my question is: suppose I have N many menu items, how can I create as many const [order, setOrder] = useState({}) as I need? I know I can't put useState in a for-loop so that's out of the question (?)
The below is my code I'm trying to work out:
export default function MenuPage() {
// getting the menu items and prices from firebase
const [menuItems, setMenuItems] = useState({});
React.useEffect(async () => {
const result = await querySnapShot();
result.forEach((doc) => {
setMenuItems(doc.data());
})
}, []);
return(
{
Object.keys(menuItems).map((key) => {
return (
<Box
key={key}
display='flex'
flexDirection='row'
alignItems='center'
justifyContent='center'
textAlign='center'
borderBottom='0.5px solid black'
sx={{ my: 10, mx: 10 }}>
<Box flex={1} textAlign='center' sx={{ fontSize: '35px', }}>
{key}:
</Box>
<br />
<Box flex={1} textAlign='center' sx={{ fontSize: '25px' }}>
${menuItems[key]}0
</Box>
<Box flex={1} display='flex' flexDirection='row' alignItems='center' justifyContent='end' sx={{ width: '50px' }}>
{/* <Typography flex={0} sx={{ mr: 5, fontFamily: 'Poppins', fontSize: '25px' }}>Order: </Typography> */}
<Box flex={0.5} textAlign='center'>
<FormControl fullWidth>
<InputLabel id="demo-simple-select-label">Order</InputLabel>
<Select
value={order}
name={key}
label="Order"
onChange={handleChange}>
<MenuItem value={1}>1</MenuItem>
<MenuItem value={2}>2</MenuItem>
<MenuItem value={3}>3</MenuItem>
</Select>
</FormControl>
</Box>
</Box>
</Box>
);
})
}
);
}
Supposing that you have an array as response you can:
const data = [
{ name: 'Foo', value: 'bar' },
{ name: 'Foo2', value: 'bar2' },
];
setState(data.map({ name: 'Foo', value: 'bar' }));
// or if no change has to be done you can set it directly into the state
setState(data);

MaterialUI TypeScript: Is it possible to add multi-level navigation menu to "NavItem"?

How to create multi-level navigation menu with MaterialUI and TypeScript?
I would like to add to the '/questions' the follwing:
2 navigation menu:
/questions/Tags
/questions/Users
like as in the Screenshot
export function NavBar(...) {
return (
<>
<Drawer>
<List>
<NavItem
to="/questions"
primary="questions"
icon={CloudOff}
/>
)}
<NavItem to="/questions" primary="questions" icon={Window} />
</List>
</Drawer>
</>
);
}
Lot of posibility...
The best way it's to prepare an array with the menu information and map on, something like this :
const TestArea = ({ params }) => {
const menu = [{ mainMenu: "tata", subMenu: ["toto", "titi"] }];
return (
<>
<Toolbar
style={{
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
}}
>
{menu.map((item) => {
return (
<>
<Typography variant="overline">{item.mainMenu}</Typography>
{item.subMenu.map((subItem) => {
return (
<Typography variant="caption" style={{ marginLeft: "40%" }}>
{subItem}
</Typography>
);
})}
</>
);
})}
</Toolbar>
</>
);
};
With this base, you can customize with the component of your choose, render element like link with a path to...
Button color="inherit" component={Link} to="/classic">
Yes, I know, it's plain JSX not TSX, it's just for example.
If you need more information, say me !

Add dynamic value beside label in Material UI React Tabs Component

I am creating tabs using material ui and i want to add value beside label (ex: 05 written beside Recommendation label) which will be dynamic in the tabs component just like shown in below image:
Here is my code snippet of Tabs component:
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={value} onChange={handleChange} aria-label="basic tabs example">
<Tab label="Recommendation" />
<Tab label="Ongoing" />
<Tab label="Completed" />
</Tabs>
</Box>
One option is to create your own component, pass that in as a the Tab label, and then style it as needed. For example:
const TabWithCount = ({ children, count }) => {
return (
<Box sx={{ display: "inline-flex", alignItems: "center" }}>
<Typography component="div">{children}</Typography>
{count ? (
<Typography
component="div"
variant="body2"
sx={{ marginLeft: "0.5rem" }}
>
{count}
</Typography>
) : null}
</Box>
);
};
...
<Tab
label={<TabWithCount count="05">Recommendation</TabWithCount>}
/>
I've created a quick (unstyled) example to illustrate the solution: https://codesandbox.io/s/basictabs-material-demo-forked-i9543?file=/demo.js

Material-UI useAutocomplete cannot access option in onChange

I don't know what I am doing wrong but I cannot access the option.slug from the object I receive from the API. I am trying to change pass a URL slug and change the route of my app. I am trying to do this using the onChange prop inside my useAutocomplete() definition.
Accessing the option.slug or any other property works well inside of my JSX structure. As you can see I am grabbing option.title and so on to render my list items which works well...
Instead of grabbing the actual slug I keep getting the route of "/components/undefined"
But when I try accessing the option.slug on the top level of my component in the useAutocomplete() definition it doesn't seem to work. Based on the original documentation though, that's the way to do it.
https://material-ui.com/components/autocomplete/#useautocomplete
export default function NavigationSearch({ onClose, results }) {
const router = useRouter();
const {
getRootProps,
getInputLabelProps,
getInputProps,
getListboxProps,
getOptionProps,
groupedOptions
} = useAutocomplete({
id: 'use-autocomplete-demo',
options: results,
getOptionLabel: (option) => option.title,
onChange: (option) => router.push(`/components/${option.slug}`)
});
return (
<React.Fragment>
{/* 1. SEARCH INPUT */}
<Container maxWidth="md" disableGutters>
<Box display="flex" width="100%">
<motion.div
style={{ width: 'inherit' }}
initial="hidden"
animate="visible"
variants={animation}>
<Box display="flex" width="inherit">
<Search width="inherit" {...getRootProps()}>
<SearchIcon color="primary" />
<InputBase
placeholder="Search for components, patterns..."
style={{ color: 'inherit', width: 'inherit' }}
autoFocus
{...getInputProps()}
/>
</Search>
</Box>
</motion.div>
<IconButton color="primary" onClick={onClose}>
<CloseIcon />
</IconButton>
</Box>
</Container>
{/* SEARCH RESULTS */}
<BackdropOverlay open={true} onClick={onClose}>
<Container maxWidth="md" disableGutters>
{groupedOptions.length > 0 ? (
<Results>
<List
{...getListboxProps()}
subheader={
<Box paddingX={2}>
<Typography
variant="overline"
color="textSecondary"
gutterBottom>
Popular search results
</Typography>
</Box>
}>
{groupedOptions.map((option, index) => (
<Item
button
key={option.title}
{...getOptionProps({
option,
index
})}>
<Box
display="flex"
justifyContent="space-between"
width="100%">
<Box>
<Typography color="textPrimary">
{option.title}
</Typography>
<Typography variant="caption" color="textSecondary">
{option.type}
</Typography>
</Box>
<Box alignSelf="flex-end">
<Typography variant="caption" color="textSecondary">
/components/button
</Typography>
</Box>
</Box>
</Item>
))}
</List>
</Results>
) : null}
</Container>
</BackdropOverlay>
</React.Fragment>
);
}
This is the API response that I store in the results prop:
0: {id: 1, title: "Button", slug: "button"}
1: {id: 2, title: "Switch", slug: "switch"}
2: {id: 3, title: "Tags", slug: "tags"}
3: {id: 4, title: "Checkbox", slug: "checkbox"}
4: {id: 5, title: "Toast", slug: "toast"}
Autocomplete component uses useAutocomplete hook under the hood so they share the same API. In Autocomplete API. This is the signature of onChange:
function(event: object, value: T | T[], reason: string) => void
The second argument is the option value which is what you need here. So change your code to:
onChange: (_, option) => router.push(`/components/${option.slug}`)
To fix the undefined value issue.

Resources