Component not re-rendering after change in an array state in React - javascript

The component is not rerendering after the deletion of an element in the state but the state does change.
In the component, you can add an element in the array (which is a state) through form, see all the elements in the array, and delete it from the state using the button. So after deleting an element that is in the state, the component does not rerender. Following is the code of the component:
import React, { useEffect, useState } from 'react';
import {
Typography,
IconButton,
Button,
TextField,
Paper,
} from '#mui/material';
import {
CancelOutlined,
AddBoxOutlined,
VisibilityOutlined,
VisibilityOffOutlined,
} from '#mui/icons-material';
export default function Test1() {
const [subNames, setSubNames] = useState([]);
const [subName, setSubName] = useState('');
const [showSubForm, setShowSubForm] = useState(false);
const onSubNameChange = (e) => {
setSubName(e.target.value);
};
const onSubNameSubmit = () => {
if (!subName) return alert('Enter name!');
setSubNames((prev) => prev.concat({ name: subName }));
setShowSubForm(false);
setSubName('');
};
const subForm = (
<>
<div
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}>
<TextField
label='Sub Todo Name'
onChange={onSubNameChange}
name='subTodoName'
value={subName}
size='small'
variant='outlined'
fullWidth
/>
<IconButton onClick={onSubNameSubmit}>
<AddBoxOutlined color='primary' />
</IconButton>
</div>
<br />
</>
);
const onDelete = (position, e) => {
let arr = subNames;
arr.splice(position, 1);
setSubNames(arr);
};
return (
<div>
<h1>Hello World!</h1>
{subNames.map((item, key) => (
<Paper
key={key}
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
margin: 'auto',
padding: 10,
marginTop: 10,
borderRadius: '10px',
}}
elevation={3}>
<div sx={{ display: 'flex', alignItems: 'center' }}>
<Typography variant='body1'>
<b>Sub Todo-{key + 1}:</b>
</Typography>
<Typography variant='body1'>{item?.name}</Typography>
</div>
<IconButton onClick={(e) => onDelete(key, e)}>
<CancelOutlined color='primary' />
</IconButton>
</Paper>
))}
<br />
{showSubForm && subForm}
<div>
{showSubForm && (
<Button
variant='contained'
sx={{ float: 'right' }}
color='primary'
size='small'
startIcon={<VisibilityOffOutlined />}
onClick={() => setShowSubForm(false)}>
Add sub todo item
</Button>
)}
{!showSubForm && (
<Button
variant='contained'
sx={{ float: 'right' }}
onClick={() => setShowSubForm(true)}
color='primary'
size='small'
startIcon={<VisibilityOutlined />}>
Add sub todo item
</Button>
)}
</div>
</div>
);
}

React won't update the DOM every time you give the same state to a setState. For primitive values like Number, String, and Boolean, it's obvious to know whether we are giving a different value or not.
For referenced values like Object and Array on the other hand, changing their content doesn't flag them as different. It should be a different memory reference. See your commented code to understand what you are doing wrong:
let arr = subNames; // does a reference copy, means arr === subNames
arr.splice(position, 1); // changes its content, but still arr === subNames
setSubNames(arr); // at this point it's like nothing has changed
A solution could be the spread operator, which will create a copy of your existing array but on a new memory reference, like so:
let arr = [...subNames]; // creates a copy of subNames on a new reference
arr.splice(position, 1); // updates the content of the newly created array
setSubNames(arr); // new reference is given to setSubNames

Related

MUI : Out-of-range value `X` for the select component

I'm making a glossary where each of the definitions are a cards that can be flipped (CardFlip) I build an array where I send for each card, the data via props to my component "CardFlip" dealing with the actual construction of cards with my data
This is my first component sending everything :
<div>
{glossaire.map((carte, index) => (
<Fragment key={carte.dat_id}>
<CardFlip item={carte} tags={tags} />
</Fragment>
))}
</div>
First prop ,"item", contains information such as: a title, a definition, a search tag
Second prop, "tags", is the list of tags that a definition can have, a definition can have only one tag, right now only those tags are available : "Application", "Entreprise", "Technique" and "Télécom"
And here is the code for my second component (only the interesting part):
export default function CardFlip = ({ item, user, tags }) => {
// -- [Variables] --
// Flip front / back
const [isFlipped, setIsFlipped] = useState(false);
// Storage
const [titreDef, setTitreDef] = useState("");
const [definitionDef, setDefinitionDef] = useState("");
const [tagSelected, setTagSelected] = useState("");
// Flag for error
const [errorTitre, setErrorTitre] = useState(false);
const [errorDefinition, setErrorDefinition] = useState(false);
const [errorSelect, setErrorSelect] = useState(false);
console.log(item.dat_tag);
console.log(tags);
// -- [Handlers] --
// UPDATE
const handleTitre = (data) => {
setTitreDef(data);
setErrorTitre(false);
};
const handleDefinition = (data) => {
setDefinitionDef(data);
setErrorDefinition(false);
};
const handleSelect = (event) => {
const {
target: { value },
} = event;
setTagSelected(value);
setErrorSelect(false);
}
return (
<Grow in style={{ transformOrigin: "0 0 0" }} {...{ timeout: 1000 }}>
<div style={{ display: "flex", padding: "10px" }}>
<ReactCardFlip
isFlipped={isFlipped}
flipDirection="horizontal"
style={{ height: "100%" }}
>
<div
className={style.CardBack}
style={{ display: "flex", height: "100%" }}
>
<Card className={style.mainCard}>
<CardActions className={style.buttonFlipCard}>
<Tooltip title="Retour">
<IconButton
className={style.iconFlipCard}
disableRipple
onClick={() => setIsFlipped((prev) => !prev)}
>
<ChevronLeftIcon />
</IconButton>
</Tooltip>
</CardActions>
<CardContent>
<div className={style.divTagBack}>
<FormControl
sx={{width: "90%"}}
>
<InputLabel
id="SelectLabel"
sx={{display: "flex"}}
>
{<TagIcon />}
{" Tag"}
</InputLabel>
<Select
labelId="SelectLabel"
label={<TagIcon /> + " Tag"}
renderValue={(selected) => (
<Chip
onMouseDown={(event) => {
event.stopPropagation();
}}
key={selected}
label={selected}
icon={<TagIcon />}
/>
)}
defaultValue={item.dat_tag}
onChange={handleSelect}
>
{tags && tags.map((tag) => (
<MenuItem key={tag.dat_tag} value={tag.dat_tag}>
{tag.dat_tag}
</MenuItem>
))}
</Select>
</FormControl>
</div>
</CardContent>
</Card>
</div>
</ReactCardFlip>
</div>
</Grow>
);
};
When the user returns the card, he can change the title, description and tag of the chosen card.
My problem is with the Select.
In order to display the selected tag before any modification, I display the tag in defaultValue={item.dat_tag}
(Also tried with value and not defaultValue)
Then with my second prop, I build the list of my menu.
This is where I get my warning (which seems to extend the rendering time of the page considerably (Since it load / render like +100 definitions, getting a warning for every definition)
MUI: You have provided an out-of-range value Entreprise for the select component.
Consider providing a value that matches one of the available options or ''. The available values are "".
This is an example of what a console.logs told me about my props :
item.dat_tag
Entreprise
tags
0: {dat_tag: "Applicatif"}
1: {dat_tag: "Entreprise"}
2: {dat_tag: "Technique"}
3: {dat_tag: "Télécom"}
I already looked at several posts saying to put in a string variable my tag data (item.dat_tag) or to display my menu when it is not empty. No change.

How to pass data from input field to parent component using context API in React?

I have an input field in child component. If you type in valid wallet address it should display wallet balance in parent component.
I don't know how to pass that e.target.value we are reading from input field.
I tried to do this function in parent component - Dashboard.
const [walletBalance, setWalletBalance] = useState('');
const getWalletBalance = async () => {
try {
const web3 = new Web3(Web3.givenProvider || 'ws://localhost:3000');
for (const wallet of dummyWallets) {
const balance = await web3.eth.getBalance(wallet.address);
console.log(balance);
const balanceConvertedFromWeiToEth = await Web3.utils.fromWei(balance, 'ether');
const shortenedBalance =
balanceConvertedFromWeiToEth.length >= 10
? balanceConvertedFromWeiToEth.slice(0, 10)
: balanceConvertedFromWeiToEth;
setWalletBalance(shortenedBalance);
}
} catch (error) {
console.error(error);
throw new Error('No wallet found');
}
};
render (
<p>Wallet balance: {walletBalance}</p>
and then pass this function down to child component with input field (need to prop drill 3 times for that) and in child component say:
const [inputText, setInputText] = useState('');
const handleInputChange = (e) => {
setInputText(e.target.value);
if (inputText === wallet.address) {
getWalletBalance()
}
};
render (
<input value={inputText} onChange={handleInputChange} />
)
It didn't work. No errors. Just nothing happens.
Then I thought that I need to do the other way around: Instead of trying to pass getWalletBalance() down as props, I need to read data from input field and pass that data to parent component, because parent needs to have access to the input field and read from it to be able to display wallet balance in parent.
I've also tried to create context:
import React, { useState, createContext } from 'react';
import { dummyWallets } from '../mocks/dummyWallets';
import Web3 from 'web3';
export const WalletContext = createContext();
// eslint-disable-next-line react/prop-types
export const TransactionProvider = ({ children }) => {
const [inputText, setInputText] = useState('');
const [walletBalance, setWalletBalance] = useState('');
const handleInputChange = async (e) => {
setInputText(e.target.value);
const web3 = new Web3(Web3.givenProvider || 'ws://localhost:3000');
for (const wallet of dummyWallets) {
if (inputText === wallet.address) {
const balance = await web3.eth.getBalance(wallet.address);
console.log(balance);
const balanceConvertedFromWeiToEth = await Web3.utils.fromWei(balance, 'ether');
const shortenedBalance =
balanceConvertedFromWeiToEth.length >= 10
? balanceConvertedFromWeiToEth.slice(0, 10)
: balanceConvertedFromWeiToEth;
setWalletBalance(shortenedBalance);
}
}
};
return (
<WalletContext.Provider
value={{ walletBalance, handleInputChange, inputText, setInputText,
setWalletBalance }}>
{children}
</WalletContext.Provider>
);
};
Then pass handleInputChange to onChange in child component and inputText as value, then use walletBalance in parent component.
All context things walletBalance, handleInputChange, inputText, setInputText and setWalletBalance threw undefined error.
Here is child component consuming data from context:
import React, { FC, useState, useContext } from 'react';
import { Formik, Form, Field } from 'formik';
import { Button, InputAdornment, Box, Grid } from '#material-ui/core';
import TextField from '#eyblockchain/ey-ui/core/TextField';
import { Search } from '#material-ui/icons';
import SvgEthereum from '../../../images/icon/Ethereum';
import { makeStyles } from '#material-ui/styles';
import { Theme } from '#material-ui/core';
import { dummyWallets } from '../../../shared/mocks/dummyWallets';
import Web3 from 'web3';
import { WalletContext } from '../../../shared/context/WalletContext';
const etherIconBorderColor = '#eaeaf2';
const useStyles = makeStyles((theme: Theme) => ({
etherIconGridStyle: {
padding: '16px 0 0 0'
},
etherIconStyle: {
fontSize: '220%',
textAlign: 'center',
color: theme.palette.primary.light,
borderTop: `solid 1px ${etherIconBorderColor}`,
borderBottom: `solid 1px ${etherIconBorderColor}`,
borderLeft: `solid 1px ${etherIconBorderColor}`,
padding: '5px 0 0 0',
[theme.breakpoints.down('sm')]: {
fontSize: '150%',
lineHeight: '43px'
}
},
buttonStyle: {
paddingTop: theme.spacing(2)
},
searchIconStyle: {
color: theme.palette.primary.light
}
}));
const onSubmitHandler = () => {
return;
};
const SearchInputForm: FC<any> = (props) => {
const { walletBalance, handleInputChange, inputText } = useContext(WalletContext);
const classes = useStyles();
return (
<Formik
onSubmit={onSubmitHandler}
initialValues={{ name: '' }}
render={({ values }) => {
return (
<Form>
<Grid container>
<Grid item xs={1} className={classes.etherIconGridStyle}>
<Box className={classes.etherIconStyle}>
<SvgEthereum />
</Box>
</Grid>
<Grid item xs={11}>
<Field
required
name="name"
placeholder={props.placeholdervalue}
value={inputText}
component={TextField}
onChange={handleInputChange}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<Search fontSize="large" className={classes.searchIconStyle} />
</InputAdornment>
)
}}
/>
</Grid>
<Grid item xs={12}>
<Box
display="flex"
justifyContent="flex-end"
alignItems="flex-end"
className={classes.buttonStyle}>
<Button type="submit" variant="contained" color="secondary" size="large" disabled>
Create Alert
</Button>
</Box>
</Grid>
</Grid>
</Form>
);
}}
/>
);
};
export default SearchInputForm;
Here is parent component where I pull walletBalance from context:
const Dashboard: FC<any> = () => {
const { walletBalance } = useContext(WalletContext);
const classes = useStyles();
return (
<>
<Container maxWidth={false} className={classes.dashboardContainer}>
<Grid>
<Box data-testid="alert-page-heading">
<Typography variant="h3">Alerts</Typography>
</Box>
<Box data-testid="alert-page-subheading">
<Typography className={classes.subHeadingStyle}>
Monitor the Web 3.0 for interesting blockchain events
</Typography>
</Box>
</Grid>
<Grid container spacing={4}>
<Grid item xs={12} sm={8}>
<Box className={classes.dashboardCard}>
<Box className={classes.searchBox}>
<SearchTabs />
</Box>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between'
}}>
<Box
sx={{
height: '60%',
padding: '1%',
fontSize: 20
}}>
<a href="http://localhost:3000/" className={classes.backToAlertsLink}>
Back to my alerts
</a>
<p className={classes.boldText}>Recent transactions</p>
</Box>
<Box sx={{ height: '60%', padding: '4%', fontSize: 20 }}>
<span>Wallet Balance</span>
<span className={classes.balanceNumber}>{walletBalance} ETH</span>
</Box>
</Box>
</Box>
</Grid>
<Grid item xs={12} sm={4}>
<Box className={classes.dashboardCard}>
<Box sx={{ height: '60%', padding: '2%', fontSize: 20, fontWeight: 'bold' }}>
Top 10 alerts
</Box>
</Box>
</Grid>
</Grid>
</Container>
</>
);
};
export default Dashboard;
The problem is that I pull handleChange from context and apply to input field in child component to read the value from the input and then pull walletBalance from context in parent but that walletBalance is not updated (doesn't contain input value in it).
Can someone please share knowledge on how to read data from child component input field and display something from that in parent component?
Any advice is appreciated. Thank you.

Issue with javascript map function

I am working with the map function in react and I am getting all my info to map except for my button ref. Below is the code. The data is getting pulled from a database and passed as a prop as well. When I filter the list I get the look I am looking for but when I go to select a doctor I only get the last name for the doctor that was the last item iterated, and this is across all the buttons when I click them. Where am I going wrong if I may ask.
import {
Button,
Card,
CardActions,
CardContent,
Grid,
Typography,
} from "#material-ui/core";
import { makeStyles } from "#material-ui/styles";
import { useRef } from "react";
const styles = makeStyles({
bound: {
margin: "40px 200px",
position: "relative",
},
card: {
maxWidth: "70%",
margin: "50px auto",
borderStyle: "solid",
borderRadius: "50px",
borderColor: "black",
borderWidth: "3px",
"&:hover": {
boxShadow: "black",
borderColor: "green",
},
},
null: {
display: "none",
},
btn: {
margin: "20px 0 0 0",
borderRadius: "20px",
"&:hover": {
backgroundColor: "green",
color: "white",
},
},
});
const SearchResult = (props) => {
const doctorList = props.doctor;
const selectedRef = useRef();
const selectedHandler = (e) => {
console.log(e.target.target);
console.log(selectedRef.current.value);
};
const doctorCard = (doctorList) => {
return (
<Card
variant="outlined"
className={`${doctorList ? classes.card : classes.null}`}
key={doctorList.user_id}
id={doctorList.user_id}
>
<CardContent>
<Grid container>
<Grid xs={6} md={6} lg={6} item>
<img src="#" alt="Img.png" />
</Grid>
<Grid xs={6} md={6} lg={6} item>
<Typography variant="h6">
Dr. {doctorList.first_name} {doctorList.last_name}
</Typography>
<Typography variant="h6">
Speciality: {doctorList.title}
</Typography>
<Typography variant="h6">
Primary Language: {doctorList.primary_language}
</Typography>
</Grid>
</Grid>
<CardActions>
<Button
variant="contained"
className={classes.btn}
type="submit"
ref={selectedRef}
value={doctorList.last_name}
onClick={selectedHandler}
size="large"
fullWidth
>
Select
</Button>
</CardActions>
</CardContent>
</Card>
);
};
const classes = styles();
return (
<div className={classes.bound}>{doctorList.doctor.map(doctorCard)}</div>
);
};
export default SearchResult;
OUTPUT
You are looping list and using common reference 'selectedRef' in you list component. Once list rendering finishes, last_name of last item in your array is being stored in 'selectedRef' reference. That's why whenever you click it's always getting last item.
To resolve this you might want to break component on granular level. Use two component ItemList and ListItem. ItemList will simply loop array and in ListItem component create reference to each item while looping. That should resolve your issue.
const ItemList = (props) => {
const renderList=(doctor)=>{
return <ListItem key={`doctor-${doctor.id}`} {...doctor}/>
}
return <div>{props.data.map(renderList)}</div>
}
const ListItem = () => {
// reference below create new reference for every item
const selectedRef = useRef();
// rest of logic to render doctor card
}

how can i append selected row data in another table

what my task is I am using a table with two different conditions like in the first table whatever data coming I will show that in the first table and in the second table whatever row I select in the first table that I want to show in the second table as the second table I called select summary so my task is in the first table whatever row I selected in need to how that row in the second table I am using same table component for this for better you can see CodeSandBox link
import React, { useState, useMemo, useEffect } from "react";
import {
Grid,
makeStyles,
CardContent,
Card,
Box
} from "#material-ui/core";
import EnhancedTable from "./EnhancedTable";
const useStyles = makeStyles((theme) => ({
root: {
padding: theme.spacing(0, 2, 2),
},
formGrid: {
padding: theme.spacing(2),
},
cardColor: {
borderColor: "#0bb7a7",
},
}));
function AddToExclusionList() {
const classes = useStyles();
const [sanctionsList, setSanctionsList] = useState([]);
const updateListsRow = ({ index, value, row }, listType) => {
switch (listType) {
case "sanctions":
setSanctionsList((prevState) => {
prevState[index].status = value;
return [...prevState];
});
break;
default:
}
};
return (
<Grid className={classes.root}>
<Grid item xs={12} style={{ textTransform: "capitalize" }}>
<Card className={classes.cardColor} variant="outlined">
<CardContent>
<>
<EnhancedTable
show={true}
step="first"
/>
</>
</CardContent>
</Card>
</Grid>
<Box mt={3}></Box>
<Grid item xs={12} style={{ textTransform: "capitalize" }}>
<Card className={classes.cardColor} variant="outlined">
<CardContent>
<>
<h3>summary table</h3>
<EnhancedTable
checkboxToggle={(rowDetails) => {
updateListsRow(rowDetails, "sanctions");
}}
/>
</>
</CardContent>
</Card>
</Grid>
</Grid>
);
}
export default AddToExclusionList;
CodeSandBox Link
You've achieved your goal very weird! Anyway, based on your code in codesandbox. You need to add a state to AddToExclusionList component, like this:
const [newRows, setNewRows] = useState([]);
const setSummaryRows = (selectedRows) => {
const copy = [...rows];
const filteredRows = copy.filter((x) => selectedRows.includes(x.name));
setNewRows(filteredRows);
};
We need the mentioned state to update the summary table's rows.
Also add rows and setNewRows prop to EnhancedTable and give it rows from out of the component. In addition move rows and createData to App.js. So you should use EnhancedTable in App.js same as bellow:
<Grid className={classes.root}>
<Grid item xs={12} style={{ textTransform: "capitalize" }}>
<Card className={classes.cardColor} variant="outlined">
<CardContent>
<>
<h3>first table</h3>
<EnhancedTable
// passing data for rendering table according condition
step="first"
rows={rows}
setNewRows={(selected) => {
setSummaryRows(selected);
}}
/>
</>
</CardContent>
</Card>
</Grid>
<Box mt={3}></Box>
<Grid item xs={12} style={{ textTransform: "capitalize" }}>
<Card className={classes.cardColor} variant="outlined">
<CardContent>
<>
<h3>summary table</h3>
<EnhancedTable
// trying to pasing selected data
rows={newRows}
setNewRows={() => {}}
/>
</>
</CardContent>
</Card>
</Grid>
</Grid>
And the last part is using useEffect based on selected in EnhancedTable component:
useEffect(() => {
setNewRows(selected);
}, [selected]);

How to toggle class when user selects a list item coming from api in react.s?

I'm working on an application where I render the DB results by a loop through using the map method.
When the user clicks on each item, first it will add those id's into an array and toggle a CSS class to change the border color of a particular selected item, when the item is selected again, the border will disappear, same goes for all items rendered in the UI. Adding the id's into an array is working fine.
I'm facing a problem toggling CSS style to change the border color when click on each item.
Here is my code to render items on DOM
const [selectedItems, setSelectedItems] = React.useState([])
const onClickHandler = (id) => {
if (selectedItems.indexOf(id) > -1) {
let updatedItems = selectedItems.filter(itemID => itemID !== id)
console.log(updatedItems);
setSelectedItems(updatedItems)
} else {
setSelectedItems(prevState => [...prevState, id] )
}
setOpen(true);
}
{
courses.length > 0 && courses.map(course => {
return (
<>
<CourseItem course={course} onClickHandler={onClickHandler} itemSelect={itemSelect} />
</>
)
})
}
function CourseItem({ course, onClickHandler }) {
const classes = useStyles();
return (
<>
<Col style={{ marginTop: 10 }}>
<Paper elevation={0} className={classes.card}>
<div className={classes.cardTop} style={{ backgroundImage: `url('${course.thumbnail}')`, backgroundSize: 'cover', backgroundPosition: 'center' }}>
<div style={{ padding: '8px 18px', display: 'flex', justifyContent : 'space-between' }}>
<CheckCircleIcon onClick={() => onClickHandler(course._id)} />
{/* <Typography component={"label"} variant="caption"> { course.level }</Typography> */}
<Chip label={course.level} style={{ color: '#000', backgroundColor: '#f9f9f9', fontFamily: 'regular1', position: 'relative', zIndex: 0}} variant="outlined" />
</div>
</div>
<div className={classes.cardBottom}>
<Typography> { course.moduleName }</Typography>
<Typography variant="subtitle2" component={"span"}> { course.description }</Typography>
</div>
</Paper>
</Col>
</>
)
}
You can use tow different class for two different condition. Then use clsx to conditionally apply class.
clsx link -> npm clsx

Categories

Resources