I am trying to insert a google maps map in a React application. I would rather not use a non-official library (the ones that I have found lack documentation) and I have already managed inserting the map.
My problem is that the map is re-rendered every time the state of the parent component changes; although the values that change are completely irrelevant from what the map needs.
After a bit of research (I am new to React) I came across the React.memo() HOC which is supposed to prevent re-renders of child components when their props are unchanged. For some reason however I cannot get it to work correctly. Event when I insert the map inside a component with no props, any change in the parent state results in a re-render of the map.
Here is the parent component:
const CompanyDepotsPopup = () => {
const classes = useStyles();
const dispatch = useDispatch();
const open = useSelector((state) => selectIsDepotsPopupOpen(state));
const company = useSelector((state) => selectSelectedCompany(state));
const depotsStatus = useSelector((state) => selectDepotsStatus(state));
const {t} = useTranslation();
const [value, setValue] = useState(0);
const [phone, setPhone] = useState("");
const handleChange = (event, newValue) => {
setValue(newValue);
};
const closeModal = () => {
dispatch(toggleDepotsPopup({}));
}
useEffect(() => {
if (company) {
dispatch(depotsListed({companyId: company.id}));
}
}, [company])
if (!company) return <></>;
if (depotsStatus === "loading") {
return <CenteredLoader/>
}
function TabPanel(props) {
const {children, value, index} = props;
return (
<div
hidden={value !== index}
style={{height: "100%"}}
>
{value === index && (
<Box boxShadow={3} mt={1} ml={2} mr={2} height={"100%"}>
{children}
</Box>
)}
</div>
);
}
return (
<Dialog fullWidth={true} open={open}
aria-labelledby="company-dialog-popup">
<DialogTitle >
{company.name}
</DialogTitle>
<DialogContent style={{padding: 0, margin: 0}}>
<Divider/>
<Box mr={0} ml={0} mt={0} p={0} height="95%">
<div >
<AppBar position="static">
<Tabs value={value} onChange={handleChange} aria-label="depots tabs" centered>
<Tab label={t("Company's depots list")}/>
<Tab label={t("Add new depot")}/>
</Tabs>
</AppBar>
<TabPanel value={value} index={0}>
<DepotsList/>
</TabPanel>
<TabPanel value={value} index={1}>
<Paper>
<Grid container spacing={2}>
<Grid item xs={12} sm={12} md={12} lg={12}>
<TextField
onChange={(event) => setPhone(event.target.value)}
id="phone"
label={t("Phone")}
type="text"
fullWidth
value={phone}
/>
</Grid>
<Grid item xs={12} sm={12} md={12} lg={12}>
<div style={{height: "250px", display: "flex", "flexDirection": "column"}}>
<MyMap
id="myMap"
/>
</div>
</Grid>
<Grid item xs={12} sm={12} md={12} lg={12} align={"center"}>
<Button variant={"outlined"}>
{t("Save")}
</Button>
</Grid>
</Grid>
</Paper>
</TabPanel>
</div>
</Box>
</DialogContent>
<DialogActions style={{marginTop: "20px"}}>
<Button
variant={"outlined"}
onClick={closeModal}
color="secondary"
>
Done
</Button>
</DialogActions>
</Dialog>
)}
And here is the Map component:
import React, {useEffect} from "react";
const Map = ({id}) => {
const onScriptLoad = () => {
const map = new window.google.maps.Map(
document.getElementById(id),
{
center: {lat: 41.0082, lng: 28.9784},
zoom: 8
}
);
const marker = new window.google.maps.Marker({
position: {lat: 41.0082, lng: 28.9784},
map: map,
title: 'Hello Istanbul!'
});
}
useEffect(() => {
if (!window.google) {
const s = document.createElement("script");
s.type = "text/javascript";
s.src = "https://maps.google.com/maps/api/js?key=''"
const x = document.getElementsByTagName('script')[0];
x.parentNode.insertBefore(s, x);
s.addEventListener('load', e => {
onScriptLoad();
})
} else {
onScriptLoad();
}
}, []);
return (
<div style={{width: "100%", height: "100%"}} id={id}/>
);
}
const MyMap = React.memo(Map);
export default MyMap;
Every time setPhone is called when the user types the phone and the state changes, the map is re-rendered. Could someone explain to me why the React.memo does not work and how should I proceed in order to avoid re-rendering the map?
I think my guts feeling is this component
function TabPanel(props) {
const {children, value, index} = props;
return (
<div
hidden={value !== index}
style={{height: "100%"}}
>
{value === index && (
<Box boxShadow={3} mt={1} ml={2} mr={2} height={"100%"}>
{children}
</Box>
)}
</div>
);
}
This is defined inside a component, therefore the instance of this component keeps changing after any state change. In order to prevent it, move it outside of the component, like this
function TabPanel()
function CompanyDepotsPopup()
Instead of
function CompanyDepotsPopup() {
function TabPanel()
}
The reason is also your TabPanel wraps everything else.
Related
Im having troubles to expand and contract a Collapse Component from MaterialUI since Im mapping and array and iterating the same component, when i press the collapse button, all components expands/contracts at the same time ( I suppose that Im not providing an identifier to point where the collapse function should be used),Im currently Using an State to control the collapse action:
const [expanded, setExpanded] = useState(false);
This is the return where I iterate the component using map on RecetasAll object,
return (
<React.Fragment key={RecetasAll.id}>
<Card className="searchItem" sx={{ maxWidth: 345 }}>
<CardHeader
action={<IconButton aria-label="settings"></IconButton>}
title={RecetasAll.titulo}
/>
<h4
className="Dieta"
style={{
backgroundColor: color(RecetasAll.Tiporeceta.tipoReceta),
}}
>
{RecetasAll.Tiporeceta.tipoReceta}
</h4>
<span className="Calorias">{RecetasAll.informacionNutricional}</span>
<CardMedia
component="img"
height="194"
image={RecetasAll.imagen}
alt="Paella dish"
/>
<CardContent>
{RecetasAll.Productos.map((Productos) => {
return (
<React.Fragment key={Productos.id}>
<Typography variant="body2" color="text.secondary">
{Productos.producto}
</Typography>
</React.Fragment>
);
})}
</CardContent>
<CardActions disableSpacing>
<IconButton aria-label="add to favorites">
<FavoriteIcon />
</IconButton>
<ExpandMore
expand={expanded}
onClick={() => setExpanded(!expanded)}
aria-expanded={expanded}
>
<ExpandMoreIcon />
</ExpandMore>
</CardActions>
<Collapse in={expanded} timeout="auto" unmountOnExit>
<CardContent id={RecetasAll.id}>
<Typography paragraph>Preparacion:</Typography>
<Typography paragraph>{RecetasAll.pasos}</Typography>
<Button
href="#contained-buttons"
variant="contained"
onClick={handleSearch}
>
Ver mas
</Button>
</CardContent>
</Collapse>
</Card>
</React.Fragment>
);
});
return <>{itemRecetas}</>;
}
Im triying to set an id property to the CardContent since its the child of the Collapse component
id={RecetasAll.id}
this is the function Im using to expand or collapse but I dont know how to get the id properly to compare its value with expanded state:
const handleExpandClick = (e) => {
let clickedItemId = e.currentTarget.id;
if (expanded === clickedItemId) {
setExpanded(!expanded);
} else {
setExpanded(clickedItemId);
}
};
You could refactor every card into a new component and that way you can have a state to open/close the individual card. When iterating you can pass in the RecetasAll.
const MyCard = ({ RecetasAll }) => {
const [isExpanded, setIsExpanded] = useState(false);
const toggleExpanded = () => {
setIsExpanded(prevIsExpanded => !prevIsExpanded);
};
return (
...
<ExpandMore
expand={isExpanded}
onClick={toggleExpanded}
aria-expanded={isExpanded}
>
...
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
...
);
};
If you don't want to use a new component you could store all the ids of the expanded cards in a state. Based on if the id is in the array the card will be expanded or collapsed.
const [expandedIds, setExpandedIds] = useState([]);
const toggleExpanded = (id) => {
setExpandedIds((prevExpandedIds) => {
// if id is already in array remove
if (prevExpandedIds.includes(id))
return prevExpandedIds.filter((i) => i !== id);
// else add to array
return [...prevExpandedIds, id];
});
};
return (
...
<ExpandMore
expand={expandedIds.includes(RecetasAll.id)}
onClick={() => toggleExpanded(RecetasAll.id)}
aria-expanded={expandedIds.includes(RecetasAll.id)}
>
...
<Collapse in={expandedIds.includes(RecetasAll.id)} timeout="auto" unmountOnExit>
...
)
I am doing a project and I want to show a modal window to compulsorily accept some Cookies.
I need that cookie window to be launched automatically, without pressing any button, when the application starts, that is, before the dashboard is loaded.
This is my code:
ModalCookie.js
imports ...
const style = {
position: 'absolute',
top: '50%',
left: '50%',
};
export default function CookiesModal() {
const handleOpen = () => setOpen(true);
const [open, setOpen] = React.useState(false);
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setCookiesPreview(false);
setOpen(false);
};
const handleNotClose = (event, reason) => {
if (reason && reason == "backdropClick")
return;
}
const [value, setValue] = React.useState('1');
const handleChange = (event, newValue) => {
setValue(newValue);
};
return (
<ThemeProvider theme={theme}>
<div>
<Fade in={open}>
<Box>
<BootstrapDialog
onClose={handleNotClose}
aria-labelledby="customized-dialog-title"
open={open}
closeAfterTransition
BackdropComponent={Backdrop}
BackdropProps={{
timeout: 500,
}}
>
<BootstrapDialogTitle id="customized-dialog-title" onClose={handleClose}>
</BootstrapDialogTitle>
<TabContext value={value}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<TabList onChange={handleChange} aria-label="lab API tabs example">
<Tab
label="AAAAA"
value="1" />
</TabList>
</Box>
<TabPanel value="1">
<p>...</p>
</TabPanel>
</TabContext>
<Button
onClick={handleClose}
variant="contained"
color="secondary"
>
Aceptar
</Button>
</BootstrapDialog>
</Box>
</Fade>
</div>
</ThemeProvider>
);
}
Config.js
// COOKIES
export const setCookiesPreview = ( state ) => {
localStorage.setItem('cookiesPreview', state)
}
// --
export const isCookiesPreview = () => {
const active=localStorage.getItem('cookiesPreview') ? localStorage.getItem('cookiesPreview') : true;
setCookiesPreview(false);
return(
active
);
}
And my dashboard:
imports...
// MAIN
export const Dashboard = () => {
const state = useSelector( state => state);
console.log('isCookiesPreview='+isCookiesPreview());
if(isCookiesPreview()){
console.log('CookiesPreview ON ------------------------------------------------')
setTimeout(() => {
CookiesModal.handleOpen();
}, 15000);
}
return (
<>
<ThemeProvider theme={theme}>
<div>
<HeaderBar/>
{(alertMessage != null) && (alertMessage.length>0) ? <Alert severity={alertSeverity}>{alertMessage}</Alert> : <></>}
<Grid container item={true} xs={12}>
<BodyGrid/>
</Grid>
<Grid container item={true} xs={12} pt={4}>
<BottomBar/>
</Grid>
</div>
</ThemeProvider>
</>
)
I am trying to use the handleOpen() constant from ModalCookie.js to open the window from Dashboard.js and save the locale that those cookies have been accepted so as not to show it the following times
I can't get the window to show up, but it does show me the logs I've put on the Dashboard related to cookies.
It tells me that HandleOpen is not a function.
I am working on a Frontend Mentor project and I am trying to pass the hover status from the parent component (App) to the child component (Listing).
You can see that I have created a state object called hover inside the App component and passed it to the Listing component but when the hover object updates the css style is not applied as it should be on the Typography element inside the Listing component. Or at least there isn't a re-render if it does.
App.js
let [hover, updateHover] = useState(false);
updateHover = () => {
if(hover === false){hover = true; console.log(hover); return(0);}
else{hover = false; console.log(hover); return;}
}
return (
<ThemeProvider theme={ theme }>
<div style={{backgroundColor:'hsl(180, 52%, 96%)',}}>
<div style={{backgroundImage:`url(${headerImage})`, backgroundSize:'cover', height:'10rem'}}></div>
<Box display="flex" justifyContent="center" alignItems="center">
<Box style={{width:'70%', marginTop:'5rem'}}>
<Card style={styles.listing} onMouseEnter={updateHover} onMouseLeave={updateHover}>
<CardActionArea href="#" className="listingHover">
<Listing id="one" hover={hover} />
</CardActionArea>
</Card>
</Box>
</Box>
</div>
</ThemeProvider>
);
}
export default App;
Listing.js
function Listing(props) {
let id = props.id
let hover = props.hover
return (
<React.Fragment>
<Box className="listing" display="flex" sx={{backgroundColor:'#fff', width:'100%', height:'7.3rem'}}>
<Box>
<Typography variant="h4"><Link className={hover ? 'jobTitle': null} href="#" color="secondary" underline="none">{jobs[id].name}</Link></Typography>
</Box>
</Box>
</Box>
</React.Fragment>
)
}
export default Listing
I think your problem here is that you re declared updateHover.
You should change the name and have something like
const [hover, setHover] = useState(false);
const updateHover = () => {
if (!hover) {
console.log(hover);
setHover(true);
return(0)
} else {
setHover(false)
return;
}
}
as an aside why are you returning from the function? do you need the console.log? a cleaner option would be
const [hover, setHover] = useState(false);
const updateHover = () => setHover(!hover) // flips the current value i.e same as if (hover === true) setHover(false) else setHover(true);
return (
<ThemeProvider theme={ theme }>
<div style={{backgroundColor:'hsl(180, 52%, 96%)',}}>
<div style={{backgroundImage:`url(${headerImage})`, backgroundSize:'cover', height:'10rem'}}></div>
<Box display="flex" justifyContent="center" alignItems="center">
<Box style={{width:'70%', marginTop:'5rem'}}>
<Card style={styles.listing} onMouseEnter={updateHover} onMouseLeave={updateHover}>
<CardActionArea href="#" className="listingHover">
<Listing id="one" hover={hover} />
</CardActionArea>
</Card>
</Box>
</Box>
</div>
</ThemeProvider>
);
}
You are wrong
change state in the React.
Try like this.
const [hover, setHover] = useState(false);
updateHover = () => {
setHover(!hover)
}
or
const [hover, setHover] = useState(false);
return (
<ThemeProvider theme={ theme }>
<div style={{backgroundColor:'hsl(180, 52%, 96%)',}}>
<div style={{backgroundImage:`url(${headerImage})`, backgroundSize:'cover', height:'10rem'}}></div>
<Box display="flex" justifyContent="center" alignItems="center">
<Box style={{width:'70%', marginTop:'5rem'}}>
<Card style={styles.listing} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}>
<CardActionArea href="#" className="listingHover">
<Listing id="one" hover={hover} />
</CardActionArea>
</Card>
</Box>
</Box>
</div>
</ThemeProvider>
);
I'm trying to bind to a nested array to text inputs from a form. There are 6 blocks and each block contains 3 values. I have initially populated the array for mapping by using:
new Array(6).fill(['','',''])
and rendering the values using 2 loops. One for the 6 blocks and the other 3 values inside each block. The 3 values are the ones binding to the form. I refer to each of the inputs using the second parameter of the map function which is the index.
Here is the code in its entirety.
import {
Card,
CardContent,
CardHeader,
CardMedia,
TextField,
Grid,
Typography,
Button,
} from "#material-ui/core";
import axios from "axios";
import { useState } from "react";
import { useHistory } from "react-router";
export default function CreateEditQuestion() {
const answerPack = new Array(6).fill(["", "", ""]);
const [question, setQuestion] = useState("");
const [answers, setAnswer] = useState(answerPack);
const [TimeAllowed, setTimeAllowed] = useState(30);
const [Score, setScore] = useState(1);
const [Date, setDate] = useState("");
const history = useHistory();
const handleAnswerSet = (value, answerIndex, answerTextIndex) => {
var updatedAnswer = answers;
updatedAnswer[answerIndex][answerTextIndex] = value;
setAnswer(updatedAnswer);
};
const handleSubmit = () => {
let data = {
question,
answer_set: answers,
time_allowed: TimeAllowed,
score_value: Score,
date: Date,
};
for (const [key, value] of Object.entries({ question, Date })) {
if (value.trim().length == 0) {
alert(`${key} has not been filled in`);
return false;
}
}
axios
.post(
"https://localhost:8000/question",
data
)
.then((resp) => {
alert("Succesfully added Question");
history.push("/question");
})
.catch((err) => {
console.log(err);
});
};
return (
<>
<h1>Create Question</h1>
<Card elevation={1}>
<CardHeader></CardHeader>
<CardContent>
<div>
<TextField
fullWidth
label="Question"
variant="outlined"
onChange={(e) => {
setQuestion(e.target.value);
}}
></TextField>
</div>
<Grid container direction={"row"} spacing={4}>
{answers.map((answerTexts, i) => {
return (
<Grid key={i} item md={4} width={50}>
{answerTexts.map((text, j) => {
return (
<div style={{ width: "70%", marginTop: "30px" }}>
<TextField
fullWidth
label={`Answer ${i + 1} - ${j + 1}`}
onChange={(ev) => {
handleAnswerSet(ev.target.value, i, j);
}}
variant="outlined"
/>
<br />
</div>
);
})}
</Grid>
);
})}
</Grid>
<Grid container direction={"row"} spacing={4}>
<Grid item md={5} width={80}>
<Typography variant="h6" gutterBottom gutterTop>
Extra Options
</Typography>
<TextField
label={"Time Allowed : "}
variant="outlined"
defaultValue={TimeAllowed}
onChange={(ev) => {
setTimeAllowed(ev.target.value);
}}
/>{" "}
<TextField
label="Score"
variant="outlined"
defaultValue={Score}
onChange={(ev) => {
setScore(ev.target.value);
}}
/>
</Grid>
<Grid item md={5} width={100}>
<Typography variant="h6" gutterBottom gutterTop>
Question Date
</Typography>
<TextField
type="date"
onChange={(ev) => {
setDate(ev.target.value);
}}
/>
</Grid>
</Grid>
<div align="center">
<Button
onClick={() => {
handleSubmit();
}}
variant="contained"
color="primary"
>
Submit Question
</Button>
</div>
</CardContent>
</Card>
</>
);
}
Problem:
On each block, changing any value also changes all the other corresponding inputs in other blocks, so if the first input on the first block is changed, then all other first inputs in the other blocks also get changed,e.g(changing answer 1-1 also changes 2-1,3-1,4-1, etc). I could not trace why. Only the corresponding values should be changed
This is the function responsible for setting the values.
const handleAnswerSet = (value, answerIndex, answerTextIndex) => {
var updatedAnswer = answers;
updatedAnswer[answerIndex][answerTextIndex] = value;
setAnswer(updatedAnswer);
};
The state of a value set using React useState hook gets set to the proper value and then reset to null. Critical code below. The click event that sets the startDate to the current date and time is 3 components down from where startDate is initialized. When setStartDate did not work I created an arrow function, updateStartDate. Both had the same problem where the startDate was changed after the click event (witnessed per the console.log in the top component), but was null just before the next click event (per the console.log in the click event). This is not an async problem as I see the change made before subsequent click.
If this is something that just does not work please explain. I could probably fix with useReducer but prefer to keep the useState if there is something I can do to correct this... If not correctable then I would like to at least understand why it does not work so that I can avoid this problem in the future.
export const DisplayTicTacToeContainer = (props) => {
const [startDate, setStartDate]= useState();
const updateStartDate = (newDate) => {
setStartDate(newDate);
}
useEffect (() => {
setStartDate(null);
}, []);
useEffect(() => {
console.log( "displayTicTacToeContainer useEffect for change of startDate = ", startDate)
}, [startDate]);
return (
<DisplayTicTacToeMatch arrayOfMatchingItems ={arrayOfMatchingItems}
startDate={startDate}
setStartDate={setStartDate}
updateStartDate={updateStartDate}
/>);
}
//-----------------------------------------------
export const DisplayTicTacToeMatch = (props) => {
const { startDate,
setStartDate,
updateStartDate,
} = props;
useEffect(() => {
// Performs some prep and working fine.
}, []);
return (
<TicTacToe
startDate={startDate}
setStartDate={setStartDate}
updateStartDate={updateStartDate}
/>
);
}
//-----------------------------------------------
const TicTacToeContainer = (props) => {
const { startDate,
setStartDate,
updateStartDate,
} = props;
const [board, setBoard] = useState(<Board
updateStartDate={updateStartDate}
startDate={startDate}
setStartDate={setStartDate}/>);
return (
<Board/>
)
}
export default TicTacToeContainer;
I renamed the component to BoardComponent and the state variable to boardLayout. I included the full return portion of the BoardComponent below.
As I am still experiencing the problem I would agree with you that, "DisplayTicTacToeContainer is being mounted twice". Any thoughts on how I can avoid this from happening?
Other than this inability to setStartDate, everything is working fine.
//-----------------------------------------------
const Board = (props) => {
const { updateStartDate,
startDate,
setStartDate,
} = props;
return (
<>
<Grid container maxwidth="lg" alignItems="center" spacing={1}>
<Grid item xs={9}>
<Grid container alignItems="center">
<Grid item xs={9}>
<Typography variant = "body1">
First select a square. Once the "Inquiry" word or phrase appears below, find
the correct response in the column on the right and select that buttton. A correct
response will fill the square previously selected with an "O" or "X".
</Typography>
<div style={{ width: '100%' }}>
<Box
display="flex"
flexWrap="wrap"
p={1}
m={1}
bgcolor="background.paper"
css={{ maxWidth: 900 }}
>
<Box p={1} bgcolor="grey.300">
Inquiry : {inquiry}
</Box>
</Box>
<Box
display="flex"
flexWrap="wrap"
p={1}
m={1}
bgcolor="background.paper"
css={{ maxWidth: 900 }}
>
<Box p={1} bgcolor="grey.300">
Next move by : {currentPlayer}
</Box>
<Box p={1} bgcolor="grey.300">
{showStatus}
</Box>
</Box>
</div>
</Grid>
</Grid>
<MyAux>
{boardLayout.map((row, rowId) => {
const columns = row.map((column, columnId) => (
<Grid key={columnId} item>
<ButtonBase >
<Paper
onClick={(e) => {
clickSquareHandler(e);
}}
elevation={4}
data-coord={rowId + ':' + columnId}
id={"Square" + rowId.toString() + columnId.toString()}
className={classes.Paper}>
<Icon
className={classes.Icon}
style={{fontSize: 78}}>
</Icon>
</Paper>
</ButtonBase>
</Grid>
));
return (
<Grid
key={rowId}
className={classes.Grid}
container
spacing={2}>
{columns}
</Grid>)
})}
</MyAux>
</Grid>
<Grid item xs={3} >
<Paper className={classes.paper}>
<Typography variant = "body1">
Response Options
</Typography>
<ButtonGroup
orientation="vertical"
color="secondary"
aria-label="vertical outlined secondary button group"
>
{responseChoices.map((choice) => (
<Controls.Button
key ={choice.value}
text={choice.value}
variant="contained"
color = "secondary"
onClick={() => {
chooseChecker(choice);
}}
className={
response && response.value === choice.value ? "selected" : ""
}
disabled={!!selected[choice.value]}
fullWidth = "true"
size = "small"
/>
))}
</ButtonGroup>
</Paper>
</Grid>
</Grid>
</>
)
}
BoardContainer.propTypes = {
won: PropTypes.func,
size: PropTypes.number
};
export default BoardContainer;
At least, code below doesn't make much sense.
Please don't set state value as a component.
Also, try to name state variable different from components, since it will confuse you at some ppint.
const [board, setBoard] = useState(<Board
updateStartDate={updateStartDate}
startDate={startDate}
setStartDate={setStartDate}/>);
return (
<Board/>
)
Another possibility is that the DisplayTicTacToeContainer is being mounted twice, but I can't confirm it with the code provided.