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) => (
// ...
);
Related
My app has two buttons, one that displays information and another that hides it. This works as intended, however, some of the images loaded in from the API don't have any data available, and when the button to display the info is pressed, it returns a []
How would I target those that load with an empty array, and add in a string. For example "no information for this breed is available?"
App.js
import './App.css';
import './Dog.js';
import './index.css';
import "./Grid.js";
import NestedGrid from './Grid.js';
import Album from './Grid.js';
import AppLayout from './Grid.js';
function DogApp() {
return (
<div className="dogApp">
<AppLayout />
</div>
);
}
export default DogApp;
Grid.js
import * as React from 'react';
import AppBar from '#mui/material/AppBar';
import Button from '#mui/material/Button';
import Card from '#mui/material/Card';
import CardActions from '#mui/material/CardActions';
import CardContent from '#mui/material/CardContent';
import CardMedia from '#mui/material/CardMedia';
import CssBaseline from '#mui/material/CssBaseline';
import Grid from '#mui/material/Grid';
import Stack from '#mui/material/Stack';
import Box from '#mui/material/Box';
import Toolbar from '#mui/material/Toolbar';
import Typography from '#mui/material/Typography';
import Container from '#mui/material/Container';
import Link from '#mui/material/Link';
import { createTheme, ThemeProvider } from '#mui/material/styles';
import FetchAPI from './FetchAPI';
const cards = [1, 2, 3, 4, 5, 6, 7, 8, 9];
const theme = createTheme();
export default function AppLayout() {
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<AppBar position="relative">
</AppBar>
<main>
{/* Hero unit */}
<Box
sx={{
bgcolor: 'background.paper',
pt: 8,
pb: 6,
}}
>
<Container maxWidth="sm">
<Stack
sx={{ pt: 4 }}
direction="row"
spacing={2}
justifyContent="center"
>
</Stack>
</Container>
</Box>
<Container sx={{ py: 8 }} maxWidth="md">
{/* End hero unit */}
<Grid container spacing={4}>
{cards.map((card) => (
<Grid item key={card} xs={12} sm={6} md={4}>
<Card
sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}
>
<FetchAPI />
<CardContent sx={{ flexGrow: 1 }}>
</CardContent>
</Card>
</Grid>
))}
</Grid>
</Container>
</main>
</ThemeProvider>
);
}
FetchAPI.js
import React, { useState, useEffect } from 'react'
const FetchAPI = () => {
const [show, setShow] = useState({});
const [data, setData] = useState([]);
const apiGet = () => {
const API_KEY = "";
fetch(`https://api.thedogapi.com/v1/images/search?limit=2&page=10&order=Desc?API_KEY=${API_KEY}`)
.then((response) => response.json())
.then((json) => {
console.log(json);
setData([...data, ...json]);
});
};
useEffect(() => { //call data when pagee refreshes/initially loads
apiGet();
}, []);
return (
<div>
{data.map((item, id) => (
<div>
<img alt="dog photos" className="dogImg" src={item.url}></img>
{show[id] === false ? <p>{JSON.stringify(item.breeds, null, '\t')}</p> : null}
<button onClick={() => setShow((prev) => ({ ...prev, [id]: false }))}>Learn more about this dog!</button>
<button onClick={() => setShow((prev) => ({ ...prev, [id]: true }))}>Hide information</button>
</div>
))}
</div>
)
}
export default FetchAPI;
You can compare the length of array like:
if(arr.length===0){
return "no information for this breed is available"
}
The above condition is just for understanding you can change the logic as well either you want to return or save in some state.
I have data returned from the backend now i am trying to map it to react component which is throwing an error Cannot read property of null what is correct way to print keys in the div or card , i have also attached the data
index.js
import React, { Component } from 'react';
import './app.css';
import ReactImage from './react.png';
import AppBar from '#mui/material/AppBar';
import Box from '#mui/material/Box';
import Toolbar from '#mui/material/Toolbar';
import Typography from '#mui/material/Typography';
import IconButton from '#mui/material/IconButton';
import MenuIcon from '#mui/icons-material/Menu';
import Paper from '#mui/material/Paper';
import Grid from '#mui/material/Grid';
import { styled } from '#mui/material/styles';
export default class JOB_DESC extends Component {
state = { data: null };
async componentDidMount() {
try {
const response = await fetch('/api/getUsername');
const data = await response.json();
this.setState({ data: data });
} catch (e) {
if (e.name != "AbortError") this.setState({ error: e.message });
}
}
render() {
const data = this.state.data;
console.log("DATA", data);
return (
<Box sx={{ flexGrow: 1 }}>
<AppBar position="static">
<Toolbar>
<IconButton
size="large"
edge="start"
color="inherit"
aria-label="menu"
sx={{ mr: 2 }} >
<MenuIcon />
</IconButton>
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
Job.com
</Typography>
</Toolbar>
</AppBar>
<div style={{ padding: 20 }}>
<Grid container spacing={4}>
<Grid item xs={12}>
<p>`{data["User Account"]}`</p>
<p>`{data["User Location"]}`</p>
<p> `{data["Company Name"]}`</p>
</Grid>
</Grid>
</div>
</Box>
);
}
}
data
{ ‘User Account': Admin’,
‘User Location': ': New York, NY',
'Company Name': ': Millennium’ }
It looks like by the time your component gets rendered, data will still be null as you've set it to null. So attempting to index null will give you Cannot read property of null.
What you'd want to do is wrap your component with a condition to check if data exists by the time it renders.
{ data && (
<div style={{ padding: 20 }}>
<Grid container spacing={4}>
<Grid item xs={12}>
<p>`{data["User Account"]}`</p>
<p>`{data["User Location"]}`</p>
<p> `{data["Company Name"]}`</p>
</Grid>
</Grid>
</div>
)}
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);
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)
}