I want to add the macchines in machine array so I defined a specific component with add function in it. So when I add the "process" in "processes" array then it is reflecting on the console using useEffect. But when I add a machine it is reflected in MachineGround Component But not in App component. Overall I am planning to add a dashboard where if even a mcahine is added in machines array it should reflect in the processes in App Component and the dashboard should be updated.
I will appreciate your help.
App component
import React, { useEffect, useState } from 'react';
import { Container, Typography, Box, Button } from '#mui/material'
import MachineGround from './Components/MachineGround'
import { Process } from './types'
const App = () => {
const [processes, setProcesses] = useState<Process[]>([{
Name: 'Process-1', machines: [
{
Name: 'Machine-1', devices: [{
Name: 'device-1',
type: 'Timer'
}]
}]
}]) // dummy process
// const [processes, setProcesses] = useState<Process[]>([])
const [count, setCount] = useState<number>(1) // dummy process count.
// Add Process
const addProcess = () => {
if (processes.length < 10) {
setCount(count + 1)
const processNow: Process = {
Name: `Process-${count}`,
machines: []
}
setProcesses((process) => {
return (
[...process, processNow]
)
})
} else {
alert("Limit can't exceeds 10")
}
}
// Delete Process
const deleteProcess = (passProcess: Process) => {
setProcesses(processes.filter((process) => process.Name !== passProcess.Name))
}
useEffect(() => {
console.log(processes)
}, [processes])
return (
<>
<Container maxWidth='lg'>
<Typography variant='h3' mt={5} sx={{ fontWeight: 700 }} align={'center'} gutterBottom>
My DashBoard
</Typography>
<Box sx={{ bgcolor: '#F4F4F7', paddingInline: 5, borderRadius: 10 }} >
{/* here will be the renders of processes */}
{
processes.map((process) => (
<Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }} pb={2} pt={2}>
<Typography variant='h6' >
{process.Name}
</Typography>
<Button variant='contained' onClick={() => {
deleteProcess(process)
}}>
Delete
</Button>
</Box>
<MachineGround process={process} />
</Box>
))
}
</Box>
<Button variant='contained' color='primary' sx={{ marginBlock: 5, marginLeft: 10 }} onClick={addProcess}> Add Process</Button>
</Container>
</>
);
}
export default App;
import React, { useEffect, useState } from 'react'
import DeviceGround from './DeviceGround'
import { Box, Typography, Button } from '#mui/material'
//types
import { Machine, Process } from '../types'
type Props = {
process: Process
}
const MachineGround = ({ process }: Props) => {
const [count, setCount] = useState<number>(1)
const [machines, setMachines] = useState<Machine[]>(process.machines)
const handleAddMachine = () => {
if (machines.length < 10) {
const newMachine: Machine = { Name: `Machine-${count}`, devices: [] }
setMachines((machines) => [...machines, newMachine])
setCount(count + 1)
} else {
alert("You can't add more than 10 Machines.")
}
}
const handleDeleteMachine = (machine: Machine) => {
setMachines(machines.filter((current) => current.Name !== machine.Name))
}
useEffect(() => {
console.log('machines Array Changed')
}, [machines])
return (
<Box sx={{ bgcolor: '#00e676', borderRadius: 5 }} mt={2} ml={3} mr={3} pt={1} pb={1} mb={2}>
{machines.map((machine: Machine) => {
return (
<>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }} mt={2}>
<Typography paragraph ml={5} sx={{ fontWeight: 700 }}>
{machine.Name}
</Typography>
<Button variant='outlined' size='small' sx={{ marginRight: 5 }} onClick={() => {
handleDeleteMachine(machine)
}}>Delete Machine</Button>
</Box>
<Box>
{/* {
machine.devices.length !== 0 ?
<DeviceGround machine={machine}></DeviceGround>
: null we dont need conditional render
} */}
<DeviceGround machine ={machine} />
</Box>
</>
)
})}
<Button variant='contained' size='small' sx={{ marginLeft: 5 }} onClick={handleAddMachine}>Add Machine</Button>
</Box >
)
}
export default MachineGround
I am thinking that should I use Redux ? or another state management then what should I do? I messed up the states.
State management tools like Redux, Context-API can be a good option here but even if you do not want to use them, you can make use of normal JavaScript functions. Just pass them as props from your parent component to child component.
I will explain what I mean here. Write a function in your parent component which take a machine object and update the machines array. Now pass this component as props to your child component. Now inside your child component call this function with the machine object that you want to add to your machines array. And boom, your machines array in parent will be updated.
Related
I have data returned from the backend as an array that i want to populate on react component.
home.js
import Head from "next/head";
import Header from "../src/components/Header";
import * as React from 'react';
import { styled } from '#mui/material/styles';
import Box from '#mui/material/Box';
import Paper from '#mui/material/Paper';
import Grid from '#mui/material/Grid';
import TextField from '#mui/material/TextField';
import SendIcon from '#mui/icons-material/Send';
import Stack from '#mui/material/Stack';
import Button from '#mui/material/Button';
import getDupImages from "../src/services/getDupImages";
import { useState, useEffect } from 'react'
const Item = styled(Paper)(({ theme }) => ({
backgroundColor: theme.palette.mode === 'dark' ? '#1A2027' : '#fff',
...theme.typography.body2,
padding: theme.spacing(1),
textAlign: 'center',
color: theme.palette.text.secondary,
}));
export default function Home({data}) {
let _data;
const fetchData = async () => {
_data = await getDupImages();
console.log("DATA>>>", _data);
return _data;
};
const submit = (event) => {
event.preventDefault();
fetchData();
}
return (
<>
<Head>
<title>De-Dup</title>
<link rel="icon" type="image/ico" href="/img/goals.ico" />
</Head>
<Header />
<section>
<Box sx={{ flexGrow: 1 }}>
<Grid container spacing={2}>
<Grid item xs={5}>
<Box
component="form"
sx={{
'& > :not(style)': { m: 1, width: '75ch' },
}}
noValidate
autoComplete="off"
>
<TextField id="outlined-basic" label="location path" variant="outlined" />
<Stack direction="row" spacing={2}>
<Button variant="contained" onClick={submit} endIcon={<SendIcon />}>
Submit
</Button>
</Stack>
</Box>
</Grid>
<Grid item xs={7}>
{data.map((d) => {
return (
<div>
{d.title}
</div>
);
})}
</Grid>
</Grid>
</Box>
</section>
</>
);
}
Error
1 of 1 unhandled error
Server Error
TypeError: Cannot read property 'map' of undefined
This error happened while generating the page. Any console logs will be displayed in the terminal window.
Put the data in the component state and check if there actually is data before displaying it.
const [data, setData] = useState();
const fetchData = async () => {
setData(await getDupImages());
}
Then in your JSX:
{!!data && data.map(d => <div>{d.title}</div>}
You are trying to render the data before it is available. Use this instead
{data && data.map((d) => {
return (
<div>
{d.title}
</div>
);
})}
Either initialise the data state as an array or use the Optional chaining (?.) operator before the map function:
data?.map((d) => {
return <div>{d.title}</div>;
})
Hope this helps.
I am trying to fetch data with RTK Query in next.js project and everything were fine until I had to fetch some more data from /api/exams endpoint. I have fetched almost everything from that endpoint and i know that's working fine but i still can't fetch some data from it. I'll provide screenshots of all code that's related to it. ok so here is the code where endpoints are:
Then let's continue with exams-response where i define body of endpoint:
Now I will provide code in my custom hook where i import that data from api/exams endpoint query:
And now i will show code of the actual page where i use them and where i think problem may lie also with another file which i will provide after this:
import { memo } from "react"
import { useIntl } from "react-intl"
import Stack from "#mui/material/Stack"
import Typography from "#mui/material/Typography"
import { ExamoTypesEnum } from "src/common/types/examo-types-enum"
import { rolesEnum } from "src/core/roles-enum"
import { useExamAssign } from "src/features/exams/hooks/use-exam-assign"
import { useExams } from "src/features/exams/hooks/use-exams"
import { useStartExam } from "src/features/exams/hooks/use-start-exam"
import { useIsMobile } from "src/helpers/use-is-mobile"
import { useAppSelector } from "src/redux/hooks"
import { ExamoCard } from "src/ui/examo/examo-card"
import { ExamoCardsGrid } from "src/ui/examo/examo-cards-grid"
import { ExamoHeader } from "src/ui/examo/examo-header"
import { CustomizedDialogs } from "src/ui/filter"
import { LoadingSpinner } from "src/ui/loading-spinner"
import { styled } from "src/ui/theme"
import { UnauthenticatedComponent } from "src/ui/unauthenticated"
import { useTags } from "../tags/hooks/use-tags"
import { useActiveExamQuery } from "./api"
const Ourbox = styled.div`
display: flex;
justify-content: space-between;
`
export const ExamsPage = memo(() => {
const isMobile = useIsMobile()
const userRole = useAppSelector((state) => state.auth.role)
const intl = useIntl()
const {
exams,
isLoadingExams,
selectedTags,
setSelectedTags,
checkedFilterTags,
setCheckedFilterTags,
} = useExams()
const { availableTags } = useTags()
const isLoadingAnExam = useAppSelector((state) => state.exam.isLoadingAnExam)
const { startAsync } = useStartExam()
const { data: activeExam, isFetching: isFetchingActiveExam } =
useActiveExamQuery(undefined, { refetchOnMountOrArgChange: 1 })
if (userRole === rolesEnum.None) {
return <UnauthenticatedComponent />
}
return (
<Stack sx={{ paddingX: isMobile ? 3 : "10vw", paddingY: 4 }} gap={4}>
<Ourbox>
<ExamoHeader
header={intl.formatMessage({
id: "exams-header",
defaultMessage: "Choose your exam",
})}
subtitle={intl.formatMessage({
id: "exams-header-subtitle",
defaultMessage:
"Our operators make quizzes and tests to help you upgrade and test your skills.",
})}
/>
<CustomizedDialogs
id="exams-page-filter"
selectedTags={selectedTags}
setSelectedTags={setSelectedTags}
availableTags={availableTags || []}
checkedFilterTags={checkedFilterTags}
setCheckedFilterTags={setCheckedFilterTags}
/>
</Ourbox>
{isLoadingExams && <LoadingSpinner />}
{!isLoadingExams && (!exams || exams.length === 0) && (
<Typography>
{intl.formatMessage({
id: "no-exams-available",
defaultMessage: "No exams available",
})}
</Typography>
)}
{exams && exams.length > 0 && (
<ExamoCardsGrid>
{exams.map((exam) => (
<ExamoCard
key={exam.id}
type={ExamoTypesEnum.EXAM}
useAssign={useExamAssign}
isStartButtonDisabled={
isLoadingAnExam ||
isFetchingActiveExam ||
(activeExam?.exam?.id !== undefined &&
exam.id !== activeExam.exam.id)
}
isResuming={
activeExam?.exam?.id !== undefined &&
exam.id === activeExam.exam.id
}
handleStart={() => startAsync(exam.id)}
isLoading={isLoadingAnExam}
title={exam.title}
duration={exam.duration}
tags={[
...new Set(exam.templates?.flatMap((et) => et.tags) || []),
]}
numberOfQuestions={exam.templates.reduce(
(total, current) => total + current.numberOfQuestions,
0,
)}
deadline-start={exam["deadline-start"]}
deadline-end={exam["deadline-end"]}
i={exam.id}
/>
))}
</ExamoCardsGrid>
)}
</Stack>
)
})
and the last one which is as mapped through in above the code. so it's :
import { memo } from "react"
import * as React from "react"
import { useIntl } from "react-intl"
import AccessTimeIcon from "#mui/icons-material/AccessTime"
import ExpandMoreIcon from "#mui/icons-material/ExpandMore"
import MoreHorizIcon from "#mui/icons-material/MoreHoriz"
import Box from "#mui/material/Box"
import Card from "#mui/material/Card"
import MuiChip from "#mui/material/Chip"
import Collapse from "#mui/material/Collapse"
import Grid from "#mui/material/Grid"
import IconButton, { IconButtonProps } from "#mui/material/IconButton"
import Stack from "#mui/material/Stack"
import { styled } from "#mui/material/styles"
import Typography from "#mui/material/Typography"
import { ExamoTypesEnum } from "src/common/types/examo-types-enum"
import { useAssignType } from "src/common/types/use-assign-type"
import { useAppSelector } from "src/redux/hooks"
import { ExamoAssignTo } from "src/ui/examo/examo-assign-to"
import { ExamoStartDialogBtn } from "src/ui/examo/examo-start-btn"
interface props {
duration: string | null
title: string
tags: string[] | null
numberOfQuestions: number | null
isLoading: boolean
handleStart: () => void
useAssign: useAssignType
isStartButtonDisabled: boolean
isResuming: boolean
type: ExamoTypesEnum
i: number
"deadline-start": string
"deadline-end": string
}
export const ExamoCard = memo(
({
duration,
tags,
title,
isLoading,
numberOfQuestions,
isStartButtonDisabled,
isResuming,
type,
handleStart,
useAssign,
i,
"deadline-end": deadlineEnd,
"deadline-start": deadlineStart,
}: props) => {
console.log(deadlineStart)
const intl = useIntl()
const user = useAppSelector((state) => state.auth)
const durationHours = duration?.split(":")[0]
const durationMinutes = duration?.split(":")[1]
// const [expanded, setExpanded] = React.useState(false)
const [expandedId, setExpandedId] = React.useState(-1)
// const handleExpandClick = () => {
// setExpanded(!expanded)
// }
const handleExpandClick = (i: number) => {
setExpandedId(expandedId === i ? -1 : i)
}
const preventParentOnClick = (event: React.MouseEvent<HTMLElement>) => {
event.stopPropagation()
}
interface ExpandMoreProps extends IconButtonProps {
expand: boolean
}
const ExpandMore = styled((props: ExpandMoreProps) => {
const { expand, ...other } = props
return <IconButton {...other} />
})(({ theme, expand }) => ({
transform: !expand ? "rotate(0deg)" : "rotate(180deg)",
transition: theme.transitions.create("transform", {
duration: theme.transitions.duration.shortest,
}),
}))
return (
<Grid item xs={12} lg={6}>
<Card
onClick={() => handleExpandClick(i)}
aria-expanded={expandedId === i}
elevation={2}
sx={{
padding: "1rem",
height: "100%",
borderRadius: "1rem",
border: "solid 1px var(--palette-grey-400)",
transition: "all 0.1s ease-in-out",
":hover": {
backgroundColor: "var(--palette-grey-100)",
cursor: "pointer",
},
}}
>
<Stack direction="row" justifyContent="space-between">
<Stack
gap={numberOfQuestions ? 1.5 : 6}
sx={{
width: "100%",
}}
>
<Stack
direction="row"
sx={{
justifyContent: "space-between",
}}
>
<Typography
variant="h5"
sx={{ whiteSpace: "pre-wrap", wordBreak: "break-all" }}
>
{title}
</Typography>
<ExpandMore expand={expandedId === i}>
<ExpandMoreIcon />
</ExpandMore>
<Stack
direction="row"
gap={1}
sx={{
justifyContent: "flex-end",
alignItems: "center",
marginTop: "-1.75rem",
}}
>
<AccessTimeIcon />
<Typography
whiteSpace="nowrap"
variant="h6"
>{`${durationHours}h ${durationMinutes}m`}</Typography>
</Stack>
</Stack>
<Collapse in={expandedId === i} timeout="auto" unmountOnExit>
<Stack direction="row" gap={4}>
{numberOfQuestions && (
<Typography variant="h6">
{`${numberOfQuestions} ${intl.formatMessage({
id: "questions",
defaultMessage: "Questions",
})}`}
</Typography>
)}
</Stack>
<Stack
direction="row"
sx={{ flexWrap: "wrap", gap: 1, marginTop: "1rem" }}
>
{tags?.map((t, index) => (
<MuiChip
key={index}
label={t}
variant="filled"
sx={{ fontWeight: "bold" }}
color="secondary"
size="small"
/>
))}
</Stack>
</Collapse>
</Stack>
<Stack
sx={{ marginTop: "-0.5rem" }}
justifyContent="space-between"
alignItems="center"
spacing={user.role === "Operator" ? 0.1 : 1}
>
<MoreHorizIcon sx={{ marginLeft: "2rem" }} />
<Box onClick={preventParentOnClick}>
<Stack sx={{ marginLeft: "1.5rem" }}>
{user.role === "Operator" && (
<ExamoAssignTo useAssign={useAssign} title={title} />
)}
</Stack>
</Box>
<Box onClick={preventParentOnClick}>
<ExamoStartDialogBtn
type={type}
isResuming={isResuming}
handleStart={handleStart}
isLoading={isLoading}
isDisabled={isStartButtonDisabled}
/>
</Box>
</Stack>
</Stack>
</Card>
</Grid>
)
},
)
To sum up guys, I want to also fetch deadlineStart and deadlineEnd but i can't. I think problem is in last file or second to last, because maybe am not defining them properly in interface props in last code. And also i almost forgot to mention that when I try to console.log(deadlineStart) in the last code it says undefined in the browser. Edited Network Pic :
here is screenshot when i console log single exams :
I've been working on simplifying my code and am curious how I would approach passing a secondary value using props and fetching data from the back end. I'm using material UI's Autocomplete and the PERN stack. Everything is working, except I want to change "region_name" to be a prop. So I can change the values dynamically within event details.js. when I'm fetching other data.
I currently have this component setup.
Production.js
import Autocomplete from "#mui/material/Autocomplete";
import Box from "#mui/material/Box";
import Stack from "#mui/material/Stack";
import TextField from "#mui/material/TextField";
export default function CustomAutoComplete(props) {
return (
<Stack sx={{ m: 1 }}>
<Autocomplete
sx={{ ml: 2, mr: 2 }}
size="small"
id="combo-box-demo"
freeSolo
inputValue={props.inputValue}
onInputChange={(event, newValue) => {
props.set(newValue);
}}
getOptionLabel={(data) => `${data.region_name}`}
options={props.data}
isOptionEqualToValue={(option, value) =>
option.region_name === value.region_name
}
renderOption={(props, data) => (
<Box component="li" {...props} key={data.id}>
{data.region_name}
</Box>
)}
renderInput={(params) => <TextField {...params} label="Region" />}
/>
</Stack>
);
}
Then importing it into a separate file EventDetails.js fetching the data and storing it in LocalStorage, which I'll move to useState eventually.
import CustomAutoComplete from "../../atoms/AutoComplete";
import FormGroup from "#mui/material/FormGroup";
import { Fragment, useState, useEffect } from "react";
import { useLocalStorage } from "../../utils/LocalStorage.js";
const EventDetails = () => {
const [region, setRegion] = useLocalStorage("region", "");
const [getRegion, setGetRegion] = useState([]);
// fetching backend data
useEffect(() => {
fetch("/authentication/region")
.then((response) => response.json())
.then((getRegion) => setGetRegion(getRegion));
}, []);
return (
<Fragment>
<FormGroup sx={{ p: 2, m: 1 }}>
<CustomAutoComplete
inputValue={region}
set={setRegion}
data={getRegion}
key={getRegion.id}
name={region_name} // <--feeble attempt
label="Region"
/>
</FormGroup>
</Fragment>
);
};
I had someone help me with finding a solution just had to create a prop variable like
export default function CustomAutoComplete(props) {
const { labelValue } = props;
return (renderOption={(props, data) => (
<Box component="li" {...props} key={data.id}>
{data[labelKey]}
</Box>
)})
then in the parent component
<CustomAutoComplete labelValue="region_name" />
I'm running into an issue that I can't quite figure out. I'm building a Wordle clone, the state seems to be updating on some events and not on others, and I can't quite track down why.
I have a Keyboard component, which takes handleKeyClick as a prop from the parent component, and that is attached to two event handlers.
Parent Component
import { Box, Divider, Grid, Typography } from "#mui/material";
import { useState, useEffect, useCallback } from 'react';
import Keyboard from "../Keyboard";
import { v4 as uuid } from 'uuid'
import WordleNotifbar from "../WordleNotifBar";
import Loading from "../Utils/Loading";
import { IGuessState } from "../../types";
interface IGuessGridProps {
addGuess: Function,
guesses: any,
answer: any
}
const GuessGrid = (props: IGuessGridProps) => {
const { addGuess, guesses, answer } = props;
let [notif, setNotif] = useState<boolean>(false);
const [guess, setGuess] = useState<string[]>([]);
const styles = {
input: {
border: ".5px solid white",
height: "50px",
display: "flex",
borderRadius: "5px",
justifyContent: "center",
alignItems: "center",
backgroundColor: "",
color: "white",
},
container: {
minWidth: "300px",
width: "30%",
maxWidth: "450px",
margin: "0 auto",
marginTop: "15px",
},
}
// In the parent component, I have defined the function I'm passing in as a prop as such:
const handleAddCharacter = (char: string) => {
setGuess([...guess, char])
}
// Not fully implemented yet
const handleBackspace = (e: MouseEvent): void => {
e.preventDefault();
setGuess([...guess])
}
const handleSubmit = (): void => {
let word = guess.join('')
if (word.length === answer.length) {
addGuess(word.toLowerCase())
setGuess([]);
}
else {
setNotif(true);
setTimeout(() => {
setNotif(false);
}, 1000)
}
}
if (answer) {
return <>
<Divider />
<Grid container sx={styles.container} >
{answer.split('').map((_: string, index: number) => {
return (<Grid item xs={12 / answer.length} sx={styles.input} key={uuid()}>
<Box>
<Typography>
{guess[index]}
</Typography>
</Box>
</Grid>)
})}
</Grid>
<Keyboard guesses={guesses} answer={answer} handleKeyClick={handleAddCharacter} handleBackspace={handleBackspace} submitFunc={handleSubmit} />
{notif ? <WordleNotifbar message="Not Enough Characters" duration={1000} /> : ""}
</>;
} else {
return <Loading />
}
};
export default GuessGrid;
Keyboard Component
import { Box, Grid, SxProps, Theme, Typography } from "#mui/material";
import { useCallback, useEffect, useState } from 'react';
import { v4 as uuid } from 'uuid';
import BackspaceIcon from '#mui/icons-material/Backspace';
import React from "react";
interface IKeyboardProps {
guesses: string[],
answer: string,
handleKeyClick: any,
submitFunc: any,
handleBackspace: any
}
const Keyboard = (props: IKeyboardProps) => {
const { guesses, answer, handleKeyClick, submitFunc, handleBackspace } = props
const [guessedLetters, setGuessedLetters] = useState<string[]>();
const topRow = 'qwertyuiop'.toUpperCase().split('');
const middleRow = 'asdfghjkl'.toUpperCase().split('');
const bottomRow = 'zxcvbnm'.toUpperCase().split('');
const allKeys = topRow.concat(middleRow.concat(bottomRow));
// When the component is initialized, I am establishing an event listener in the window for the key press events.
useEffect(() => {
window.addEventListener('keypress', handlePhysicalKeyPress)
}, [])
useEffect(() => {
const allGuessedCharacters = guesses.join('').split('');
const uniqueGuessedCharacters = allGuessedCharacters.filter((val: string, index: number, self) => self.indexOf(val) === index)
setGuessedLetters(uniqueGuessedCharacters);
}, [guesses])
const handleVirtualKeyPress = (e: any) => {
handleKeyClick(e.target.textContent)
}
const handlePhysicalKeyPress = (e: KeyboardEvent) => {
e.preventDefault()
if (allKeys.includes(e.key.toUpperCase())) {
handleKeyClick(e.key.toUpperCase());
}
}
const genKeyStyles = (character: string, _: number): SxProps<Theme> => {
character = character.toLowerCase()
const styles = {
width: character === "bs" || character === "enter" ? "63px" : "33px",
marginX: "1px",
marginY: "1px",
borderRadius: "5px",
height: "50px",
color: "black",
textAlign: "center",
backgroundColor: "#DDD",
display: "flex",
justifyContent: "center",
alignItems: "center",
};
if (guessedLetters) {
if (answer.indexOf(character) >= 0 && guessedLetters.indexOf(character) >= 0) {
styles.backgroundColor = "green"
} else if (answer.indexOf(character) < 0 && guessedLetters.indexOf(character) >= 0) {
styles.backgroundColor = "#777"
}
}
return styles
}
return <Box sx={{ display: "flex", flexDirection: "column", justifyContent: "center", marginTop: "10px", }}>
<Box sx={{ display: "flex", justifyContent: "center" }}>
{topRow.map((letter: string, index: any) => {
return (
<Box sx={genKeyStyles(letter, index)} key={uuid()} onClick={handleVirtualKeyPress}>
<Typography key={uuid()}>{letter}</Typography>
</Box>
)
})}
</Box>
<Box sx={{ display: "flex", justifyContent: "center" }}>
{middleRow.map((letter: string, index: any) => {
return (
<Box sx={genKeyStyles(letter, index)} key={uuid()} onClick={handleVirtualKeyPress}>
<Typography key={uuid()}>{letter}</Typography>
</Box>
)
})}
</Box>
<Box sx={{ display: "flex", justifyContent: "center" }}>
<Box sx={genKeyStyles("enter", 1)} key={uuid()} onClick={submitFunc}>
<Typography key={uuid()}>enter</Typography>
</Box>
{bottomRow.map((letter: string, index: any) => {
return (
<Box sx={genKeyStyles(letter, index)} key={uuid()} onClick={handleVirtualKeyPress}>
<Typography key={uuid()}>{letter}</Typography>
</Box>
)
})}
<Box sx={genKeyStyles("bs", 1)} key={uuid()} onClick={handleBackspace}>
<Typography key={uuid()}><BackspaceIcon /></Typography>
</Box>
</Box>
</Box>
};
export default Keyboard;
What happens is that the virtual key press seems to update the state properly, but the physical keypress seems to reset the state back to an empty array. I can't really figure out a good reason why this is happening. Any thoughts? I appreciate your help in advance!
Link to Live Application
When you do:
useEffect(() => {
window.addEventListener('keypress', handlePhysicalKeyPress)
}, [])
...you are attaching a specific handlePhysicalKeyPress function as event listener. But that function is re-created at each component re-render, so you no longer reference the "current" function "version" (should you try to remove it, you would not be able to because it is no longer the same reference).
As such, the actual listener is the very first "version" of your function, which calls the very first "version" of your handleKeyClick prop, which is the very first "version" of your handleAddCharacter function, which knows only the very first version of your guess state... which is an empty array.
That is why when handlePhysicalKeyPress is executed by a key press, it builds a new guess array from an empty array.
While you should avoid this discrepancy between what you attach to your event listener and your actual "render-time" function, there should be a very simple solution to your specific case: should you use the functional form of your state setter, even if it is the "very first version", it should use the "current" state version:
setGuess((currentGuess) => [...currentGuess, char])
I have a dynamically generated set of dropdowns and accordions that populate at render client-side (validated user purchases from db).
I'm running into an error that I'm sure comes from my menu anchorEl not knowing 'which' menu to open using anchorEl. The MUI documentation doesn't really cover multiple dynamic menus, so I'm unsure of how to manage which menu is open
Here is a pic that illustrates my use-case:
As you can see, the menu that gets anchored is actually the last rendered element. Every download button shows the last rendered menu. I've done research and I think I've whittled it down to the anchorEl and open props.
Here is my code. Keep in mind, the data structure is working as intended, so I've omitted it to keep it brief, and because it's coming from firebase, I'd have to completely recreate it here (and I think it's redundant).
The component:
import { useAuth } from '../contexts/AuthContext'
import { Accordion, AccordionSummary, AccordionDetails, Button, ButtonGroup, CircularProgress, ClickAwayListener, Grid, Menu, MenuItem, Typography } from '#material-ui/core'
import { ExpandMore as ExpandMoreIcon } from '#material-ui/icons'
import LoginForm from '../components/LoginForm'
import { motion } from 'framer-motion'
import { useEffect, useState } from 'react'
import { db, functions } from '../firebase'
import styles from '../styles/Account.module.scss'
export default function Account() {
const { currentUser } = useAuth()
const [userPurchases, setUserPurchases] = useState([])
const [anchorEl, setAnchorEl] = useState(null)
const [generatingURL, setGeneratingURL] = useState(false)
function openDownloads(e) {
setAnchorEl(prevState => (e.currentTarget))
}
function handleClose(e) {
setAnchorEl(prevState => null)
}
function generateLink(prefix, variationChoice, pack) {
console.log("pack from generate func", pack)
setGeneratingURL(true)
const variation = variationChoice ? `${variationChoice}/` : ''
console.log('link: ', `edit-elements/${prefix}/${variation}${pack}.zip`)
setGeneratingURL(false)
return
if (pack.downloads_remaining === 0) {
console.error("No more Downloads remaining")
setGeneratingURL(false)
handleClose()
return
}
handleClose()
const genLink = functions.httpsCallable('generatePresignedURL')
genLink({
fileName: pack,
variation: variation,
prefix: prefix
})
.then(res => {
console.log(JSON.stringify(res))
setGeneratingURL(false)
})
.catch(err => {
console.log(JSON.stringify(err))
setGeneratingURL(false)
})
}
useEffect(() => {
if (currentUser !== null) {
const fetchData = async () => {
// Grab user products_owned from customers collection for user UID
const results = await db.collection('customers').doc(currentUser.uid).get()
.then((response) => {
return response.data().products_owned
})
.catch(err => console.log(err))
Object.entries(results).map(([product, fields]) => {
// Grabbing each product document to get meta (title, prefix, image location, etc [so it's always current])
const productDoc = db.collection('products').doc(product).get()
.then(doc => {
const data = doc.data()
const productMeta = {
uid: product,
title: data.title,
main_image: data.main_image,
product_prefix: data.product_prefix,
variations: data.variations
}
// This is where we merge the meta with the customer purchase data for each product
setUserPurchases({
...userPurchases,
[product]: {
...fields,
...productMeta
}
})
})
.catch(err => {
console.error('Error retrieving purchases. Please refresh page to try again. Full error: ', JSON.stringify(err))
})
})
}
return fetchData()
}
}, [currentUser])
if (userPurchases.length === 0) {
return (
<CircularProgress />
)
}
return(
currentUser !== null && userPurchases !== null ?
<>
<p>Welcome, { currentUser.displayName || currentUser.email }!</p>
<Typography variant="h3" style={{marginBottom: '1em'}}>Purchased Products:</Typography>
{ userPurchases && Object.values(userPurchases).map((product) => {
const purchase_date = new Date(product.purchase_date.seconds * 1000).toLocaleDateString()
return (
<motion.div key={product.uid}>
<Accordion style={{backgroundColor: '#efefef'}}>
<AccordionSummary expandIcon={<ExpandMoreIcon style={{fontSize: "calc(2vw + 10px)"}}/>} aria-controls={`${product.title} accordion panel`}>
<Grid container direction="row" alignItems="center">
<Grid item xs={3}><img src={product.main_image} style={{ height: '100%', maxHeight: "200px", width: '100%', maxWidth: '150px' }}/></Grid>
<Grid item xs={6}><Typography variant="h6">{product.title}</Typography></Grid>
<Grid item xs={3}><Typography variant="body2"><b>Purchase Date:</b><br />{purchase_date}</Typography></Grid>
</Grid>
</AccordionSummary>
<AccordionDetails style={{backgroundColor: "#e5e5e5", borderTop: 'solid 6px #5e5e5e', padding: '0px'}}>
<Grid container direction="column" className={styles[`product-grid`]}>
{Object.entries(product.packs).map(([pack, downloads]) => {
// The pack object right now
return (
<Grid key={ `${pack}-container` } container direction="row" alignItems="center" justify="space-between" style={{padding: '2em 1em'}}>
<Grid item xs={4} style={{ textTransform: 'uppercase', backgroundColor: 'transparent' }}><Typography align="left" variant="subtitle2" style={{fontSize: 'calc(.5vw + 10px)'}}>{pack}</Typography></Grid>
<Grid item xs={4} style={{ backgroundColor: 'transparent' }}><Typography variant="subtitle2" style={{fontSize: "calc(.4vw + 10px)"}}>{`Remaining: ${downloads.downloads_remaining}`}</Typography></Grid>
<Grid item xs={4} style={{ backgroundColor: 'transparent' }}>
<ButtonGroup variant="contained" fullWidth >
<Button id={`${pack}-btn`} disabled={generatingURL} onClick={openDownloads} color='primary'>
<Typography variant="button" style={{fontSize: "calc(.4vw + 10px)"}} >{!generatingURL ? 'Downloads' : 'Processing'}</Typography>
</Button>
</ButtonGroup>
<ClickAwayListener key={`${product.product_prefix}-${pack}`} mouseEvent='onMouseDown' onClickAway={handleClose}>
<Menu anchorOrigin={{ vertical: 'top', horizontal: 'right' }} transformOrigin={{ vertical: 'top', horizontal: 'right' }} id={`${product}-variations`} open={Boolean(anchorEl)} anchorEl={anchorEl}>
{product.variations && <MenuItem onClick={() => generateLink(product.product_prefix, null, pack) }>{`Pack - ${pack}`}</MenuItem>}
{product.variations && Object.entries(product.variations).map(([variation, link]) => {
return (
<MenuItem key={`${product.product_prefix}-${variation}-${pack}`} onClick={() => generateLink(product.product_prefix, link, pack)}>{ variation }</MenuItem>
)
})}
</Menu>
</ClickAwayListener>
</Grid>
</Grid>
)}
)}
</Grid>
</AccordionDetails>
</Accordion>
</motion.div>
)
})
}
</>
:
<>
<p>No user Signed in</p>
<LoginForm />
</>
)
}
I think it also bears mentioning that I did check the rendered HTML, and the correct lists are there in order - It's just the last one assuming the state. Thanks in advance, and please let me know if I've missed something, or if I can clarify in any way. :)
i couldn't manage to have a menu dynamic,
instead i used the Collapse Panel example and there i manipulated with a property isOpen on every item of the array.
Check Cards Collapse Example
On the setIsOpen method you can change this bool prop:
const setIsOpen = (argNodeId: string) => {
const founded = tree.find(item => item.nodeId === argNodeId);
const items = [...tree];
if (founded) {
const index = tree.indexOf(founded);
founded.isOpen = !founded.isOpen;
items[index]=founded;
setTree(items);
}
};
<IconButton className={clsx(classes.expand, {
[classes.expandOpen]: node.isOpen,
})}
onClick={()=>setIsOpen(node.nodeId)}
aria-expanded={node.isOpen}
aria-label="show more"
>
<MoreVertIcon />
</IconButton>
</CardActions>
<Collapse in={node.isOpen} timeout="auto" unmountOnExit>
<CardContent>
<MenuItem onClick={handleClose}>{t("print")}</MenuItem>
<MenuItem onClick={handleClose}>{t("commodities_management.linkContainers")}</MenuItem>
<MenuItem onClick={handleClose}>{t("commodities_management.linkDetails")}</MenuItem>
</CardContent>
</Collapse>
I think this is the right solution for this: https://stackoverflow.com/a/59531513, change the anchorEl for every Menu element that you render. :D
This code belongs to TS react if you are using plain JS. Then remove the type.
import Menu from '#mui/material/Menu';
import MenuItem from '#mui/material/MenuItem';
import { useState } from 'react';
import { month } from '../../helper/Utilities';
function Company() {
const [anchorEl, setAnchorEl] = useState<HTMLElement[]>([]);
const handleClose = (event: any, idx: number) => {
let array = [...anchorEl];
array.splice(idx, 1);
setAnchorEl(array);
};
<div>
{month &&
month.map((val: any, ind: number) => {
return (
<div
key={val.id + 'w9348w344ndf allBankAndCardAccountOfClient'}
style={{ borderColor: ind === 0 ? '#007B55' : '#919EAB52' }}
>
<Menu
id='demo-positioned-menu'
aria-labelledby='demo-positioned-button'
anchorEl={anchorEl[ind]}
open={anchorEl[ind] ? true : false}
key={val.id + 'w9348w344ndf allBankAndCardAccountOfClient' + ind}
onClick={(event) => handleClose(event, ind)}
anchorOrigin={{
vertical: 'top',
horizontal: 'left',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'left',
}}
>
<MenuItem
key={val.id + 'w9348w344ndf allBankAndCardAccountOfClient' + ind}
onClick={(event) => handleClose(event, ind)}
style={{
display: ind === 0 ? 'none' : 'inline-block',
}}
>
<span
style={{
marginLeft: '.5em',
color: 'black',
background: 'inherit',
}}
>
Make Primary
</span>
</MenuItem>
<MenuItem onClick={(event) => handleClose(event, ind)}>
<span style={{ marginLeft: '.5em', color: 'black' }}>Edit</span>
</MenuItem>
<MenuItem
onClick={(event) => handleClose(event, ind)}
style={{
display: ind === 0 ? 'none' : 'inline-block',
}}
>
<span style={{ marginLeft: '.5em', color: 'red' }}>Delete</span>
</MenuItem>
</Menu>
</div>
);
})}
</div>;
}
export default Company;