I have a component that fetches a random person, saves it to a state variable array and after mapping through that array, it returns certain elements. My goal is to render a "social card" with an avatar and personal infos, I want to use a material UI CARD component for this. Also I'd like to get any feedback on my logic here, whether this is a good way to achieve what I have achieved. The setTimeout() is only there so I can render some loading animation. Also when I tried to return a full card component inside the array map, I could not render {item.something.something}
export default function SocialCardsfunction() {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const classes = useStyles();
const nextPerson = () => {
fetch("https://randomuser.me/api")
.then((response) => response.json())
.then((response) => {
setItems(response.results);
});
};
useEffect(() => {
fetch("https://randomuser.me/api")
.then((response) => response.json())
.then((response) => {
setTimeout(() => {
setItems(response.results);
setLoading(false);
}, 1000);
});
}, []);
if (loading) {
return <div>Loading ...</div>;
} else {
return (
<div>
{items.map((item, i) => {
return (
<div>
<h2>{item.name.first}</h2>
<img src={item.picture.large} alt='picture' key={i} />
<button onClick={() => nextPerson()}>click me</button>
</div>
);
})}
</div>
);
}
}
const useStyles = makeStyles({
root: {
minWidth: 275,
},
bullet: {
display: "inline-block",
margin: "0 2px",
transform: "scale(0.8)",
},
title: {
fontSize: 14,
},
pos: {
marginBottom: 12,
},
});
Your logic looks good but there's always something that can be improved a little further.
If you would like to store only a single user a time in your SocialCard then I would extract only one single user from the API rather than a list because the API returns an array of only one object anyway.
First, I would change the state and include status and error. With status, you can easily check in which status your component is at the moment and based on that render different things/messages in your App. With error, you can define your own error in case something goes wrong and then render an error message in your App. Also, I've re-used your fetch as it was used twice and it was redundant. This way you have a nice single function that can be used anywhere while also making sure that loading is shown while it fetches the result. I've also used MaterialUI Card component for rendering the user data. You can check how the result looks like here
import React, { useState, useEffect } from "react";
import { makeStyles } from "#material-ui/core/styles";
import Card from "#material-ui/core/Card";
import CardActionArea from "#material-ui/core/CardActionArea";
import CardActions from "#material-ui/core/CardActions";
import CardContent from "#material-ui/core/CardContent";
import CardMedia from "#material-ui/core/CardMedia";
import Button from "#material-ui/core/Button";
import Typography from "#material-ui/core/Typography";
const useStyles = makeStyles({
root: {
maxWidth: 345
}
});
function App() {
const classes = useStyles();
const [state, setState] = useState({
user: {},
status: "idle",
error: null
});
const { user, status, error } = state;
const getUser = () => {
setState((prevState) => ({
...prevState,
status: "loading"
}));
fetch("https://randomuser.me/api").then(async (res) => {
if (res.ok) {
const data = await res.json();
setState((prevState) => ({
...prevState,
user: data.results[0],
status: "processed"
}));
} else {
setState({
user: {},
status: "failed",
error: "Error message"
});
}
});
};
useEffect(() => {
getUser();
}, []);
if (status === "loading") {
return <div>Loading ...</div>;
}
if (status === "failed") {
return <div>{error}</div>;
}
if (status === "processed") {
return (
<Card className={classes.root}>
<CardActionArea>
<CardMedia
component="img"
alt="user"
height="140"
image={user.picture.large}
title="user"
/>
<CardContent>
<Typography gutterBottom variant="h5" component="h2">
{user.name.first}
</Typography>
</CardContent>
</CardActionArea>
<CardActions>
<Button size="small" color="primary" onClick={getUser}>
Show another user
</Button>
</CardActions>
</Card>
);
} else {
// Default placeholder
return <div>hi</div>;
}
}
export default App;
However, if you would like to store all the users you fetch when clicking the button, I would suggest moving the fetch and state of users into the parent component and leave SocialCard component only for rendering a single user. Then, in the parent component I would ensure that the setState would look something like this in the getUser function
setState((prevState) => ({
...prevState,
users: [...prevState.users, ...data.results], // This merges your previous user objects with new user object
status: "processed"
}));
This way, you can keep all the users in your parent component and using map you can render each user with your SocialCard component. Take a note that you would need to refactor your components further in order to make this work. I'll leave it as an exercise for you if you want to go this route.
Changes Made
Instead of repeating the same code make a function and call that function in useEffect.
Each child in a list should have a unique "key" prop.
Card from Material UI is used.(I have not focused on styling much XD)
import {
Button,
Card,
CardActions,
makeStyles,
} from "#material-ui/core";
import React, { useEffect, useState } from "react";
export default function SocialCardsfunction() {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const classes = useStyles();
const fetchPerson = () => {
fetch("https://randomuser.me/api")
.then((response) => response.json())
.then((response) => {
setTimeout(() => {
setItems(response.results);
setLoading(false);
}, 1000);
});
};
useEffect(() => {
fetchPerson();
}, []);
if (loading) {
return <div>Loading ...</div>;
} else {
return (
<div>
{items.map((item, i) => {
return (
<div key={i}>
<Card className={classes.root}>
<h2>{item.name.first}</h2>
<img
alt="img"
src={item.picture.large}
className={classes.large}
/>
<CardActions>
<Button onClick={() => fetchPerson()} size="small">Next</Button>
</CardActions>
</Card>
</div>
);
})}
</div>
);
}
}
const useStyles = makeStyles({
root: {
minWidth: 275
},
bullet: {
display: "inline-block",
margin: "0 2px",
transform: "scale(0.8)"
},
title: {
fontSize: 14
},
pos: {
marginBottom: 12
}
});
Related
I have an ECommerce project, and this project contains the operations of displaying products, creating a product, deleting a product, and displaying a specific product information.
And my problem is only in displaying the data.
Where through the App file, the data is fetched from the backend and passed to the component "Products" and then displayed on the interface.
The problem is that the data was fetched from the backend and the data coming from the backend appeared in the browser and the request succeeded, but the problem is that the data was not displayed on the interface, and the interface was completely empty.
How can I solve this problem?
App.js:
import * as React from "react";
import Navbar from "./Navbar";
import Products from "./Products";
import { getAllProducts } from "../services/ECommerceServices";
const App = () => {
console.log("Hi in App file");
// const [products, setProducts] = React.useState([]);
const getAllProductsFun = () => {
console.log("Hi I am in App file get all products");
getAllProducts().then((products) => {
// console.log(products);
// setProducts(products);
console.log("pppppppppppp: ", products);
return products;
});
};
return (
<>
<Navbar />
<Products getAllProductsFun={getAllProductsFun} />
</>
);
};
export default App;
products.js:
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 { makeStyles } from "#mui/styles";
import { Typography } from "#mui/material";
import Head from "next/head";
import useMediaQuery from "#mui/material/useMediaQuery";
import { useTheme } from "#mui/material/styles";
import Dialog from "./Dialog";
import Product from "./Product";
import { getAllProducts } from "../services/ECommerceServices";
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,
}));
const useStyles = makeStyles({
main: {
padding: "4rem ",
},
typo: {
color: "#ffc400 !impoertant",
},
// button: {
// textTransform: "none !important",
// backgroundColor: "#ffc400 !important",
// color: "white !important",
// padding: 14,
// },
});
function Products({ getAllProductsFun }) {
console.log("check: ", getAllProductsFun);
const theme = useTheme();
const breakpoint = useMediaQuery(theme.breakpoints.down("sm"));
return (
<>
<Head>
<title>Solek</title>
</Head>
<Grid
container
direction={breakpoint ? "column" : "row"}
style={{ margin: "4rem" }}
>
<Grid item xs={12} sm={9} md={9}>
<Typography
variant="h4"
gutterBottom
component="div"
// className={classes.typo}
style={{ fontWeight: 600 }}
>
Our Products
</Typography>
</Grid>
<Grid
item
xs={12}
md={3}
sm={3}
style={{
direction: "row",
justifyContent: "flex-end",
alignItems: "center",
}}
>
<Dialog />
</Grid>
</Grid>
<Box sx={{ flexGrow: 1, margin: 8 }}>
<Grid container spacing={3}>
{getAllProductsFun()?.map((product, index) => (
<Grid item xs={12} sm={6} md={3} key={index}>
<Item>
{" "}
<Product key={product.id} product={product} />;
</Item>
</Grid>
))}
</Grid>
</Box>
</>
);
}
export default Products;
EcommerceServices.js:
import axios from "axios";
// [id]
// get All Products
// export async function getAllProducts() {
// const res = await axios.get("https://fakestoreapi.com/products");
// const products = await res.data;
// console.log("products: ", products);
// return products;
// }
export async function getAllProducts() {
const res = await axios
.get("https://fakestoreapi.com/products")
.then((res) => {
const products = res.data;
console.log("products: ", products);
return products;
});
return res;
}
// get element by ID
export async function getSingleProductRequest(context) {
const id = context.params.id;
const req = await axios.get("https://fakestoreapi.com/products/" + id);
const product = await req.json();
console.log("product: ", product);
return product;
}
// get product by ID
export async function getProductsOneByOne() {
const req = await fetch("https://fakestoreapi.com/products");
const products = await req.json();
const paths = products.map((product) => {
return {
params: {
id: product.id.toString(),
},
};
});
return {
paths,
fallback: false,
};
}
// delete product
export const deleteProduct = async (id) => {
await axios
.delete(`https://fakestoreapi.com/products/${id}`)
.then((res) => {
res.json();
console.log("data: ", res.json());
})
.then((json) => {
console.log("json data: ", json);
})
.catch((err) => console.log("error: ", err));
};
The problem is that your getAllProductsFun api call is asynchronous. So you'll need to save those values and then display it rather than attempting to call it on render.
In your App.js you can create a state to store the fetched products and call the function on your app mounting like this:
const App = () => {
const [products, setProducts] = React.useState([]) // defaults to empty array
...
React.useEffect(() => {
getAllProducts().then(response => setProducts(response))
}, []) // empty deps array means it will only call once on mount
return (
<>
<Navbar />
<Products products={products} />
</>
);
}
Now you can render the results in Products like this:
function Products({ products }) {
...
return (
...
{products.map(...)}
...
)
}
We upgraded MUI from v4 to v5 and we have UI tests which started failing. Error is:
TypeError: Cannot read property 'secondary' of undefined (I added comment to which line in code this refers)
Test example:
describe('<AnonDragNDropFileUpload />', () => {
it('should render', () => {
const blob = () => {
return new File(['Test string'], 'Test file.txt');
};
const fileSet: AnonFileSet = {
originalFile: { get: blob, set: () => undefined },
compressedFile: { get: () => undefined, set: () => undefined },
};
const result = render(<AnonDragNDropFileUpload fileSet={fileSet} downloadFileAction={jest.fn()} clearFileAction={jest.fn()} />);
expect(result).toBeTruthy();
});
});
Code:
import { Paper } from '#mui/material';
import { green, red } from '#mui/material/colors';
import { lighten, Theme } from '#mui/material/styles';
import makeStyles from '#mui/styles/makeStyles';
import { JobInputFileTypeEnum } from 'app/api';
import React, { useCallback, useEffect, useState } from 'react';
import { useDropzone } from 'react-dropzone';
import { AnonFileSet } from '.';
const useDropZoneStyles = makeStyles((theme: Theme) => ({
dragndropZone: {
backgroundColor: lighten(theme.palette.secondary.light, 0.8), // <-- this line fails
width: '100%',
},
info: {
backgroundColor: green[100],
width: '100%',
},
}));
interface Props {
fileSet: AnonFileSet;
clearFileAction: (fileType?: JobInputFileTypeEnum) => void;
downloadFileAction: () => void;
}
export const AnonDragNDropFileUpload: React.FC<Props> = ({ fileSet, clearFileAction, downloadFileAction }) => {
const classes = useDropZoneStyles();
const [fileLabel, setFileLabel] = useState('');
const onDrop = useCallback(async (acceptedFiles: File[]) => {
setFileLabel(fileSet.originalFile.get()?.name ?? '');
fileSet.originalFile.set(acceptedFiles[0]);
}, []);
const { acceptedFiles, getRootProps, getInputProps } = useDropzone({ onDrop, multiple: false, accept: '.csv' });
const { ref, ...rootProps } = getRootProps();
const handleDeleteFile = () => {
acceptedFiles.splice(
acceptedFiles.findIndex((x) => x.name === fileSet.originalFile.get()?.name),
1,
);
clearFileAction();
};
useEffect(() => {
setFileLabel(fileSet.originalFile.get()?.name ?? '');
}, [fileSet.originalFile.get()]);
if (fileSet.originalFile.get())
return (
<Paper variant="outlined">
<div className="flex px-8 py-32 justify-center">
<div className="flex">
<a style={{ color: '#888888', textDecoration: 'underline', cursor: 'default' }}>{fileLabel}</a>
<p className="mx-4"> </p>
<a onClick={handleDeleteFile} style={{ color: red[600], cursor: 'pointer' }} role="link">
{'[Clear File]'}
</a>
<p className="mx-4"> </p>
{fileSet.compressedFile?.get() && (
<a onClick={downloadFileAction} style={{ color: green[600], cursor: 'pointer' }} role="link">
{'[Download File]'}
</a>
)}
</div>
</div>
</Paper>
);
return (
<Paper {...rootProps} className={classes.dragndropZone} variant="outlined">
<div className="flex px-8 py-32 justify-center">
<input {...getInputProps()} name="customerCSVFilename" placeholder="CSV File"/>
<p>{fileLabel}</p>
</div>
</Paper>
);
};
What I've tried so far:
Checked if ThemeProvider is available
Added custom theme just to the code block which fails
All other tests which are testing hooks or custom logic (like pure TypeScript) are working without any issues, but it seems that somehow using styles from MUI is not working. When I remove these lines, test is passing, so my guess it has something with MUI makeStyles.
Any ideas? Thanks for helping me out.
Try to use a mocked component, wrapped inside a ThemeProvider instance:
import theme from './path/to/your/theme'
const MockAnonDragNDropFileUpload = (props: any) => {
return (
<ThemeProvider theme={theme}>
<AnonDragNDropFileUpload {...props} />
</ThemeProvider>
);
}
To mock the component using the existing theme you could separate its declaration into a distinct file:
const theme = createTheme({
...
});
export default theme;
Then use the mocked instance in the tests:
describe('<AnonDragNDropFileUpload />', () => {
it('should render', () => {
...
const result = render(
<MockAnonDragNDropFileUpload
fileSet={fileSet}
downloadFileAction={jest.fn()}
clearFileAction={jest.fn()}
/>
);
expect(result).toBeTruthy();
});
});
The following code caused useEffect() to be called infinitely. Why is that the case? Removing drawingboarditems from the useEffect dependencies array solves the infinite loop, but as a result DrawingBoard will not automatically rerender whenenver a user adds an item to the database.
DrawingBoard.jsx
export default function DrawingBoard() {
const [drawingboarditems, setdrawingboarditems] = useState([]);
const currentUser = useContext(CurrentUserContext);
const [loading, setLoading] = useState(true);
const classes = useStyles();
useEffect(() => {
if (currentUser) {
const items = [];
//retrieving data from database
db.collection("drawingboarditems")
.where("userID", "==", currentUser.id)
.get()
.then((query) => {
query.forEach((doc) => {
items.push({
id: doc.id,
...doc.data(),
});
});
setdrawingboarditems(items);
setLoading(false);
});
}
}, [currentUser, drawingboarditems]);
return (
<>
{loading == false ? (
<Container>
<Masonry
breakpointCols={breakpoints}
className="my-masonry-grid"
columnClassName="my-masonry-grid_column"
>
{drawingboarditems.map((item) => (
<div>
<Note item={item} />
</div>
))}
<Note form />
</Masonry>
</Container>
) : (
<div className={classes.root}>
<CircularProgress />
</div>
)}
</>
);
Note.jsx
import React from "react";
import { Card, CardHeader, IconButton, makeStyles } from "#material-ui/core";
import MoreVertIcon from "#material-ui/icons/MoreVert";
import Form from "./Form";
import CardWindow from "./CardWindow";
const useStyles = makeStyles((theme) => ({
card: {
backgroundColor: theme.palette.secondary.main,
margin: theme.spacing(1, 0),
},
}));
export default function Note({ item, form }) {
const classes = useStyles();
return (
<Card className={classes.card}>
{form ? (
<Form />
) : (
<CardHeader
action={
<CardWindow/>
}
title={item.title}
/>
)}
</Card>
);
}
Form.jsx
import React, { useContext } from "react";
import TextField from "#material-ui/core/TextField";
import { makeStyles } from "#material-ui/core/styles";
import AddCircleOutlineIcon from "#material-ui/icons/AddCircleOutline";
import IconButton from "#material-ui/core/IconButton";
import { db } from "../FireStore";
import { CurrentUserContext } from "../utils/Context";
const useStyles = makeStyles((theme) => ({
form: {
"& .MuiTextField-root": {
margin: theme.spacing(1),
width: "70%", // 70% of card in drawing board
},
},
}));
export default function Form() {
const classes = useStyles();
const [value, setValue] = React.useState("");
const currentUser = useContext(CurrentUserContext);
const handleChange = (event) => {
setValue(event.target.value);
};
const handleSubmit = (event) => {
if (value) {
event.preventDefault();
db.collection("drawingboarditems").add({
title: value,
userID: currentUser.id,
});
setValue("");
}
};
return (
<form className={classes.form} noValidate autoComplete="off">
<div>
<TextField
id="standard-textarea"
placeholder="Add item"
multiline
onChange={handleChange}
value={value}
/>
<IconButton aria-label="add" onClick={handleSubmit}>
<AddCircleOutlineIcon fontSize="large" />
</IconButton>
</div>
</form>
);
}
In your case I would move the logic to the DrawingBoard component, and would pass props to the children, so when a children adds an item, the main component would know to refresh the list of items.
Example (not tested):
Extract the logic to work with FireBase to functions. In that way they would be more re-usable, and would not add clutter to your code.
const drawingboarditemsCollection = 'drawingboarditems';
function getAllNotes(userID) {
return db.collection(drawingboarditemsCollection)
.where("userID", "==", userID)
.get()
.then((query) => {
return query.map(doc => {
items.push({
id: doc.id,
...doc.data(),
});
});
});
}
function addNote(userID, title) {
return db.collection(drawingboarditemsCollection).add({
title,
userID,
});
}
The DrawingBoard component should handle the connection with the server, and should pass functions as props to children:
export default function DrawingBoard() {
const [drawingboarditems, setdrawingboarditems] = useState([]);
const currentUser = useContext(CurrentUserContext);
const [loading, setLoading] = useState(true);
const classes = useStyles();
// add the logic to get notes by
const getNotes = useCallback(() => {
setLoading(true);
getAllNotes(currentUser.id)
.then(items => {
setdrawingboarditems(items);
})
.finally(=> {
setLoading(false);
});
}, [currentUser]);
// create the submit handler
const handleSubmit = value => {
addNote(currentUser.id, value)
.then(getNotes); // after adding a note update the items
}
// initial get notes, or when currentUser changes
useEffect(() => {
getNotes();
}, [getNotes]);
return (
<>
{loading == false ? (
<Container>
<Masonry
breakpointCols={breakpoints}
className="my-masonry-grid"
columnClassName="my-masonry-grid_column"
>
{drawingboarditems.map((item) => (
<div>
<Note item={item} />
</div>
))}
<Note form onSubmit={handleSubmit} />
</Masonry>
</Container>
) : (
<div className={classes.root}>
<CircularProgress />
</div>
)}
</>
);
}
Pass the onSubmit function to Form:
export default function Note({ item, form, onSubmit }) {
const classes = useStyles();
return (
<Card className={classes.card}>
{form ? (
<Form onSubmit={onSubmit} />
) : (
<CardHeader
action={
<CardWindow/>
}
title={item.title}
/>
)}
</Card>
);
}
Use onSubmit to handle the submission:
export default function Form({ onSubmit }) {
const classes = useStyles();
const [value, setValue] = React.useState("");
const handleChange = (event) => {
setValue(event.target.value);
};
const handleSubmit = (event) => {
if (value) {
event.preventDefault();
onSubmit(value); // let the parent handle the actual update
setValue("");
}
};
return ( ... );
}
I am fetching list of testsuites using api call on component mount. Api returns list in chronological order.
Setting them as options for a select dropdown(Material-UI).
Then set the selected option to latest testSuite and using its Id get the corresponding testSuite data.
Data is retrieved successfully and pie chart is getting displayed.
Api calls are working fine and React dev tools shows the selectedTestSuite value to be set correctly.But DOM doesn't show the selection in the select dropdown.
Can someone please advise what is the mistake I am doing in this code? Thanks in advance.
import clsx from 'clsx';
import PropTypes from 'prop-types';
import { Doughnut } from 'react-chartjs-2';
import { makeStyles } from '#material-ui/styles';
import axios from 'axios';
import { useSpring, animated } from 'react-spring';
import '../../Dashboard.css';
import MenuItem from '#material-ui/core/MenuItem';
import {
Card,
CardHeader,
CardContent,
Divider,
TextField,
} from '#material-ui/core';
import CircularProgress from '#material-ui/core/CircularProgress';
const useStyles = makeStyles(() => ({
circularloader: {
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
},
actions: {
justifyContent: 'flex-end',
},
inputField: {
width: '150px',
},
}));
const TestSuiteVsScanCount = (props) => {
const { className, ...rest } = props;
const classes = useStyles();
const [doughData, setDoughData] = useState([]);
const [dataLoadedFlag, setDataLoadedFlag] = useState(false);
const [testSuites, setTestSuites] = useState([]);
const [selectedTestSuite, setSelectedTestSuite] = useState({});
useEffect(() => {
function getTestSuites() {
axios.get('http://localhost:3000/api/v1/testsuite/12').then((resp) => {
setTestSuites(resp.data.reverse());
});
}
getTestSuites();
}, []);
useEffect(() => {
if (testSuites.length > 0) {
setSelectedTestSuite(() => {
return {
type: testSuites[0].TestSuiteName,
id: testSuites[0].TestSuiteId,
};
});
}
}, [testSuites]);
useEffect(() => {
function getTestSuiteData() {
let doughData = [];
if (selectedTestSuite.id) {
axios
.get(
'http://localhost:3000/api/v1/summary/piechart/12?days=30&testsuiteid=' +
selectedTestSuite.id,
)
.then((resp) => {
resp.data.forEach((test) => {
doughData = [test.TestCount, test.ScanCount];
});
setDoughData({
labels: ['Test Count', 'Scan Count'],
datasets: [
{
data: doughData,
backgroundColor: ['#FF6384', '#36A2EB'],
hoverBackgroundColor: ['#FF6384', '#36A2EB'],
},
],
});
setDataLoadedFlag(true);
});
}
}
getTestSuiteData();
}, [selectedTestSuite]);
const ChangeType = (id) => {
testSuites.forEach((suite) => {
if (suite.TestSuiteId === id) {
setSelectedTestSuite({
type: suite.TestSuiteName,
id: suite.TestSuiteId,
});
}
});
};
return (
<Card {...rest} className={clsx(classes.root, className)}>
<CardHeader
action={
<TextField
select
label="Select Test Suite"
placeholder="Select Tests"
value={selectedTestSuite.id}
className={classes.inputField}
name="tests"
onChange={(event) => ChangeType(event.target.value)}
variant="outlined"
InputLabelProps={{
shrink: true,
}}
>
{testSuites.map((testSuite) => (
<MenuItem
key={testSuite.TestSuiteId}
value={testSuite.TestSuiteId}
>
{testSuite.TestSuiteName}
</MenuItem>
))}
</TextField>
}
title="Test Suite vs Scan Count"
/>
<Divider />
<CardContent>
<div>
{dataLoadedFlag ? (
<Doughnut data={doughData} />
) : (
<CircularProgress
thickness="1.0"
size={100}
className={classes.circularloader}
/>
)}
</div>
</CardContent>
<Divider />
</Card>
);
};
TestSuiteVsScanCount.propTypes = {
className: PropTypes.string,
};
export default TestSuiteVsScanCount;
I was able to fix this issue with the help of my colleague by setting the initial state of selectedTestSuite to {type:'', id:0} instead of {}.
Changed this
const [selectedTestSuite, setSelectedTestSuite] = useState({});
To this
const [selectedTestSuite, setSelectedTestSuite] = useState({type:'', id:0});
But I am not sure why this worked.
I believe that the main problem is when you pass a value to TextField component with undefined, the TextField component will assume that is an uncontrolled component.
When you set you initial state for selectedTestSuite to be {} the value for selectedTestSuite.id will be undefined. You can find value API reference in https://material-ui.com/api/text-field/
I am fetching array of objects from an API endpoint,
I am using react-redux for state management.
I have two problems.
data gets fetched but only loads and displays after I refresh the page.
console logging single statement 4 times 2 blank array before setting state and 2 with data after setting state where it should have been only once I guess.
Following is my code.
Dashboard Action
import dashboardAPI from "../../../API/ShopUserAPI/dashboardAPI"
export const CURRENT_ORDER_LOAD = "CURRENT_ORDER_LOAD"
export const CURRENT_ORDER_FETCH = "CURRENT_ORDER_FETCH"
export const CURRENT_ORDER_ERROR = "CURRENT_ORDER_ERROR"
export const currentOrderAction = () => {
return dispatch => {
dispatch({ type: CURRENT_ORDER_LOAD, payload: '' })
dashboardAPI.get('/orders')
.then(resp => {
console.log(resp)
dispatch({ type: CURRENT_ORDER_FETCH, payload: resp.data.data.orders })
})
.catch(err => {
console.log(err)
dispatch({ type: CURRENT_ORDER_ERROR, payload: 'error occured. Retry !' })
})
}
}
Dashboard Reducer
import { CURRENT_ORDER_LOAD, CURRENT_ORDER_FETCH, CURRENT_ORDER_ERROR } from "../actions/dashboardActions";
const initialState = {
isLoading: true,
currentOrderData: [],
currentOrdererror: ''
}
const dashboardReducer = (state = initialState, action) => {
switch (action.type) {
case CURRENT_ORDER_LOAD:
return {
...state,
isLoading: true
}
case CURRENT_ORDER_FETCH:
return {
...state,
isLoading: false,
currentOrderData: action.payload
}
case CURRENT_ORDER_ERROR:
return {
...state,
isLoading: false,
currentOrdererror: action.payload
}
default:
return state
}
}
export default dashboardReducer
Dashboard page
import React from 'react'
import { Link } from 'react-router-dom'
import { makeStyles } from '#material-ui/core/styles';
import { Button, Typography, Grid, DialogContent, DialogActions, Dialog, DialogTitle, Divider, ExpansionPanel, ExpansionPanelSummary, ExpansionPanelDetails } from "#material-ui/core";
import ExpandMoreIcon from '#material-ui/icons/ExpandMore';
import { connect } from "react-redux";
const useStyles = makeStyles((theme) => ({
contentCenter: {
display: 'flex',
justifyContent: 'center',
marginTop: '1rem'
},
newSection: {
marginTop: '1rem'
},
innerSection: {
padding: '1rem'
},
heading: {
fontSize: theme.typography.pxToRem(15),
color: theme.palette.text.secondary,
},
secondaryHeading: {
fontSize: theme.typography.pxToRem(15),
color: theme.palette.text.secondary,
},
}))
const CurrentOrders = (props) => {
console.log(props.currentOrderData) // console.log printing 4 times
const [expanded, setExpanded] = React.useState(false);
const handleChange = (panel) => (event, isExpanded) => {
setExpanded(isExpanded ? panel : false);
};
const classes = useStyles()
return (
<div className={classes.newSection}>
{/* current orders list */}
<Typography className={classes.contentCenter}>
Current orders
</Typography>
{/* Orders expansion panel list */}
{props.currentOrderData.map(currentOrders => (
<ExpansionPanel expanded={expanded === 'panel1'} onChange={handleChange('panel1')} className={classes.newSection}>
<ExpansionPanelSummary
expandIcon={<ExpandMoreIcon />}
aria-controls="panel1bh-content"
id="panel1bh-header"
>
<Typography className={classes.heading}>Order ID: #{currentOrders.orderID}</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<Grid container justify="space-between">
<Grid item>
<Link to="/order" style={{ textDecoration: "none" }}>
<Button size="small" variant="contained" color="primary">
View Order
</Button>
</Link>
</Grid>
</Grid>
</ExpansionPanelDetails>
</ExpansionPanel>
))}
</div>
)
}
const mapStateToProps = state => {
return {
currentOrderData: state.dashboard.currentOrderData
}
}
export default connect(mapStateToProps)(CurrentOrders)
I've dispatched dashboard action in the parent component of this page
DIspatching of Dashboard
import React, { useEffect } from "react";
import { Container } from "#material-ui/core";
import CurrentOrders from "./CurrentOrders";
import TodaysOrders from "./TodaysOrders";
import { makeStyles } from '#material-ui/core/styles';
import { Button, Typography, Grid, DialogContent, DialogActions, Dialog, DialogTitle, Divider } from "#material-ui/core";
import { connect } from "react-redux";
import { currentOrderAction } from "../../../store/ShopUserStore/actions/dashboardActions";
const useStyles = makeStyles((theme) => ({
contentCenter: {
display: 'flex',
justifyContent: 'center',
marginTop: '1rem'
},
newSection: {
marginTop: '1rem'
},
innerSection: {
padding: '1rem'
},
buttonSuccess: {
color: theme.palette.success.main
}
}))
const Home = (props) => {
const [open, setOpen] = React.useState(false);
useEffect(() => {
props.getCurrentOrders()
})
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
const classes = useStyles()
return (
<div>
<Container>
{/* shop status */}
<Grid container justify="flex-start" className={classes.newSection}>
<Button variant="outlined" color="primary" onClick={handleClickOpen} className={classes.buttonSuccess}>
Online
</Button>
</Grid>
{/* shop status confirmation dialog */}
<Dialog onClose={handleClose} aria-labelledby="customized-dialog-title" open={open}>
<DialogTitle id="customized-dialog-title" onClose={handleClose}>
Shop status
</DialogTitle>
<Divider />
<DialogContent>
<Typography className={classes.newSection}>
Are you sure that you want to change shop status ?
</Typography>
</DialogContent>
<DialogActions>
<Button autoFocus onClick={handleClose} color="primary">
No
</Button>
<Button autoFocus onClick={handleClose} color="primary">
Yes
</Button>
</DialogActions>
</Dialog>
<Grid container spacing={3}
>
<Grid item xs={12} md={6}>
<CurrentOrders />
</Grid>
<Grid item xs={12} md={6}>
<TodaysOrders />
</Grid>
</Grid>
</Container>
</div>
)
}
const mapDispatchToProps = (dispatch) => {
return {
getCurrentOrders: () => dispatch(currentOrderAction())
}
}
export default connect(null, mapDispatchToProps)(Home)
When you run your application, you should stumble into a nasty loop. The effect hook runs when the component mounts but also when the component updates. Because we are setting the state after every data fetch, the component updates and the effect runs again. It fetches the data again and again. That's a bug and needs to be avoided. We only want to fetch data when the component mounts. That's why you can provide an empty array as second argument to the effect hook to avoid activating it on component updates but only for the mounting of the component.
Try this:
useEffect(async () => {
props.getCurrentOrders()
}, []);