I'm a complete beginner in React and I was pretty happy with my first app since I, maybe, have a memory leak ? My app is very laggy after entering value in the input, after click on the add button, at bottom right, and I suspect that I don't use 'useState' like I should ? I dunno, I've been searching for hours :( ...
Here's the app : https://n3g.gitlab.io/react-conso-energie/
Here's the code :
In the App.js (parent) :
import React, { useState } from 'react'
import firebase from './firebase'
import AddForm from './AddForm'
import ListingTable from './ListingExpansionPanels'
import moment from 'moment'
// MATERIAL UI
import { CssBaseline } from '#material-ui/core'
import Grid from '#material-ui/core/Grid'
import Snackbar from '#material-ui/core/Snackbar'
import MuiAlert from '#material-ui/lab/Alert'
import './styles.css'
export default function App () {
// BDD
const dbRef = firebase.database().ref('items')
// LISTING DATA
const [listingData, setListingData] = useState([{}])
dbRef.once('value', (snapshot) => {
const releves = snapshot.val()
const listingData = []
for (const [key, releve] of Object.entries(releves)) {
listingData.push({
key: key,
month: releve.month,
gaz: releve.gaz,
electricite: releve.electricite,
total: releve.total,
submissionDate: releve.submissionDate
})
}
const listingDataSorted = listingData.sort((a, b) => (a.submissionDate > b.submissionDate) ? 1 : -1)
setListingData(listingDataSorted)
})
const lastItemIndex = listingData.length - 1
// MONTHS
const [selectedDate, handleDateChange] = useState(new Date())
const dateFormat = moment(selectedDate).format('MMMM')
// ELECTRICITÉ
const constElec = { prix: 0.15356, abo: 117.56 }
const [kw, setKw] = useState('')
const diffElec = kw - listingData[lastItemIndex].electricite
const resultatElec = Math.round((constElec.prix * diffElec + (constElec.abo / 12)) * 1e2) / 1e2
// GAZ
const constGaz = { prix: 0.0681, abo: 215.73, indice: 11.34 }
const [m3, setm3] = useState('')
const diffGaz = m3 - listingData[lastItemIndex].gaz
const resultatGaz = Math.round((((constGaz.indice * diffGaz) * constGaz.prix) + (constGaz.abo / 12)) * 1e2) / 1e2
// TOTAL
const total = Math.round((resultatElec + resultatGaz) * 1e2) / 1e2
// SUBMIT
const handleSubmit = () => {
const releve = {
submissionDate: moment(selectedDate).unix(),
month: dateFormat,
gaz: m3,
electricite: kw,
total: total
}
dbRef.push(releve)
setOpenSnack(true)
setm3('')
setKw('')
}
// DELETE
const handleDelete = key => {
dbRef.child(key).remove()
}
// SNACKBAR
function Alert (props) {
return <MuiAlert elevation={6} variant='filled' {...props} />
}
const [openSnack, setOpenSnack] = React.useState(false)
const handleClose = (event, reason) => {
if (reason === 'clickaway') {
return
}
setOpenSnack(false)
}
return (
<>
<CssBaseline />
<div className='App'>
<Grid container justify='center' spacing={2}>
<Grid item xs={12}>
<h1>Conso Energie</h1>
<AddForm m3={m3} setm3={setm3} kw={kw} setKw={setKw} selectedDate={selectedDate} handleDateChange={handleDateChange} handleSubmit={handleSubmit} />
</Grid>
<Grid item xs={12}>
<ListingTable listingData={listingData} handleDelete={handleDelete} />
</Grid>
</Grid>
<Snackbar open={openSnack} autoHideDuration={3500} onClose={handleClose}>
<Alert onClose={handleClose} severity='success'>
Sauvegardé
</Alert>
</Snackbar>
</div>
</>
)
}
In the AddForm.js (child - I'm using Material UI) :
import React from 'react'
import TextField from '#material-ui/core/TextField'
import InputAdornment from '#material-ui/core/InputAdornment'
import Button from '#material-ui/core/Button'
import CloudUploadIcon from '#material-ui/icons/CloudUpload'
import { MuiPickersUtilsProvider, DatePicker } from '#material-ui/pickers'
import MomentUtils from '#date-io/moment'
import moment from 'moment'
import 'moment/locale/fr'
import Dialog from '#material-ui/core/Dialog'
import DialogActions from '#material-ui/core/DialogActions'
import DialogContent from '#material-ui/core/DialogContent'
import DialogTitle from '#material-ui/core/DialogTitle'
import Fab from '#material-ui/core/Fab'
import AddIcon from '#material-ui/icons/Add'
export default function AddForm ({ m3, setm3, kw, setKw, handleSubmit, selectedDate, handleDateChange }) {
const handleUpdateGaz = function (event) {
setm3(Number(event.target.value))
}
const handleUpdateKw = function (event) {
setKw(Number(event.target.value))
}
moment.locale('fr')
const [open, setOpen] = React.useState(false)
const handleClickOpenAddDialog = () => {
setOpen(true)
}
const handleCloseAddDialog = () => {
setOpen(false)
}
return (
<>
<div>
<Fab className='fab-btn-add' color='primary' aria-label='add' onClick={handleClickOpenAddDialog}>
<AddIcon />
</Fab>
<Dialog
open={open}
onClose={handleCloseAddDialog}
aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description'
>
<DialogTitle id='alert-dialog-title'>Entrer les valeurs</DialogTitle>
<DialogContent>
<form className='addform' noValidate autoComplete='off'>
<MuiPickersUtilsProvider utils={MomentUtils}>
<DatePicker
inputVariant='outlined'
value={selectedDate}
onChange={handleDateChange}
label='Mois'
format='MMMM Y'
views={['month']}
minDate={new Date('2020-01-01')}
maxDate={new Date('2020-12-31')}
/>
</MuiPickersUtilsProvider>
<TextField
label='Electricité'
variant='outlined'
type='number'
InputProps={{
endAdornment: <InputAdornment position='end'>kW</InputAdornment>
}}
value={kw}
onChange={handleUpdateKw}
/>
<TextField
label='Gaz'
variant='outlined'
type='number'
InputProps={{
endAdornment: <InputAdornment position='end'>m3</InputAdornment>
}}
value={m3}
onChange={handleUpdateGaz}
/>
<Button
className='btn-submit'
size='large'
variant='contained'
color='primary'
startIcon={<CloudUploadIcon />}
onClick={handleSubmit}
>
Confirmer
</Button>
</form>
</DialogContent>
<DialogActions>
<Button onClick={handleCloseAddDialog} color='primary' autoFocus>
Retour
</Button>
</DialogActions>
</Dialog>
</div>
</>
)
}
Pretty sure it's unrelated but here's the ListingExpansionPanel.js
import React from 'react'
import ExpansionPanel from '#material-ui/core/ExpansionPanel'
import ExpansionPanelSummary from '#material-ui/core/ExpansionPanelSummary'
import ExpansionPanelDetails from '#material-ui/core/ExpansionPanelDetails'
import Typography from '#material-ui/core/Typography'
import ExpandMoreIcon from '#material-ui/icons/ExpandMore'
import Icon from '#material-ui/core/Icon'
import Grid from '#material-ui/core/Grid'
import Button from '#material-ui/core/Button'
import Chip from '#material-ui/core/Chip'
import SwipeableViews from 'react-swipeable-views'
export default function ListingTable ({ listingData, handleDelete }) {
const dataRow = listingData.map((data, key) => (
<ExpansionPanel key={key} className='relevePanel'>
<ExpansionPanelSummary
expandIcon={<ExpandMoreIcon />}
aria-controls={key}
id={key}
>
<Typography>
<span>
<Icon style={{ marginRight: 15 }}>calendar_today</Icon>
{data.month}<b>{data.total ? ' : ' + data.total + ' €' : ''}</b>
</span>
</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<SwipeableViews>
<Typography className='expansion-detail'>
<Grid container>
<Grid item xs={12}>
<b style={{ color: '#cacaca' }}>2020</b>
</Grid>
<Grid item xs>
<Icon>power</Icon>
<span>{data.electricite} <small>Kwh</small></span>
<div>
<Chip className='plus' size='small' label='+3,4%' />
</div>
</Grid>
<Grid item xs>
<Icon>whatshot</Icon>
<span>{data.gaz} <small>m<sup>3</sup></small></span>
<div>
<Chip className='moins' size='small' label='-5,2%' />
</div>
</Grid>
<Grid item xs={12}>
<Button
className='btnRemove'
style={{ marginTop: 15 }}
size='small'
color='secondary'
onClick={() => handleDelete(data.key)}
>
Supprimer
</Button>
</Grid>
</Grid>
</Typography>
<Typography className='expansion-detail'>
<Grid container>
<Grid item xs={12}>
<b style={{ color: '#cacaca' }}>2019</b>
</Grid>
<Grid item xs>
<Icon>power</Icon>
<span>{data.electricite} <small>Kwh</small></span>
<div>
<Chip className='plus' size='small' label='+3,4%' />
</div>
</Grid>
<Grid item xs>
<Icon>whatshot</Icon>
<span>{data.gaz} <small>m<sup>3</sup></small></span>
<div>
<Chip className='moins' size='small' label='-5,2%' />
</div>
</Grid>
<Grid item xs={12}>
<Button
className='btnRemove'
style={{ marginTop: 15 }}
size='small'
color='secondary'
onClick={() => handleDelete(data.key)}
>
Supprimer
</Button>
</Grid>
</Grid>
</Typography>
</SwipeableViews>
</ExpansionPanelDetails>
</ExpansionPanel>
))
return (
<>
{dataRow}
</>
)
}
Thank you A LOT for your help, if someone see something.
I continue to search..
Thanks.
Update : Added the whole code for this 2 files
Update 2 : When I disable the firebase link, it's ok ! So I'll investigate this way..
This won't solve your problem but one thing that will use less memory is not creating an anonymous function, but replacing it with a reference in child components.
In the AddForm.js :
// pass the reference to changeKw instead of creating an anonymous function:
<TextField
value={kw}
onChange={changeKw} // e will be passed automatically
/>
In the App.js (parent) :
const changeKw = e => {
console.log(e.target.value) // e gets passed
setKw(e.target.value)
}
Related
I have a two div I want to send selected text to another div using onSelect event? Right now entire para sending to another div but I want to send just selected text. How can I do this?
Demo:- https://codesandbox.io/s/selected-text-send-to-another-div-using-onselect-0ccnrn?file=/src/App.js
My Code:-
import React from "react";
import { Box, Grid, TextField, Typography } from "#material-ui/core";
import { useState } from "react";
const SendSelectedText = () => {
const [label1, setlabel1]=useState('');
const [para, setPara]=useState('This is Old Para');
const handleClick = () => {
setlabel1(para);
};
return (
<>
<Box className="sendSelectedTextPage">
<Grid container spacing={3}>
<Grid item xl={6} lg={6} md={6}>
<textarea onSelect={handleClick}>{para}</textarea>
</Grid>
<Grid item xl={6} lg={6} md={6}>
<TextField
variant="outlined"
size="small"
label="Label One"
value={label1}
multiline
rows={3}
className="sendSelectedTextPageInput"
/>
</Grid>
</Grid>
</Box>
</>
);
};
export default SendSelectedText;
Thanks for your support!
All you need is use window.getSelection().
Here is solution
import React from "react";
import { Box, Grid, TextField, Typography } from "#material-ui/core";
import { useState } from "react";
const SendSelectedText = () => {
const [label1, setlabel1] = useState("");
const [para, setPara] = useState("This is Old Para");
const handleMouseUp = () => {
setlabel1(window.getSelection().toString()); // setting up label1 value
};
return (
<>
<Box className="sendSelectedTextPage">
<Grid container spacing={3}>
<Grid item xl={6} lg={6} md={6}>
// changed event to onMouseUp
<textarea onMouseUp={handleMouseUp}>{para}</textarea>
</Grid>
<Grid item xl={6} lg={6} md={6}>
<TextField
variant="outlined"
size="small"
label="Label One"
value={label1}
multiline
rows={3}
className="sendSelectedTextPageInput"
/>
</Grid>
</Grid>
</Box>
</>
);
};
export default SendSelectedText;
Sandboxlink
You have to use the selection
const handleClick = (e) => {
setlabel1(
e.target.value.substring(e.target.selectionStart, e.target.selectionEnd)
);
};
or
const handleClick = (e) => {
setlabel1(
para.substring(e.target.selectionStart, e.target.selectionEnd)
);
};
Based on this
sandbox
I have a component, GameGrid which makes an axios call to get the data that I pass as props to another component, GameCard. I currently have a Header component that is in the same file as GameGrid. The header has buttons which when clicked, changes the endpoint that the GameGrid is calling. I am trying to figure out a way to separate the two components, Header and GameGrid, but still have the buttons on the header determine which endpoint the GameGrid should call. Also, is the GameGrid the right place to have the data being called?
GameGrid.jsx
import React, { useEffect, useState } from "react";
import AppBar from '#mui/material/AppBar';
import Toolbar from '#mui/material/Toolbar';
import Box from '#mui/material/Box';
import IconButton from '#mui/material/IconButton';
import Grid from '#mui/material/Grid';
import axios from "axios";
import GameCard from "./GameCard";
export default function GameGrid() {
const [data, setData] = useState(); // Data Hook
const [route, setRoute] = useState('/')
useEffect(() => {
const getData = async () => {
try {
const { data } = await axios.get(`${process.env.REACT_APP_URL}${route}`);
console.log(data);
setData(data);
} catch (err) {
console.log(err);
}
};
getData();
}, [route]);
const createCards = (data) => {
return <GameCard
key = {data.id}
id = {data.id}
url = {data.game_url}
thumbnail = {data.thumbnail}
title = {data.title}
platform = {data.platform}
description = {data.short_description}
genre = {data.genre}
publisher = {data.publisher}
developer = {data.developer}
/>;
}
const Header = () => {
return (
<Box sx ={{ flexGrow: 1 }}>
<AppBar position="fixed">
<Toolbar>
<Grid sx={{ flexGrow: 1,
border: '1px solid white'}} container spacing={2}>
<Grid sx={{
}}>
<IconButton onClick={() => setRoute('/')}>
<img src='computer-mouse.png' alt='Logo' id='icon'/>
</IconButton>
<IconButton onClick={() => setRoute('/shooter')}>
<img src="shooter.png" alt="Shooter Games" />
</IconButton>
</Grid>
<Grid>
</Grid>
</Grid>
</Toolbar>
</AppBar>
</Box>
)
}
return (
<>
<Header/>
<div className="card-container">
{data?.map(createCards)}
</div>
</>
)
}
GameCard.jsx
import React from "react";
import Card from "#mui/material/Card";
import CardContent from "#mui/material/CardContent";
import CardMedia from "#mui/material/CardMedia";
import Typography from "#mui/material/Typography";
import { CardActionArea, Chip } from "#mui/material";
import Stack from '#mui/material/Stack';
export default function GameCard(props) {
return (
<Card sx={{ width: 550, margin: 2}} key={props.id} id={props.id} >
<CardActionArea href={props.url}>
<CardMedia
component="img"
height="fit-content"
image={props?.thumbnail}
/>
<CardContent>
<Stack direction="row" justifyContent="space-between">
<Typography gutterBottom variant="h6" component="div">
{props.title}
</Typography>
<Chip label={props.platform} size="small"/>
</Stack>
<Typography gutterBottom variant="body1" id="description-text"
color="text.secondary"
sx={{
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap'
}}>
{props.description}
</Typography>
<Stack direction="row" justifyContent="space-between">
<Chip label={props.genre} size="small" />
<Chip label={props.publisher} size="small" />
<Chip label={props.developer} size="small" />
</Stack>
</CardContent>
</CardActionArea>
</Card>
);
}
To separate Header, simply define it with all the props it requires. In this instance, that appears to be the setRoute function though you can call it whatever you like.
For example
// Header.jsx
const Header = ({ onRouteClick }) => (
<Box sx={{ flexGrow: 1 }}>
{/* ... */}
<IconButton onClick={() => onRouteClick("/")}>
<img src="computer-mouse.png" alt="Logo" id="icon" />
</IconButton>
<IconButton onClick={() => onRouteClick("/shooter")}>
<img src="shooter.png" alt="Shooter Games" />
</IconButton>
{/* ... */}
</Box>
);
export default Header;
and in your GameGrid component, import Header from "./Header" and use...
<Header onRouteClick={setRoute} />
If you were using Typescript (highly recommended), the API for Header would look something like this
interface HeaderProps {
onRouteClick: (route: string) => void;
}
const Header = ({ onRouteClick }: HeaderProps) => (
// ...
);
I have an videos array, which in turn has objects of type Video (typing below).
I need that when clicking on the button corresponding to a specific video, I can open only one modal with the information of the clicked video.
interface VideosInfo {
id: number;
title: string;
url: string;
quiz: boolean;
}
interface PagePros {
videos: VideosInfo[]
}
Below is the component that renders the array of videos through a map, notice that inside the map, I have an onClick function that calls the modal.
import { VideoModal } from '../index';
import { useVideos } from '../../../../hooks/Videos';
export const Videos: React.FC<VideoProps> = ({ module_id }) => {
const [modalOpen, setModalOpen] = useState<boolean>(false);
const { getVideos, videos, loadingVideos } = useVideos();
const handleCloseModal = () => {
setModalOpen(false);
};
const VideosData = () => {
if (videos.length) {
return (
<List dense>
{videos?.map(video => (
<div key={video.id}>
<ListItem onClick={() => setModalOpen(true)} button>
<ListItemText primary={video.title} />
</ListItem>
<Divider />
<VideoModal
open={modalOpen}
handleClose={() => handleCloseModal()}
video={video}
video_id={video.id}
/>
</div>
))}
</List>
);
}
if (!videos.length && !loadingVideos) {
return (
<Typography variant="body1">
Não existem vídeos cadastrados neste módulo.
</Typography>
);
}
return <LoadingScreen text="Carregando vídeos..." />;
};
useEffect(() => {
getVideos(module_id);
}, [module_id, getVideos]);
return (
<Grid container spacing={2}>
<Grid item xs={12} md={12}>
<VideosData />
</Grid>
<Grid item xs={12} md={12}>
<Button variant="text" color="primary">
Novo Vídeo
</Button>
</Grid>
</Grid>
);
};
And below the VideoModal component:
export const VideoModal: React.FC<ModalProps> = ({
video,
open,
handleClose,
video_id,
}) => {
console.log('videos modal', video);
return (
<Dialog
open={open}
aria-labelledby="form-dialog-title"
onClose={handleClose}
>
<DialogTitle id="form-dialog-title">Subscribe</DialogTitle>
<DialogContent>
<h2>test</h2>
</DialogContent>
</Dialog>
);
};
I understand that the modal uses the "open" property to define whether it is open or not, but when I click the button and perform the setModalOpen, it renders a modal for each object in the array. I don't understand how I could assemble this correctly.
I solved it as follows, created a state called videoToModal of type VideosInfo and a function called handleModalOpen, passed the video parameter to the function, and in the function stored this video in the videoToModal state.
I instantiated the VideoModal component outside the map (obviously should have done this before) and passed the state to the VideoModal component's video parameter.
Below is the complete code for the component.
import React, { useEffect, useState } from 'react';
import {
Button,
Divider,
Grid,
IconButton,
List,
ListItem,
ListItemSecondaryAction,
ListItemText,
Tooltip,
Typography,
} from '#material-ui/core';
import { Delete, QuestionAnswer } from '#material-ui/icons';
import { useVideos } from '../../../../hooks/Videos';
import { useStyles } from './styles';
import { LoadingScreen } from '../../../../components/CustomizedComponents';
import { VideoModal } from '../index';
import { VideosInfo } from '../../../../hooks/Videos/types';
import { VideoProps } from './types';
export const Videos: React.FC<VideoProps> = ({ module_id }) => {
const [openModal, setOpenModal] = useState<boolean>(false);
const [videoToModal, setVideoToModal] = useState<VideosInfo>();
const classes = useStyles();
const { getVideos, videos, loadingVideos } = useVideos();
const handleCloseModal = () => {
setOpenModal(false);
};
const handleOpenModal = (video: VideosInfo) => {
setVideoToModal(video);
setOpenModal(true);
};
const VideosData = () => {
if (videos.length) {
return (
<List dense>
{videos?.map(video => (
<div key={video.id}>
<ListItem
className={classes.listItem}
onClick={() => handleOpenModal(video)}
button
>
<ListItemText
primary={video.title}
className={classes.listItemText}
/>
<ListItemSecondaryAction>
<Tooltip
placement="top"
title={
video.Quizzes?.length
? 'Clique para ver as perguntas'
: 'Clique para iniciar o cadastro de perguntas'
}
>
<IconButton edge="end" aria-label="delete">
<QuestionAnswer
color={video.Quizzes?.length ? 'primary' : 'action'}
/>
</IconButton>
</Tooltip>
<Tooltip placement="top" title="Deletar Vídeo">
<IconButton edge="end" aria-label="delete">
<Delete color="secondary" />
</IconButton>
</Tooltip>
</ListItemSecondaryAction>
</ListItem>
<Divider />
</div>
))}
<VideoModal
open={openModal}
handleClose={() => handleCloseModal()}
video={videoToModal}
/>
</List>
);
}
if (!videos.length && !loadingVideos) {
return (
<Typography variant="body1">
Não existem vídeos cadastrados neste módulo.
</Typography>
);
}
return <LoadingScreen text="Carregando vídeos..." />;
};
useEffect(() => {
getVideos(module_id);
}, [module_id, getVideos]);
return (
<Grid container spacing={2} className={classes.container}>
<Grid item xs={12} md={12}>
<VideosData />
</Grid>
<Grid item xs={12} md={12}>
<Button variant="text" color="primary">
Novo Vídeo
</Button>
</Grid>
</Grid>
);
};
Instead of using
<div key={video.id}>
can you use,
<List dense>
{videos?.map((video,i) => (
<div key={i}>
<ListItem onClick={() => setModalOpen(true)} button>
<ListItemText primary={video.title} />
</ListItem>
<Divider />
<VideoModal
open={modalOpen}
handleClose={() => handleCloseModal()}
video={video}
video_id={video.id}
/>
</div>
))}
</List>
Problem is in this code:
import React from 'react';
import { Card, CardActions, CardContent, CardMedia, Button, Typography, ButtonBase } from '#material-ui/core/';
import DeleteIcon from '#material-ui/icons/Delete';
import MoreHorizIcon from '#material-ui/icons/MoreHoriz';
import { useDispatch } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { deleteBook } from '../../../../actions/books';
import useStyles from "./styles";
const Book = (book, setCurrentId) => {
const dispatch = useDispatch();
const classes = useStyles();
const user = JSON.parse(localStorage.getItem('profile'));
const history = useHistory();
console.log(book);
const openBook = (e) => {
history.push(`/books/${book._id}`);
};
return (
<Card className={classes.card} raised elevation={6}>
<ButtonBase
component="span"
name="test"
className={classes.cardAction}
onClick={openBook}
>
<CardMedia className={classes.media} image={book.slika || 'https://user-images.githubusercontent.com/194400/49531010-48dad180-f8b1-11e8-8d89-1e61320e1d82.png'} title={book.ime_knjige} />
{(user!= null && user.result.admin_stranice === true) && (
<div className={classes.overlay2} name="edit">
<Button
onClick={(e) => {
e.stopPropagation();
setCurrentId(book._id);
}}
style={{ color: 'white' }}
size="small"
>
<MoreHorizIcon fontSize="default" />
</Button>
</div>
)}
<Typography className={classes.title} gutterBottom variant="h5" component="h2">{book.ime_knjige}</Typography>
<Typography className={classes.title} gutterBottom variant="h5" component="h2">{book.autor_knjige}</Typography>
<CardContent>
<Typography variant="body2" color="textSecondary" component="p">{book.opis_knjige && book.opis_knjige.split(' ').splice(0, 20).join(' ')}...</Typography>
<Typography variant="body2" color="textSecondary" component="p> {book.rejting}</Typography>
</CardContent>
<div className={classes.details}>
<Typography variant="body2" color="textSecondary" component="h2 {book.zanr && book.zanr.map((zanr) => `#${zanr} `)}</Typography>
</div>
</ButtonBase>
<CardActions className={classes.cardActions}>
{(user && user.result.admin_stranice === true) && (
<Button size="small" color="secondary" onClick={() => dispatch(deleteBook(book._id))}>
<DeleteIcon fontSize="small" /> Delete
</Button>
)}
</CardActions>
</Card>
);
};
export default Book;
The object "book" i pass here is defined and have all the atributes needed and their values. I used console.log to see object in console and it's there, with everything it should have. But when i try to access any of atributes the JavaScript sees them as undefined for some reason. Some idea why it does that?
I'm trying to learn react I keep encountering this error while rendering the post dialog box in the website.
comments.js file:
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import withStyles from '#material-ui/core/styles/withStyles';
import { Link } from 'react-router-dom';
import dayjs from 'dayjs';
// MUI
import Grid from '#material-ui/core/Grid';
import Typography from '#material-ui/core/Typography';
const styles = (theme) => ({
...theme,
commentImage: {
maxWidth: '100%',
height: 100,
objectFit: 'cover',
borderRadius: '50%'
},
commentData: {
marginLeft: 20
}
});
class Comments extends Component {
render() {
const { comments, classes } = this.props;
return (
//Here is the error
<Grid container>
{comments.map((comment, index) => {
const { body, createdAt, userImage, userHandle } = comment;
return (
<Fragment key={createdAt}>
<Grid item sm={12}>
<Grid container>
<Grid item sm={2}>
<img
src={userImage}
alt="comment"
className={classes.commentImage}
/>
</Grid>
<Grid item sm={9}>
<div className={classes.commentData}>
<Typography
variant="h5"
component={Link}
to={`/users/${userHandle}'}
color="primary"
>
{userHandle}
</Typography>
<Typography variant="body2" color="textSecondary">
{dayjs(createdAt).format('h:mm a, MMMM DD YYYY')}
</Typography>
<hr className={classes.invisibleSeparator} />
<Typography variabnt="body1">{body}</Typography>
</div>
</Grid>
</Grid>
</Grid>
{index !== comments.length - 1 && (
<hr className={classes.visibleSeparator} />
)}
</Fragment>
);
})}
</Grid>
);
}
}
Comments.propTypes = {
comments: PropTypes.array.isRequired
};
export default withStyles(styles)(Comments);