Interupt code and wait for user interaction in a loop - React - reactjs

I am trying to implement an "add all" button in my react app. to do that, i pass this function to the onClick method of the button :
for (element in elements) {
await uploadfunction(element)
}
const uploadfunction = async (element) => {
if (valid) {
// await performUpload(element)
}
else if (duplicate) {
//show dialog to confirm upload - if confirmed await performUpload(element)
}
else {
// element not valid set state and show failed notification
}
}
const performUpload = async (element) => {
// actual upload
if(successful){
// set state
}else{
// element not successful set state and show failed notification
}
}
the uploadfunction can have three different behaviors :
Add the element to the database and update the state
Fail to add the element and update the state
Prompt the user with the React Dialog component to ask for confirmation to add duplicat element and update the state accordingly
My problem now is since i'm using a for loop and despite using Async/await , i can't seem to wait for user interaction in case of the confirmation.
The behavior i currently have :
The for loop move to the next element no matter what the result
The Dialog will show only for a second and disappear and doesn't wait for user interaction
Wanted behavior:
Wait for user interaction (discard/confirm) the Dialog to perform the next action in the loop.
How can i achieve that with React without Redux ?

Here is an example of a component that might work as an inspiration for you.
You might split it in different components.
class MyComponent extends Component {
state = {
items: [{
// set default values for all booleans. They will be updated when the upload button is clicked
isValid: true,
isDuplicate: false,
shouldUploadDuplicate: false,
data: 'element_1',
}, {
isValid: true,
isDuplicate: false,
shouldUploadDuplicate: false,
data: 'element_1',
}, {
isValid: true,
isDuplicate: false,
shouldUploadDuplicate: false,
data: 'element_2',
}],
performUpload: false,
};
onUploadButtonClick = () => {
this.setState(prevState => ({
...prevState,
items: prevState.items.map((item, index) => ({
isValid: validationFunction(),
isDuplicate: prevState.items.slice(0, index).some(i => i.data === item.data),
shouldUploadDuplicate: false,
data: item.data
})),
performUpload: true,
}), (nextState) => {
this.uploadToApi(nextState.items);
});
};
getPromptElement = () => {
const firstDuplicateItemToPrompt = this.getFirstDuplicateItemToPrompt();
const firstDuplicateItemIndexToPrompt = this.getFirstDuplicateItemIndexToPrompt();
return firstDuplicateItemToPrompt ? (
<MyPrompt
item={item}
index={firstDuplicateItemIndexToPrompt}
onAnswerSelect={this.onPromptAnswered}
/>
) : null;
};
getFirstDuplicateItemToPrompt = this.state.performUpload
&& !!this.state.items
.find(i => i.isDuplicate && !i.shouldUploadDuplicate);
getFirstDuplicateItemIndexToPrompt = this.state.performUpload
&& !!this.state.items
.findIndex(i => i.isDuplicate && !i.shouldUploadDuplicate);
onPromptAnswered = (accepted, item, index) => {
this.setState(prevState => ({
...prevState,
items: prevState.items
.map((i, key) => (index === key ? ({
...item,
shouldUploadDuplicate: accepted,
}) : item)),
performUpload: accepted, // if at last an item was rejected, then the upload won't be executed
}));
};
uploadToApi = (items) => {
if (!this.getFirstDuplicateItemToPrompt()) {
const itemsToUpload = items.filter(i => i.isValid);
uploadDataToApi(itemsToUpload);
}
};
render() {
const { items } = this.stat;
const itemElements = items.map((item, key) => (
<MyItem key={key} {...item} />
));
const promptElement = this.getPromptElement();
return (
<div>
<div style={{ display: 'flex', flexDirection: 'row' }}>
{itemElements}
</div>
<Button onClick={this.onUploadButtonClick}>Upload</Button>
{promptElement}
</div>
)
}
}

Related

Using setState inside a arrow function resets state

I am trying to modify a state when a users input fields on my dashboard is changed. This is how the handler is intended to work:
If the state is empty. Create a user with the standard values and change its values to the changed inputs
If the user exists in the state, change the changed field in the state to the new value.
If the user does not exist. Add the user to the state and change the changed field to the new value.
I am doing this by calling this function on a change of any inputs:
const handleInputChange = (event, person) => {
let new_form_val = {
objectId: person._id,
name: person.name,
role: person.role,
privilege: person.privilege,
group: person.group
};
console.log("handle change function called")
if (formValues.length == 0)
{
console.log("formValues is empty")
new_form_val[event.target.name] = event.target.value
console.log("adding", new_form_val)
setFormValues([...formValues, new_form_val])
}
// console.log(event.target.value)
console.log("Change target id", event.target.id)
console.log("current formvalue before change", formValues)
let form_val = formValues.find((item) => item.objectId == event.target.id)
if (form_val) {
console.log("person found in formValues", form_val)
let index = formValues.indexOf(form_val)
formValues[index][event.target.name] = event.target.value
console.log("Changed already existing formvalue", formValues)
setFormValues(formValues)
}
else {
new_form_val[event.target.name] = event.target.value
console.log("new person in form value", new_form_val)
setFormValues([...formValues, new_form_val])
}
}
Later on I am using that function as an onChange event handler
useEffect(() => {
// GARL: https: //bobbyhadz.com/blog/react-push-to-state-array
setPeople([])
console.log("get users effetct ran")
axios.get('/api/getusers').then((response) => {
response.data.forEach((item, index) => {
setPeople(oldStatusArray => {
return [...oldStatusArray, <Person
key={index}
id={index+1}
_id={item._id}
name={item.name}
role={item.role}
privilege_id={item.privilege}
group_id={item.group}
onChange={(event) => handleInputChange(event, item)}
/>]
})
});
})
}, []);
The problem I am facing though is whenever the onChange function is called. The whole formValues sate is reset and replaced with the new changed state. For exmpale: I change user A to a new name and role and the change is logged to the console. I also Change User B and then C to a new group. Finally the state only has the changes made from C.
Here is the full code:
import Link from 'next/link';
import axios from 'axios';
import React, { useState, useEffect } from "react";
import Person from '../components/person' // Not actually a import
const Dashboard = () => {
const [people, setPeople] = useState([]);
const [formValues, setFormValues] = useState([]);
const handleInputChange = (event, person) => {
let new_form_val = {
objectId: person._id,
name: person.name,
role: person.role,
privilege: person.privilege,
group: person.group
};
console.log("handle change function called")
if (formValues.length == 0)
{
console.log("formValues is empty")
new_form_val[event.target.name] = event.target.value
console.log("adding", new_form_val)
setFormValues([...formValues, new_form_val])
}
// console.log(event.target.value)
console.log("Change target id", event.target.id)
console.log("current formvalue before change", formValues)
let form_val = formValues.find((item) => item.objectId == event.target.id)
if (form_val) {
console.log("person found in formValues", form_val)
let index = formValues.indexOf(form_val)
formValues[index][event.target.name] = event.target.value
console.log("Changed already existing formvalue", formValues)
setFormValues(formValues)
}
else {
new_form_val[event.target.name] = event.target.value
console.log("new person in form value", new_form_val)
setFormValues([...formValues, new_form_val])
}
}
useEffect(() => {
setPeople([])
console.log("get users effetct ran")
axios.get('/api/getusers').then((response) => {
response.data.forEach((item, index) => {
setPeople(oldStatusArray => {
return [...oldStatusArray, <Person
key={index}
_id={item._id}
name={item.name}
role={item.role}
privilege_id={item.privilege}
group_id={item.group}
onChange={(event) => handleInputChange(event, item)}
/>]
})
});
})
}, []);
const submit = (values) => {
// Submits state to backend for handling
}
return (
<div id="main">
<h1>Administration</h1>
{(people.length == 0) ?
<h1>Laddar innehållet..</h1> : people }
</div>
);
}
export default Dashboard;
Here is the output after changing the input fields a couple of times:
>> handle change function called
>> formValues is empty
>> adding - Object { objectId: "634ea9b368bd856cebfdddc0", name: "RADICATED", role: "...", privilege: "634ff6d42c7b67c5708e901b", group: "634ff7322c7b67c5708e901d" }
>> change target id 634ea9b368bd856cebfdddc0
>> current formvalue before change - Array []
>> new person in form value - Object { objectId: "634ea9b368bd856cebfdddc0", name: "RADICATED", role: "....", privilege: "634ff6d42c7b67c5708e901b", group: "634ff7322c7b67c5708e901d" }
>> CURRENT formvalues - Array [ {…} ] (len: 1)
I have also tried to adding formValues as a dependency to useEffect however, this results in a rerender of the users if I change any of the inputs as the setPeople is called in the useEffect.
How can I achieve a handleInputChange function that works as intended without updating the renderer or reseting the state?
I noticed the step 1 and 3 are actually the same so I put those together. The itemExists check if the person is already in the state. If the state is empty itemExists is false and if the person does not exists itemExists is also false.
When false we just update the field and return the previous and the new new_form_val.
When true we loop over all the current values until we find the one we want to edit, and then update the field we want to update.
const handleInputChange = (event, person) => {
const new_form_val = {
objectId: person._id,
name: person.name,
role: person.role,
privilege: person.privilege,
group: person.group,
};
// check if the item already exists
const itemExists =
formValues.find((item) => item.objectId == event.target.id) !== undefined;
if (itemExists) {
setFormValues((prevFormValues) => {
// map current values
const newValues = prevFormValues.map((item) => {
// if its not the item we're editing just return the item
if (item.objectId !== event.target.id) return item;
// if it is, update the item
const updatedItem = {
...item,
[event.target.name]: event.target.value,
};
return updatedItem;
});
return newValues;
});
} else {
// update the field with the new value
new_form_val[event.target.name] = event.target.value;
// add to the values
setFormValues((prevFormValues) => [...prevFormValues, new_form_val]);
}
};
I also updated the way the people were set. Now we first loop over all the data received from the api and create an array of Person components and set that array to the state, instead of setting the state for every result in the api data.
useEffect(() => {
// no need to set the people to an empty array since the default state is already an empty array
// setPeople([]);
console.log("get users effetct ran");
axios.get("/api/getusers").then((response) => {
const peopleFromApi = response.data.map((item, index) => (
<Person
key={index}
_id={item._id}
name={item.name}
role={item.role}
privilege_id={item.privilege}
group_id={item.group}
onChange={(event) => handleInputChange(event, item)}
/>
));
setPeople(peopleFromApi);
});
}, []);
I hope this helps you continue your project!

Use of prevState to change value in array of objects does not return expected value

I am using the following method to try to change the value of task.isComplete to !task.isComplete onClick.
handleComplete = (event) => {
event.preventDefault();
this.setState(
(prevState) => ({
listOfTasks: prevState.listOfTasks.map((task) => {
if (task.id === event.target.id) {
task.isComplete = !task.isComplete;
console.log(task);
}
return task;
}),
}),
() => console.log(this.state.listOfTasks)
);
};
when clicking the button the 2 logs are:
{nameOfTask: "aaaa", isComplete: true, id: "1610746018062"}
TodoListTest.js:54
[{…}]
0: {nameOfTask: "aaaa", isComplete: false, id: "1610746018062"}
length: 1
__proto__: Array(0)
React seems to only consider the second log state so I don't get the expected change, incomplete turning to true at the end of the operation.
you need to make a shallow copy of the task object to update its isComplete property.
handleComplete = (event) => {
event.preventDefault();
this.setState(
(prevState) => ({
listOfTasks: prevState.listOfTasks.map((task) => {
if (task.id === event.target.id) {
return {
...task,
isComplete: !task.isComplete
};
}
return task;
})
}),
() => console.log(this.state.listOfTasks)
);
};

Expected to find a valid target react dnd

I am experiencing this error with react dnd. The weird thing is that it depends on the key i specify to my react component. if i specify index, one part of my function fires this error, and when i specify item.id, another part doesnt fire. it doesnt make sense. please help.
When I specify the key to be index, the error fires when forum has no parent. however when i specify the key to be forum._id, the error fires when forum has parent. i dont know what to do, please help :)
Please visit this sandbox to reproduce:
https://codesandbox.io/s/proud-wind-hklt6
To reproduce:
Drag item 1ba on top of item 1, and then drag the item 1ba down the path.
Forum.jsx
const Forum = ({ forum, forums, setForums, move, find }) => {
const [{ isOver, canDrop }, drop] = useDrop({
accept: "forum",
hover: throttle((item, monitor) => {
if (item._id === forum._id) {
return;
}
if (!monitor.isOver({ shallow: true })) {
return;
}
if (!canDrop) return;
move(item, forum, forum.parent);
item = forum;
}, 200),
collect: (monitor) => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
}),
});
const [, drag, preview] = useDrag({
item: {
_id: forum._id,
title: forum.title,
type: "forum",
children: forum.children,
parent: forum.parent,
},
isDragging(props, monitor) {
return props._id == monitor.getItem()._id;
},
});
const getChildren = async (forumId) => {
const _forums = await ForumService.getChildren(forumId, forums);
setForums(_forums);
};
return (
<Wrapper ref={drop}>
<ForumContainer ref={drag}>
{!!forum.childrenIds?.length && (
<div>
{!forum.isOpen ? (
<ForumChevron
className="fas fa-chevron-down"
onClick={() => getChildren(forum._id)}
></ForumChevron>
) : (
<ForumChevron
className="fas fa-chevron-up"
onClick={() =>
setForums(ForumService.resetChildren(forum._id, forums))
}
></ForumChevron>
)}
</div>
)}
<ForumTitle>{forum.title}</ForumTitle>
</ForumContainer>
{forum.children && !!forum.children.length && (
<ForumChildrenWrapper>
{forum.children.map((child, index) => (
<Forum
forum={child}
setForums={setForums}
forums={forums}
key={index}
move={move}
find={find}
/>
))}
</ForumChildrenWrapper>
)}
</Wrapper>
);
};
export default Forum;
ForumManager.jsx
if (!item.parent) {
console.log("here 1");
const dest = findItem(afterItem._id, _forums);
if (!dest.children) dest.children = [];
foundItem.parent = afterItem._id;
const idx = _forums.findIndex((f) => f._id === item._id);
_forums.splice(idx, 1);
if (dest.parent === foundItem._id) {
dest.parent = "";
if (foundItem.children.length) {
// When key is item.id, error shows up here
console.log("parent & has children");
for (let child of [...foundItem.children]) {
if (child._id === dest._id) {
child.children.splice(0, 0, {
...foundItem,
children: [],
childrenIds: [],
});
}
_forums.push(child);
}
} else {
console.log("no children");
dest.children.unshift({
...foundItem,
children: [],
childrenIds: [],
});
}
} else {
// When key is index, error shows up here
console.log("no parent");
console.log(dest);
dest.parent = "";
dest.children.splice(0, 0, {
...foundItem,
children: [],
childrenIds: [],
});
}
}
Try adding debounce to the hover handler (with trailing option). The components are updating too quickly by setting the state before DnD could catch up, and the target ID had changed by the time the user dropped the item.
Also - don't use index as the key, as it will change each time.
If you remove monitor.canDrop() inside collect function, then it works. Not sure, but this is one way.

React set state error outside of componentDidMount

Im getting an error Unhandled Rejection (Error): Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops. when I try to set state in my retrieveRoleMembers function not sure how to fix it, any feedback is appreciated!
class MainCard extends Component {
state = {
userResponseData:[] ,
roleResponseDataID:[]
}
handleChange = (tab) => {
window.alert(`Tab changed to: ${tab}`);
};
retrieveRoleMembers(){
var i,j;
for (i = 0; i < this.props.userRoleDataValue.length; i++) {
if(this.props.userRoleDataValue[i].role_id === this.state.roleResponseDataID.id){
for(j=0;j<this.state.userResponseData.length;j++){
if(this.props.userRoleDataValue[i].user_id === this.state.userResponseData[j].id)
{
this.setState({ // This is where the error is happening
outputRoleMembers: this.state.userResponseData[j],
})
}
}
}}}
componentDidMount() {
this.props.getComponentById(VIEW_ROLES, Roles, this.props.searchValue.value).then(() => {
return this.setState({
roleResponseDataID: this.props.roles.data,
cardHandle: false,
})
});
this.props.fetchComponent([IS_FETCHING_DBUSERS, FETCH_DBUSERS_SUCCESS], users)
.then(() => {
return this.setState({
userResponseData: this.props.users.data,
})
});
}
render() {
if (this.props.cardHandle) {
return null
}
else {
if (this.props.sendOptionSelected === 'Role') {
this.retrieveRoleMembers()
return (
<Card mr={'0px'}>
<Tabs defaultActiveTab="Members" onChange={this.handleChange} >
{/* Group of tabs */}
<Tabs.Tab label="Members">Members</Tabs.Tab>
<Tabs.Tab label="Access">Access</Tabs.Tab>
{/* Tab panels */}
<Tabs.Panel label="Members">
<Table
data={Array.isArray(this.state.outputRoleMembers) ? this.state.outputRoleMembers : [this.state.outputRoleMembers]}
defaultPageSize={[this.state.outputRoleMembers].length}
columns={
[
{
Header: 'Fisrt Name',
accessor: 'first_name'
},
{
Header: 'Last Name',
accessor: 'last_name'
}
]
}
sortable={false}
resizable={false}
showPagination={false}
onSortedChange={() => { }}
/>
</Tabs.Panel>
</Tabs>
</Card>
)
}
}
}
const mapStateToProps = state => {
return {
roles: state.roles.item,
users: state.users
}
}
export default connect(mapStateToProps, { getComponentById,fetchComponent })(MainCard);
and when I change retrieveRoleMembers to look like so, my code works but when I inspect the console log I see a infinite loop / renders for VIEW_DBUSERS
retrieveRoleMembers(){
var i;
for (i = 0; i < this.props.userRoleDataValue.length; i++) {
if(this.props.userRoleDataValue[i].role_id === this.state.roleResponseDataID.id){
this.props.getComponentById(VIEW_DBUSERS, users, this.props.userRoleDataValue[i].user_id).then(() => {
return this.setState({
outputRoleMembers: this.props.users.data,
})
});
}}}
The problem is you are calling function inside render method. That sets the State and calls the render method again and so on. So it created a loop.
Hence you get
Unhandled Rejection (Error): Maximum update depth exceeded
I put everything inside componentDidMount by making an async function.
componentDidMount() {
this.preFetchData();
}
preFetchData async () { // made this async function.. using await to make code sync
await this.props.getComponentById(VIEW_ROLES, Roles, this.props.searchValue.value);
await this.props.fetchComponent([IS_FETCHING_DBUSERS, FETCH_DBUSERS_SUCCESS], users);
this.setState({ roleResponseDataID: this.props.roles.data, cardHandle: false, userResponseData: this.props.users.data }, () => {
this.retrieveRoleMembers(); // call your method here
});
}

React checkboxes. State is late when toggling the checkboxes

I have a group of 3 checkboxes and the main checkbox for checking those 3 checkboxes.
When I select all 3 checkboxes I want for main checkbox to become checked.
When I check those 3 checkboxes nothing happens but when I then uncheck one of those trees the main checkbox becomes checked.
Can someone explain to me what actually is happening behind the scenes and help me somehow to solve this mystery of React state? Thanks!
Here is a code snnipet:
state = {
data: [
{ checked: false, id: 1 },
{ checked: false, id: 2 },
{ checked: false, id: 3 }
],
main: false,
}
onCheckboxChange = id => {
const data = [...this.state.data];
data.forEach(item => {
if (item.id === id) {
item.checked = !item.checked;
}
})
const everyCheckBoxIsTrue = checkbox.every(item => item === true);
this.setState({ data: data, main: everyCheckBoxIsTrue });
}
onMainCheckBoxChange = () => {
let data = [...this.state.data];
data.forEach(item => {
!this.state.main ? item.checked = true : item.checked = false
})
this.setState({
this.state.main: !this.state.main,
this.state.data: data,
});
}
render () {
const checkbox = this.state.data.map(item => (
<input
type="checkbox"
checked={item.checked}
onChange={() => this.onCheckboxChange(item.id)}
/>
))
}
return (
<input type="checkbox" name="main" checked={this.state.main} onChange={this.onMainCheckBoxChange} />
{checkbox}
)
I can't make a working code snippet based on the code you provided, one of the issues was:
const everyCheckBoxIsTrue = checkbox.every(item => item === true);
where checkbox is not defined.
However, I think you confused about using the old state vs the new state, it'd be simpler to differentiate if you name it clearly, e.g.:
eventHandler() {
const { data } = this.state; // old state
const newData = data.map(each => ...); // new object, soon-to-be new state
this.setState({ data }); // update state
}
Here's a working example for your reference:
class App extends React.Component {
state = {
data: [
{ checked: false, id: 1 },
{ checked: false, id: 2 },
{ checked: false, id: 3 }
],
main: false,
}
onCheckboxChange(id) {
const { data } = this.state;
const newData = data.map(each => {
if (each.id === id) {
// Toggle the previous checked value
return Object.assign({}, each, { checked: !each.checked });
}
return each;
});
this.setState({
data: newData,
// Check if every checked box is checked
main: newData.every(item => item.checked === true),
});
}
onMainCheckBoxChange() {
const { main, data } = this.state;
// Toggle the previous main value
const newValue = !main;
this.setState({
data: data.map(each => Object.assign({}, each, { checked: newValue })),
main: newValue,
});
}
render () {
const { data, main } = this.state;
return (
<div>
<label>Main</label>
<input
type="checkbox"
name="main"
// TODO this should be automatically checked instead of assigning to the state
checked={main}
onChange={() => this.onMainCheckBoxChange()}
/>
{
data.map(item => (
<div>
<label>{item.id}</label>
<input
type="checkbox"
checked={item.checked}
onChange={() => this.onCheckboxChange(item.id)}
/>
</div>
))
}
</div>
);
}
}
ReactDOM.render(
<App />
, document.querySelector('#app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="app"></div>
Side note: You might want to consider not to use the main state
You shouldn't be storing state.main to determine whether every checkbox is checked.
You are already storing state that determines if all checkboxes are checked, because all checkboxes must be checked if every object in state.data has checked: true.
You can simply render the main checkbox like this:
<input
type="checkbox"
name="main"
checked={this.state.data.every(v => v.checked)}
onChange={this.onMainCheckBoxChange}
/>;
The line this.state.data.every(v => v.checked) will return true if all of the checkboxes are checked.
And when the main checkbox is toggled, the function can look like this:
onMainCheckBoxChange = () => {
this.setState(prev => {
// If all are checked, then we want to uncheck all checkboxes
if (this.state.data.every(v => v.checked)) {
return {
data: prev.data.map(v => ({ ...v, checked: false })),
};
}
// Else some checkboxes must be unchecked, so we check them all
return {
data: prev.data.map(v => ({ ...v, checked: true })),
};
});
};
It is good practice to only store state that you NEED to store. Any state that can be calculated from other state (for example, "are all checkboxes checked?") should be calculated inside the render function. See here where it says:
What Shouldn’t Go in State? ... Computed data: Don't worry about precomputing values based on state — it's easier to ensure that your UI is consistent if you do all computation within render(). For example, if you have an array of list items in state and you want to render the count as a string, simply render this.state.listItems.length + ' list items' in your render() method rather than storing it on state.

Resources