React function freezing website when dispatched - javascript

I passed a function to the child to check a checkbox and then to set setDispatch(true), the problem is that when I check the checkbox everything freezes and the website stops until I close and open again.
the function:
const [selectedChkBx, setSelectedChkBx] = useState({ arrayOfOrders: [] });
const onCheckboxBtnClick = (selected) => {
const index = selectedChkBx.arrayOfOrders.indexOf(selected);
if (index < 0) {
selectedChkBx.arrayOfOrders.push(selected);
} else {
selectedChkBx.arrayOfOrders.splice(index, 1);
}
setSelectedChkBx(selectedChkBx)
toggleDispatchButton()
};
const toggleDispatchButton = () => {
if (selectedChkBx.arrayOfOrders.length == 0) {
setDispatchButtonDisplay(false)
}
else {
setDispatchButtonDisplay(true)
}
}
Child Component:
<form style={{ display: 'block' }} >
<Row sm={1} md={2} lg={3}>
{ordersDisplay.map((value, key) => {
return (
<motion.div key={value.id} layout>
<DeliveryQueueComp
selectedChkBx={selectedChkBx}
toggleDispatchButton={toggleDispatchButton}
setDispatchButtonDisplay={setDispatchButtonDisplay}
value={value}
onCheckboxBtnClick={onCheckboxBtnClick}
/>
</motion.div>
)
})
}
</Row> </form>
DeliveryQueueComp Code:
<div
className={styles1.checkBox}
style={{ background: selectedChkBx.arrayOfOrders.includes(value.id) ?
'#f84e5f' : 'transparent' }}
onClick={() => { onCheckboxBtnClick(value.id) }}
>
<FontAwesomeIcon icon={faCheck} style={{ fontSize: '10px', opacity:
selectedChkBx.arrayOfOrders.includes(value.id) ? '1' : '0' }} />
</div>
If I remove toggleDispatchButtonDisplay, it works but then after a while the page freezes again.
Any thoughts about this?

As you didn't provide setDispatch code I don't know what it does, but for the rest I think I know why it's not working.
You're assigning the array and then set it to the state. If you want to do this that way you should only do a forceUpdate instead of a setState (as it has already been mutated by push and splice).
To properly update your state array you can do it like this
const onCheckboxBtnClick = (selected) => {
const index = selectedChkBx.arrayOfOrders.indexOf(selected);
if (index < 0) {
//the spread in array creates a new array thus not editing the state
setSelectedChkBx({
arrayOfOrders: [...selectedChkBx.arrayOfOrders, selected]
});
} else {
// same thing for the filter here
setSelectedChkBx({
arrayOfOrders: selectedChkBx.arrayOfOrders.filter(
(value) => value !== selected
)
});
}
toggleDispatchButton();
};
Here is the sandbox of your code https://codesandbox.io/s/eager-kalam-ntc7n7

Related

Problems with conditional rendering

I'm trying to mark divs that is clicked on my website. When I click, the array is updated but the mark won't show. It seems like the statement gameChoices.includes('Fortnite') is false, even though the array contains the exact value Fortnite.
Does anyone know why this happens? Eventually a new solution for the problem?
Code:
<Container onClick={() => {
if (gameChoices.includes('Fortnite')) {
const findIndex = gameChoices.findIndex(a => a === 'Fortnite')
findIndex !== -1 && gameChoices.splice(findIndex , 1)
} else if (gameChoices.includes('Fortnite') === false) {
gameChoices.push('Fortnite')
}
}} fluid className="d-flex fortnite gameoption position-relative">
{gameChoices.includes('Fortnite') ?
<>
<BsCheckSquare color="lightgreen" size="2rem" style={{ top: '50%', right: '50%' }} />
</>
: null
}
<h1 className="fw-bolder text-light text-center m-auto">FORTNITE</h1>
</Container>
const [gameChoices, setGameChoices] = useState([])
As I have commented:
Do not use inline click handler. It makes your markup difficult to read.
findIndex !== -1 is not required as you are already checking if it is included in array
Also gameChoices.includes('Fortnite') === false is redundant. Just a simple else is enough
But in addition to this, you need to set value to state.
Apart from that, you should instead look into .some and check for same cased text. You can in addition do trim if game name is coming from user input
const choiceExists = (game) => {
return gameChoices.some(
(name) => name.toLowerCase() === game.toLowerCase()
)
}
const clickHandler = () => {
const name = 'fortnite'
if (choiceExists(name)) {
const newGames = gameChoices.filter((game) => game.toLowerCase() !== name)
setGameChoices(newGames)
} else {
setGameChoices((choices) => choices.concat(name))
}
}
<Container onClick={clickHandler} fluid className="d-flex fortnite gameoption position-relative">
{
gameChoices.includes('Fortnite')
? <BsCheckSquare
color="lightgreen"
size="2rem"
style={{ top: '50%', right: '50%' }} />
: null
}
<h1 className="fw-bolder text-light text-center m-auto">FORTNITE</h1>
</Container>
When you update a reactive state value you should use the state setter method, so setGameChoices((choices)=>[...choices, 'Fortnite'])

I need to open and close accordion based on arrow click

I am using Material UI accordion my issue is if I click on the arrow accordion will get open but again I click on the arrow it will not get closed I need to set it when the user clicks on the arrow according will close and open based on the arrow click check code sandbox link for better understanding.
export default function ControlledAccordions() {
const [expanded, setExpanded] = React.useState(false);
// const handleChange = (panel) => (event, isExpanded) => {
// setExpanded(isExpanded ? panel : false);
// };
const handleChange = (pannel) => {
setExpanded(pannel);
};
const panaalData = ["panel1", "panel2", "panel3", "panel4"];
return (
<div>
{panaalData.map((value, i) => {
return (
<Accordion expanded={expanded === `panel${i}`}>
<AccordionSummary
expandIcon={
<ExpandMoreIcon
onClick={() => {
handleChange(`panel${i}`);
}}
style={{ cursor: "pointer" }}
/>
}
aria-controls="panel1d-content"
id="panel1d-header"
>
fdsfdsf
</AccordionSummary>
<AccordionDetails>dfdf</AccordionDetails>
</Accordion>
);
})}
</div>
);
}
Code SandBox Link
you need to reset panel in that case. You can do that in change handler.
const handleChange = (pannel) => {
setExpanded(expended === pannel ? '' : pannel);
};
when you click the already expanded panel, it just sets it to be expanded again.
you need to check whether the clicked panel is already expanded and if so collapse it instead of expanding it:
const handleChange = (pannel) => {
if (expanded === pannel) setExpanded(false);
else setExpanded(pannel);
};
Create another component called MyAccordian and keep toggling accordion logic in that component. That way you don't need to handle toggling for each and every component separately.
export default function ControlledAccordions() {
const panaalData = ["panel1", "panel2", "panel3", "panel4"];
return (
<div>
{panaalData.map((value, i) => {
return <MyAccordian value={value} />;
})}
</div>
);
}
const MyAccordian = ({ value }) => {
const [expanded, setExpanded] = React.useState(false);
return (
<Accordion expanded={expanded}>
<AccordionSummary
expandIcon={
<ExpandMoreIcon
onClick={() => {
setExpanded((prev) => !prev);
}}
style={{ cursor: "pointer" }}
/>
}
aria-controls="panel1d-content"
id="panel1d-header"
>
{value}
</AccordionSummary>
<AccordionDetails>{value}</AccordionDetails>
</Accordion>
);
};
Working Demo
export default function ControlledAccordions() {
// initial state, everything is closed,
const [expandedIndex, setExpandedIndex] = React.useState(-1);
// this should be handleClic
const handleChange = (index) => {
// in useState, current expandedIndex is passed as the argument
// whatever we return will be set as the expandedIndex
setExpandedIndex((currentIndex) => {
// if any box is open, currentIndex will be that index
// when I click on the open box, it will set the expandedIndex=-1
if (currentIndex === index) {
return -1;
} else {
// If I reached here, that means I am on a closed box
// when I click I swithc the expandedIndex to current box's index
return index;
}
});
};
const panaalData = ["panel1", "panel2", "panel3", "panel4"];
return (
<div>
{panaalData.map((value, i) => {
// when handleChange runs on AccordionSummary expandedIndex===i
// that means when i click on the current box, it will be open
const isExpanded = expandedIndex === i;
return (
<Accordion expanded={isExpanded}>
<AccordionSummary
onClick={() => handleChange(i)}
expandIcon={
// I dont know #mui/material too much.
// main question is "I need to open and close accordion based on arrow click"
<ExpandMoreIcon
onClick={() => handleChange(i)}
style={{ cursor: "pointer" }}
/>
}
aria-controls="panel1d-content"
id="panel1d-header"
>
{value}
</AccordionSummary>
<AccordionDetails
style={{ backgroundColor: "green" }}
>{`box index ${i} is open`}</AccordionDetails>
</Accordion>
);
})}
</div>
);
}
proof of work:
const handleChange = (pannel) => {
setExpanded(!pannel);
};

Conditionally render part of object onClick inside a map (REACT.js)

I am trying to conditionally render part of an object (user comment) onClick of button.
The objects are being pulled from a Firebase Database.
I have multiple objects and want to only render comments for the Result component I click on.
The user comment is stored in the same object as all the other information such as name, date and ratings.
My original approach was to set a boolean value of false to each Result component and try to change this value to false but cannot seem to get it working.
Code and images attached below, any help would be greatly appreciated.
{
accumRating: 3.7
adheranceRating: 4
cleanRating: 2
date: "2020-10-10"
place: "PYGMALIAN"
staffRating: 5
timestamp: t {seconds: 1603315308, nanoseconds: 772000000}
userComment: "Bad"
viewComment: false
}
const results = props.data.map((item, index) => {
return (
<div className='Results' key={index}>
<span>{item.place}</span>
<span>{item.date}</span>
<Rating
name={'read-only'}
value={item.accumRating}
style={{
width: 'auto',
alignItems: 'center',
}}
/>
<button>i</button>
{/* <span>{item.userComment}</span> */}
</div >
)
})
You have to track individual state of each button toggle in that case.
The solution I think of is not the best but you could create a click handler for the button and adding a classname for the span then check if that class exists. If it exists then, just hide the comment.
Just make sure that the next sibling of the button is the target you want to hide/show
const toggleComment = (e) => {
const sibling = e.target.nextElementSibling;
sibling.classList.toggle('is-visible');
if (sibling.classList.contains('is-visible')) {
sibling.style.display = 'none'; // or set visibility to hidden
} else {
sibling.style.display = 'inline-block'; // or set visibility to visible
}
}
<button onClick={toggleComment}>i</button>
<span>{item.userComment}</span>
You can try like this:
const [backendData, setBackendData] = useState([]);
...
const showCommentsHandler = (viewComment, index) => {
let clonedBackendData = [...this.state.backendData];
clonedBackendData[index].viewComment = !viewComment;
setBackendData(clonedBackendData);
}
....
return(
<div>
....
<button onClick={() => showCommentsHandler(item.viewComment, index)}>i</button>
{item.viewComment && item.userComment}
<div>
You can store an array with that places which are clicked, for example:
const [ selectedItems, setSelectedItems] = React.useState([]);
const onClick = (el) => {
if (selectedItems.includes(el.place)) {
setSelectedItems(selectedItems.filter(e => e.place !== el.place));
} else {
setSelectedItems(selectedItems.concat(el));
}
}
and in your render function
const results = props.data.map((item, index) => {
return (
<div className='Results' key={index}>
<span>{item.place}</span>
<span>{item.date}</span>
<Rating
name={'read-only'}
value={item.accumRating}
style={{
width: 'auto',
alignItems: 'center',
}}
/>
<button onClick={() => onClick(item)}>i</button>
{ /* HERE */ }
{ selectedItems.includes(item.place) && <span>{item.userComment}</span> }
</div >
)
})
You need to use useState or your component won't update even if you change the property from false to true.
In order to do so you need an id since you might have more than one post.
(Actually you have a timestamp already, you can use that instead of an id.)
const [posts, setPosts] = useState([
{
id: 1,
accumRating: 3.7,
adheranceRating: 4,
cleanRating: 2,
date: "2020-10-10",
place: "PYGMALIAN",
staffRating: 5,
timestamp: { seconds: 1603315308, nanoseconds: 772000000 },
userComment: "Bad",
viewComment: false
}
]);
Create a function that updates the single property and then updates the state.
const handleClick = (id) => {
const singlePost = posts.findIndex((post) => post.id === id);
const newPosts = [...posts];
newPosts[singlePost] = {
...newPosts[singlePost],
viewComment: !newPosts[singlePost].viewComment
};
setPosts(newPosts);
};
Then you can conditionally render the comment.
return (
<div className="Results" key={index}>
<span>{item.place}</span>
<span>{item.date}</span>
<Rating
name={"read-only"}
value={item.accumRating}
style={{
width: "auto",
alignItems: "center"
}}
/>
<button onClick={() => handleClick(item.id)}>i</button>
{item.viewComment && <span>{item.userComment}</span>}
</div>
);
Check this codesandbox to see how it works.

How to test button click in child component when testing using Jest Enzyme?

So I have a parent component called InventoryView and a child component InventoryBase that basically renders data sent to it by InventoryView. So the problem I keep running into is that I need to test if the Update button in InventoryBase is calling handleUpdate(). HandleUpdate should only be called if input values have actually been changed. I need to test whether or not handleUpdate() is being called if input changes or not.
So the button first called --> handleClick() which looks at the inputs and gathers data and then calls handleUpdate() which propagates to InventoryView. When I test handleUpdate() and make sure if the inputs have changed, it says handleUpdate() has been called zero times. Since I have changed one of the inputs the handleUpdate() and handleClick() both should be called once. But they are being called 0 times. I console.log the prev and cur input values in handleClick() so when I simulate a click on the button those values are being printed meaning handleClick() is being triggered but the click on mock is not being registered..??? I am not sure what is happening here.
InventoryView:
render() {
return(
<InventoryBase
tableHeadings={inventoryViewUpdate.tableHeadings}
tableTitle={inventoryViewUpdate.tableTitle}
tableBody={this.state.tableBody}
tableFooter={this.state.tableFooter}
handleUpdate={this.handleUpdate}
lastUpdated={this.state.lastUpdated}
endOfDay={true} />
);
}
InventoryBase: inventoryBase.updateBtn === 'UPDATE' which is the ID I use to simulate the button.
handleClick = (inputIds, prevVals) => {
let change = false;
let curVals = [];
let update = {
inputUpdate: [],
timeUpdate: this.updateDate()
};
for (let i in inputIds) {
let pkg = {
id: inputIds[i],
value: document.getElementById(inputIds[i]).value
}
curVals[i] = pkg.value;
update.inputUpdate.push(pkg);
}
for (let i in curVals) {
if (curVals[i] != prevVals[i]) change = true;
}
if (change === false) return;
console.log(curVals); <-- prints ['2', '4']
console.log(prevVals); <-- prints ['4', '4']
this.props.handleUpdate(update); <----- call handleUpdate here if inputs have changed
}
render() {
let inputIds = [];
let prevVals = [];
const { classes } = this.props;
return (
<Aux>
<Paper className={classes.root}>
<div style={{ float: 'right', width: "132px" }}>
<div style={{ display: 'flex', align: 'center', justifyContent: 'center' }}>
<Button type="button" id={inventoryBase.updateBtn} variant="contained" onClick={() => this.handleClick(inputIds, prevVals)} className={classes.updateBtn}>
<span>
<Autorenew style={{
color: '#FFFFFF',
marginRight: '8px'
}} />
</span>
<span>
<Typography style={{ marginBottom: '8px' }} className={classes.btnText}>{inventoryBase.updateBtn}</Typography>
</span>
</Button>
</div>
....
..... more code
</Paper>
</Aux>
);
}
InventoryView.test.js:
const STORE = createStore();
describe('InventoryBase', () => {
let wrapper;
let props;
beforeEach(() => {
props = {
inventory: inventory,
packages: packages,
branchLockers: branchLockers,
updateInventory: jest.fn(),
handleUpdate: jest.fn()
}
wrapper = mount(
<Provider store={STORE}>
<InventoryViewComponent {...props}/>
</Provider> , { attachTo: document.body }
);
});
test('Should update last updated time if input values have changed', () => {
document.getElementById('Envelop_1').value = '2'
wrapper.find('#UPDATE').at(0).simulate('click');
wrapper.update();
expect(props.handleUpdate).toHaveBeenCalledTimes(1);
});
});

React native - open multiple modals one after the other inside for loop

I first make an Ajax call (to an API) which provides me some data, a list of achievements (array of objects). I would like to loop through this array, show the first achievement as a Modal and on click of a button close the modal then show the next one (next achievement) and so on.
Ajax call providing the data:
getAchievements = () => {
fetch(url + '/achievements', {
method: 'get',
headers: {
Accept: 'application/json',
'Content-type': 'application/json'
}
})
.then((data) => data.json())
.then((data) => {
this.props.addData({
achievements: data.achievements
})
if(this.props.store.achievements.length > 0) {
this.setState({
showAchievementModal: true
})
}
})
.catch((error) => {
console.error(error)
})
}
Here I show the modals:
render() {
return (
{this.state.showAchievementModal &&
<Modal
animationType={'fade'}
visible={this.props.store.isModalAchievementVisible}
>
{this.props.store.achievements.map((data,index)=>{
return(
<View key={index}>
<View style={styles.container}>
<Text>{data.title}</Text>
<Text>{data.description}</Text>
<TouchableOpacity onPress={this.closeModal}>
<Text>Collect</Text>
</TouchableOpacity>
</View>
</View>
)
})}
</Modal>
}
)
}
At the moment all the Modals open at the same time. How could I open them one after the other after clicking the Collect button?
This is the updated version of my code that works:
Initialising activeModalIndex in the constructor:
constructor(props) {
super(props)
this.state = {
activeModalIndex: 0
}
}
Get achievements:
getAchievements = () => {
if(this.props.store.achievements.length > 0) {
this.setState({
showAchievementModal: true,
activeModalIndex: 0,
})
}
}
Render function:
render() {
return this.props.store.achievements.map((data,index) => this.state.activeModalIndex === index &&
<Modal>
<View key={index}>
<View style={styles.container}>
<Text>{data.title}</Text>
<Text>{data.description}</Text>
<TouchableOpacity onPress={this.closeModal}>
<Text>Collect</Text>
</TouchableOpacity>
</View>
</View>
</Modal>
)
}
Close Modal:
closeModal = () => {
const maxIndex = this.props.store.achievements.length - 1
const currentIndex = this.state.activeModalIndex
const isLastModal = currentIndex === maxIndex
const newIndex = isLastModal? -1: currentIndex +1
this.setState({
activeModalIndex: newIndex
})
}
The problem is that you have multiple Modals on your page and they all use the same boolean to check if they should be rendered. Initially, showAchievementModal is set to true, so all modals are rendered. Furthermore, after you set showAchievementModal to false in closeModal, it will permanently stay false, so no additional modals will get rendered.
render() {
return (
{this.state.showAchievementModal &&
<Modal
...
</Modal>
}
)
}
Instead of showAchievementModal you should be keeping track of index of active modal. So, after you fetch the list of achievements from your API, set the activeModalIndex to 0. After the user dismisses this first modal, set the activeModalIndex to 1 inside the closeModal method, then set it to 2 after the second modal is closed and so on.
Now, for every modal to correspond to a single achievement, we must map each element of the achievements array to a single Modal and conditionally render it only if its corresponding index is the active one.
render() {
const achievements = this.props.store.achievements;
const { activeModalIndex } = this.state;
return achievements.map((data, index) => activeModalIndex === index &&
<Modal key={index}>
<View>
<View style={styles.container}>
<Text>{data.title}</Text>
<Text>{data.description}</Text>
<TouchableOpacity onPress={this.closeModal}>
<Text>Collect</Text>
</TouchableOpacity>
</View>
</View>
</Modal>
)
}
When the users dismisses the currently active modal, simply increment the index of the active modal and the next modal will appear instead of the current one. If the new incremented value is equal or larger than the array length, nothing will get rendered, so no need to check for max index value before setting new state.
closeModal = () => {
this.setState(previousState => ({
activeModalIndex: previousState.activeModalIndex + 1,
}))
}
Also, please read about the dangers of setting index as key when rendering lists. If you happen to need ordering achievements by some value/priority and users can retrieve multiple pages of their achievements, it might cause rendering wrong components.

Categories