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

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);

Related

Select Menu isn't getting value from it's sub Menu Item

What I'm trying to do
I'm trying to create a category list with Material UI
Parent Menu Item
Child Menu Item by Parent Menu Category
What I'm getting
I'm getting only parent menu value.
Whenever I click on child item, than it showing it's parent Menu.
Code
Here I've create a codesanbox, Please help to get out from this.
Code
Please check console for see event.
<Select
displayEmpty
defaultValue=""
value={search}
onChange={handleChange}
id="grouped-select"
input={<OutlinedInput />}
label="Category"
size="small"
sx={{
width: "160px",
height: "100%",
borderRadius: 0,
textTransform: "uppercase"
}}
renderValue={(selected) => {
if (selected.length === 0) {
return (
<Typography variant="subtitle2">Select Category</Typography>
);
}
return <Typography variant="subtitle2">{selected}</Typography>;
}}
>
<MenuItem value="select category">
<Typography variant="subtitle2">Select Category</Typography>
</MenuItem>
{categories.map((category) => (
<MenuItem
key={category}
sx={{
lineHeight: "20px",
display: "block",
backgroundColor: "none"
}}
value={category}
>
<Typography variant="subtitle2">{category}</Typography>
{category === "Baby Products" &&
babyProducts.map((item) => (
<List key={item} sx={{ padding: 0 }}>
<MenuItem
sx={{
lineHeight: "12px",
display: "block"
}}
value={item}
onChange={handleChange}
>
<Typography variant="subtitle2">{item}</Typography>
</MenuItem>
</List>
))}
</MenuItem>
))}
</Select>
const babyProducts = [
"Baby",
"Baby Cereal",
"Baby Honey",
"Biscotti",
"Formula",
"Milk",
"Juice",
"Puree",
"Bath & Skincare",
"Diapers & Wipes",
"Huggies",
"Kidz",
"MamyPoko",
"Molfix",
"Pampers & Wipes",
"Oral Care",
];
const chocolates = [
"Assorted",
"Cadbury",
"Candy",
"Chocolate Balls",
"Chocolate Bars",
"Kit-Kat",
"Lolipops",
"Marshmallow",
"Mints&Gums",
"Toblenore",
];
The problem is due to nested MenuItems. You can use nested arrays instead with the marginLeft on a subcategory:
{categories.map((cat) => [
renderCategory(cat),
renderCategoryItems(cat)
])}
const renderCategory = (category) => {
return (
<MenuItem
key={category}
sx={{
lineHeight: "20px",
display: "block",
backgroundColor: "none"
}}
value={category}
>
<Typography variant="subtitle2">{category}</Typography>
</MenuItem>
);
};
const renderCategoryItems = (category) => {
const list = category === "Baby Products" ? babyProducts : [];
return list.map((item) => (
<MenuItem
key={item}
sx={{
lineHeight: "12px",
display: "block"
}}
value={item}
>
<Typography
sx={{
marginLeft: "16px"
}}
variant="subtitle2"
>
{item}
</Typography>
</MenuItem>
));
};
Update
If more subcategories are required, you can describe them as follows:
const subCategories = {
"Baby Products": [
...
],
"Chocolate": [
...
]
};
And update renderCategoryItems function:
const list = subCategories[category] || [];
Working example

Get All TextField values from loop in Next.js when I press Submit button

first of all look at these below screenshots:
There are two tasks which I want to achieve:
There are two questions shown on the page using the array map method, by default I'm showing only one question, and when I press the next part button the second question will appear with the same question and a TextField (multiline). Now I've implemented a word counter in TextField but when I type something in 1st question the counter works properly. But when I go to the next question the here counter shows the previous question's word counter value, I want them to separately work for both questions.
When I click on the next part and again when I click on the previous part then the values from TextField are removed automatically. I want the values there if I navigate to the previous and next part questions. Also, I want to get both TextField values for a form submission when I press the Submit Test button.
Below are my codes for this page. I'm using Next.js and MUI
import { Grid, Typography, Box, NoSsr, TextField } from '#mui/material';
import PersonIcon from '#mui/icons-material/Person';
import Timer from '../../../components/timer';
import Button from '#mui/material/Button';
import { useState } from 'react';
import ArrowForwardIosIcon from '#mui/icons-material/ArrowForwardIos';
import { Scrollbars } from 'react-custom-scrollbars';
import AppBar from '#mui/material/AppBar';
import Toolbar from '#mui/material/Toolbar';
import ArrowBackIosIcon from '#mui/icons-material/ArrowBackIos';
import axios from '../../../lib/axios';
import { decode } from 'html-entities';
import { blueGrey } from '#mui/material/colors';
export default function Writing({ questions }) {
const [show, setShow] = useState(false);
const [show1, setShow1] = useState(true);
const [showQuestionCounter, setShowQuestionCounter] = useState(0);
const [wordsCount, setWordsCount] = useState(0);
return (
<>
<Box sx={{ flexGrow: 1 }}>
<AppBar position="fixed" style={{ background: blueGrey[900] }}>
<Toolbar>
<Grid container spacing={2} alignItems="center">
<Grid item xs={4} display="flex" alignItems="center">
<PersonIcon
sx={{ background: '#f2f2f2', borderRadius: '50px' }}
/>
<Typography variant="h6" color="#f2f2f2" ml={1}>
xxxxx xxxxx-1234
</Typography>
</Grid>
<Grid item xs={4} container justifyContent="center">
<Timer timeValue={2400} />
</Grid>
<Grid item xs={4} container justifyContent={'right'}>
<Button
variant="contained"
style={{ background: 'white', color: 'black' }}
size="small">
Settings
</Button>
<Button
variant="contained"
style={{
background: 'white',
color: 'black',
margin: '0px 10px',
}}
size="small">
Hide
</Button>
<Button
variant="contained"
style={{ background: 'white', color: 'black' }}
size="small">
Help
</Button>
</Grid>
</Grid>
</Toolbar>
</AppBar>
</Box>
<Box
sx={{
background: blueGrey[50],
height: '100%',
width: '100%',
position: 'absolute',
}}
pt={{ xs: 13, sm: 11, md: 10, lg: 11, xl: 11 }}>
{questions.map((question, index) =>
index === showQuestionCounter ? (
<Box
key={question.id}
px={3}
sx={{ background: '#f2f2f2', pb: 4 }}
position={{
xs: 'sticky',
sm: 'sticky',
lg: 'initial',
md: 'initial',
xl: 'initial',
}}>
<Box
style={{ background: '#f7fcff', borderRadius: '4px' }}
py={1}
px={2}>
<Box>
<Typography variant="h6" component="h6" ml={1}>
Part {question.id}
</Typography>
<Typography variant="subtitle2" component="div" ml={1} mt={1}>
<NoSsr>
<div
dangerouslySetInnerHTML={{
__html: decode(question.questions[0].question, {
level: 'html5',
}),
}}></div>
</NoSsr>
</Typography>
</Box>
</Box>
<Box
style={{
background: '#f7fcff',
borderRadius: '4px',
marginBottom: '75px',
}}
pt={1}
px={3}
mt={{ xs: 2, sm: 2, md: 2, lg: 0, xl: 3 }}>
<Grid container spacing={2}>
<Grid item xs={12} sm={12} lg={6} md={6} xl={6}>
<Box
py={{ lg: 1, md: 1, xl: 1 }}
style={{ height: '50vh' }}>
<Scrollbars universal>
<Typography
variant="body1"
component="div"
style={{ textAlign: 'justify' }}
mr={2}>
<NoSsr>
<div
dangerouslySetInnerHTML={{
__html: decode(question.question_text, {
level: 'html5',
}),
}}></div>
</NoSsr>
</Typography>
</Scrollbars>
</Box>
</Grid>
<Grid
item
xs={12}
sm={12}
lg={6}
md={6}
xl={6}
mt={{ md: 4, lg: 4, xl: 4 }}>
<TextField
id={`${question.id}`}
label="Type your answer here"
multiline
name={`answer_${question.id}`}
rows={12}
variant="outlined"
fullWidth
helperText={`Words Count: ${wordsCount}`}
onChange={(e) => {
setWordsCount(
e.target.value.trim().split(/\s+/).length
);
}}
/>
</Grid>
</Grid>
</Box>
</Box>
) : null
)}
<Box sx={{ position: 'fixed', width: '100%', left: 0, bottom: 0 }}>
<Grid
container
style={{ background: blueGrey[300], display: 'flex' }}
py={2}
px={3}>
<Grid
item
xs={3}
sm={3}
lg={6}
md={6}
xl={6}
container
justifyContent={'start'}>
<Button
variant="contained"
style={{ background: 'white', color: 'black' }}
size="small">
Save Draft
</Button>
</Grid>
<Grid
item
xs={9}
sm={9}
lg={6}
md={6}
xl={6}
container
justifyContent={'end'}>
<Button
variant="contained"
size="small"
style={{
background: 'white',
color: 'black',
visibility: show1 ? 'visible' : 'hidden',
}}
endIcon={<ArrowForwardIosIcon />}
onClick={() => {
setShow((prev) => !prev);
setShowQuestionCounter(showQuestionCounter + 1);
setShow1((s) => !s);
}}>
Next Part
</Button>
{show && (
<>
<Box>
<Button
variant="contained"
style={{
background: 'white',
color: 'black',
margin: '0 10px',
visibility: show ? 'visible' : 'hidden',
}}
startIcon={<ArrowBackIosIcon />}
size="small"
onClick={() => {
setShow1((s) => !s);
setShowQuestionCounter(showQuestionCounter - 1);
setShow((prev) => !prev);
}}>
previous Part
</Button>
<Button variant="contained" color="success">
Submit Test
</Button>
</Box>
</>
)}
</Grid>
</Grid>
</Box>
</Box>
</>
);
}
export async function getServerSideProps(context) {
const { id } = context.query;
const token = context.req.cookies.token;
if (!token) {
context.res.writeHead(302, {
Location: '/',
});
context.res.end();
}
const res = await axios.get(`test/${id}/questions`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (res.data.success) {
return {
props: {
questions: res.data.data.questions,
},
};
}
}
The wordsCount state is shared between both questions, which means that when you go to the next question the state remains unchanged and shows the wordsCount from the first question. To solve it, each question needs to have it's own state which you can do by creating a Question component and mapping over it:
export default function Question({ question }) {
const [wordsCount, setWordsCount] = useState(0)
return (
<Box
// ...
>
{/* ... */}
<TextField
// ...
helperText={`${wordsCount} words`}
onChange={(e) => {
setWordsCount(e.target.value.trim().split(/\s+/).length)
}}
/>
{/* ... */}
</Box>
)
}
Then map over it:
{questions.map((question, index) =>
index === showQuestionCounter ? (
<Question key={question.id} question={question} />
) : null
)}
Currently, the value of TextField gets reset you the component is unmounted (i.e. when you go to the next question). You need to make the TextField component a controlled component, meaning that you store the value of the field in useState. And if you need to submit the value of TextField later, then you probably need to store the values in the parent component:
export default function Writing({ questions }) {
// ...
const [answers, setAnswers] = useState([])
function handleChange(id, answer) {
// copy the current answers
let newAnswers = [...answers]
// find the index of the id of the answer in the current answers
const index = newAnswers.findIndex((item) => item.id === id)
// if the answer does exist, replace the previous answer with the new one, else add the new answer
if (index) {
newAnswers[index] = { id, answer }
setAnswers(newAnswers)
} else {
setAnswers([...answers, { id, answer }])
}
}
return (
<>
{/* ... */}
{questions.map((question, index) =>
index === showQuestionCounter ? (
<Question
key={question.id}
question={question}
value={answers.find((item) => item.id === question.id)?.answer || ''}
handleChange={handleChange}
/>
) : null
)}
</>
}
In the Question component, add the handler.
export default function Question({ question, value, handleInputChange }) {
const [wordsCount, setWordsCount] = useState(0)
return (
<Box>
{/* ... */}
<TextField
helperText={`${wordsCount} words`}
value={value}
onChange={(e) => {
handleInputChange(question.id, e.target.value)
setWordsCount(e.target.value.trim().split(/\s+/).length)
}}
/>
{/* ... */}
</Box>
)
}
In the parent component (Writing) you should be able to use the values for form submission.

How to target single item in list with onClick when mapping in ReactJS?

My react component returns data from my Firestore DB and maps the data it on Material-UI cards. However, when I press the ExpandMoreIcon, it opens EVERY card. I just want to open each card individually. I know the solution has to do with useState function for expanded & setExpanded.
I've tried to fix this bug but I cant seem to make it work. Any help would be greatly appreciated.
export const NFTprojects = () => {
const [expanded, setExpanded] = useState(false);
const handleExpandClick = (id) => {
setExpanded(!expanded)
};
const [projects, setProjects] = useState([]);
const ref = firebase.firestore().collection("NFTprojects");
function getProjects() {
ref.onSnapshot((querySnapshot) => {
const items = []; //initiliaze empty array
querySnapshot.forEach((doc) => {
items.push(doc.data());
});
setProjects(items);
});
}
useEffect(() => {
getProjects();
}, []);
return (
<div>
<Grid container spacing={4} direction="row" justifyContent="flex-start" alignItems="flex-start">
{projects.map((project) => (
<Grid item xs={4}>
<Card sx={{ maxWidth: 400, borderRadius: 3, mb: 5 }}>
<CardMedia
component="img"
height="140"
image={project.imageUrl}
alt={project.projectName}
/>
<CardContent>
<Typography gutterBottom variant="h5" sx={{ fontWeight: 'bold' }}>
{project.projectName}
</Typography>
<Typography variant="h6" gutterBottom component="div" fontWeight="bold">
{project.jobPosition}
</Typography>
<Typography variant="body2" color="text.secondary" style={{ fontFamily: 'Merriweather' }}>
{project.projectDesc}
</Typography>
</CardContent>
<CardActions disableSpacing>
<Tooltip title="Website">
<IconButton aria-label="secondary marketplace" href={project.websiteLink} target="_blank">
<WebsiteIcon />
</IconButton>
</Tooltip>
<Tooltip title="Twitter">
<IconButton aria-label="twitter" href={project.twitterLink} target="_blank">
<TwitterIcon />
</IconButton>
</Tooltip>
<Tooltip title="Secondary">
<IconButton aria-label="Secondary market link" href={project.secondaryMarket} target="_blank">
<ShoppingCartIcon />
</IconButton>
</Tooltip>
<Tooltip title="Discord">
<IconButton aria-label="discord" href={project.discordLink} target="_blank">
<SvgIcon component={DiscordIcon} viewBox="0 0 600 476.6" />
</IconButton>
</Tooltip>
<Button size="small" variant="contained" sx={{ ml: 15, backgroundColor: 'black' }}>Apply</Button>
<ExpandMore
expand={expanded}
onClick={handleExpandClick}
aria-expanded={expanded}
aria-label="show more"
>
<ExpandMoreIcon />
</ExpandMore>
</CardActions>
<Collapse in={expanded} timeout="auto" unmountOnExit>
<CardContent>
<Typography variant="h6" sx={{ fontWeight: 'bold' }} style={{ fontFamily: 'Merriweather' }}>Job Description:</Typography>
<Typography paragraph>
{project.jobDesc}
</Typography>
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>Prerequisites</Typography>
<Typography paragraph>
{project.jobPrereq}
</Typography>
</CardContent>
</Collapse>
</Card>
</Grid>
))}
</Grid>
</div >
);
}
One approach is to create a separate component for the card. This will enable you to add states to the component and control them. Here is a minimal example demonstrating how you can approach it.
import React, { useState } from "react";
// this is just sample data to work with - equivalent to the data you get from Firebase
const sampleCardsArray = [
{
id: 0,
name: "Card 1",
color: "red",
description: "This is card 1",
},
{
id: 1,
name: "Card 2",
color: "blue",
description: "This is card 2",
},
{
id: 2,
name: "Card 3",
color: "green",
description: "This is card 3",
},
];
// component for all cards
export const AllCards = () => {
// this state is used to store the INDEX of the card that is currently expanded
const [expandedCard, setExpandedCard] = useState(null);
return (
<div>
{sampleCardsArray.map((card, index) => (
<OneCard
card={card}
key={card.id}
// this prop passes the boolean value of whether the card is expanded or not
isExpanded={expandedCard === index}
// this prop receives the index of the card that is expanded and sets the state
expandCard={() => setExpandedCard(index)}
/>
))}
</div>
);
};
// component for one card
// We only show the fields: name and color. We show the description when the card is clicked
export const OneCard = ({ card, isExpanded, expandCard }) => {
return (
<div>
<h1>{card.name}</h1>
<h2>{card.color}</h2>
{
// showing expand button only when card is not expanded
}
{isExpanded ? (
<p>{card.description}</p>
) : (
<button onClick={() => expandCard()}>Expand card</button>
)}
</div>
);
};

Conditionally Rendering Components From An Array of Components

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>
)}

Can I Change Box background color on click in material ui with typescript?

I want to change the background color of my Box whenever it is clicked.
OOTB i couldn't find something which could help my use case.
SO, I tried using events onClick but couldn't find the right event which could bring info on selected event and allow me to change the styling value.
Use Case -
i am creating multiple box dynamically and at once only one Box could be highlighted
{allSports !== null &&
allSports?.map((sports) => (
<Grid
item
xs={4}
sx={{ mx: "auto", my: 1, minWidth: "80%" }}
onClick={backgroundChange}
>
<Item
// onClick={() => sportChoose(sports)}
>
<Box sx={{ display: "flex", justifyContent: "space-evenly" }}>
<Box>
<img
src={
require(`../../../../../resources/images/sportsIcons/${sports.icon}`)
.default
}
/>
</Box>
<Box sx={{ m: "auto" }}>
<Typography variant="h6">{sports.name}</Typography>
</Box>
</Box>
</Item>
</Grid>
))}
import { FC, ReactElement, useState } from 'react'
import { Box } from '#mui/material'
export const MuiCard: FC = (): ReactElement => {
const [clicked, setClicked] = useState(false)
const toggleClicked = () => setClicked((prev) => !prev)
return (
<Box
component="div"
onClick={toggleClicked}
sx={{ height: 20, backgroundColor: clicked ? 'red' : 'white' }}
/>
)
}

Resources