Updating parent component only after multiple child components have completed running - javascript

I have a Parent react component with multiple child components that are created through a .map() function. I am passing in a function addCallback() as child props so I have a reference and can trigger all child's handleRun() function via the Parent.
I'm trying to update state of my Parent component to running = true when all children are running and to running = false and render said status on the parent when all children have completed running. However the state doesn't seem to update in the particular sequence I specify.
Here is how I'm doing it:
let promise1 = this.setState({isRunning: true},
() => {
this.state.childRef.map(x => x())
});
Promise.all([promise1])
.then(() => this.setState({isRunning: false}))
Here's the entire code in codesandbox: link
Would appreciate your help as I'm still pretty new to React (and Javascript in general). Thanks!

Cause runSomething is not a Promise. You must change.
runSomething() {
return new Promise((resolve, reject) => {
this.setState({ status: "running" });
// simulate running something that takes 8s
setTimeout(() => {
this.setState({ status: "idle" });
resolve(true);
}, 3000);
});
}
A working sandbox here https://codesandbox.io/s/fragrant-cloud-5o2um

Using async in a function declaration automatically returns a Promise wrapped around whatever you are returning from your function. In your case, it's undefined. This is why your current code is not throwing any errors at the moment.
You will need a mechanism to wait for the setTimeout. Changing the runSomething function like this will work
async runSomething() {
this.setState({ status: "running" });
// simulate running something that takes 8s
return new Promise(resolve => {
setTimeout(() => {
this.setState({ status: "idle" }, resolve);
}, 3000);
});
}
Do notice the line this.setState({ status: "idle" }, resolve);. It makes sure that your promise resolves not only after the setTimeout but also after the child's state is changed to "idle". Which is the correct indication that your child component has moved to "idle" state.
Codesandbox: https://codesandbox.io/s/epic-boyd-12hkj

Here is the sandbox implementation of what you are trying to achieve. Sanbox
Here i have created a state in parent component that will be updated when child is running.
this.state = {
callbacks: [],
components: [
{
index: 0, // we don't need this field its just for your info you can just create [true,false] array and index will represent component index.
status: false
},
{
index: 1,
status: false
}
]
};
When all the status in component array is true we update the idle status of parent to running.
getAllRunningStatus() {
let { components } = this.state;
let checkAllRunning = components.map(element => element.status);
if (checkAllRunning.indexOf(false) === -1) { // you can also use !includes(false)
return true;
}
return false;
}
inside your render function
<h1>Parent {this.getAllRunningStatus() ? "running" : "idle"}</h1>
Note:- I have just written a rough code. You can optimise it as per your requirements. Thanks

Related

Update state value of single object in an array

I have list of items where I want to display a loader and hide it upon completing certain action.
For example, here is my array items
[
{
"id": "69f8f183-b057-4db5-8c87-3020168307c5",
"loading": null
},
{
"id": "30d29489-0ba9-4e00-bc28-8ad34ff1a285",
"loading": true
},
{
"id": "5f54ebbd-d380-4a54-bb1d-fc6c76dd1b72",
"loading": false
}
]
I am adding item to array with loading value as null the reason is. I want to process as soon as the the state is updated, hence I am using useEffect hook to observe for any change, if any new item with loading value null is added, then I proceed for action.
My problem is, when I try to modify a single loading value to false, it gives me weird behaviour and set all loading value to false.
What I want to have it, when I change the loading value of a single item in array, then the UI should re-render only for the changed item.
If you want to have a look at fiddle with working example, here is the link https://codesandbox.io/s/d8lh4-d8lh4
Where am I going wrong here?
It's simple use this code:
setTimeout(() => {
setItems((existingItems) =>
existingItems.map((item) =>
item.id === newItem?.id ? { ...item, loading: false } : item
)
);
}, 2000);
Looking at your code, I think the issue is related to accessing the wrong value of newItem and items in setTimeout. Both of them can be solved by doing something similar to the one below.
const handleUpload = newItem => {
// set loading to false to new item after 2 seconds
setTimeout(
theNewItem => {
setItems(exisitingItems =>
exisitingItems.map(item =>
item.id === theNewItem.id ? { ...item, loading: false } : theNewItem,
),
);
},
2000,
newItem,
);
};
You have [items] dependency in your useEffect, which is calling setItems in loop in your handleUpload function.

React: Realtime rendering Axios return

hope you're well. I've been working on a company list component. I am having a problem with updating it in real time, because the axios call I'm sending to 'getById' after it renders is returning only a promise and not the actual data that it is supposed to and I don't have any idea as to why. So when I push the so called new company that I've just added into the array, which is in state, it is only pushing a promise into the array and not the actual Company. I don't have any idea why this is. What the code is supposed to be doing, is it is supposed to be putting the new company into the database, returning the success result, and then I'm using the item from that to make a fresh get call to the axios DB which is supposed to be returning the information I just entered so that I can then insert it into the same array in state that is within the list that is rendered in the company list. However, as I mentioned, only the promise is coming up for some reason.
At one point I was able to get this working, but I did that by essentially calling, 'componentDidMount' after the promise was pushed into the call back clause of the setState funciton of the push function - which was essentially causing the entire component to re-render. I'm a fairly new coder, but my understanding is is that that is a fairly unconventional way to code something, and contrary to good coding methodologies. I believe I should be able to push it into state, and then have it change automatically. I have attached the relevant code below for you to examine. If you believe you need more please let me know. If someone could please tell me why I am getting this weird promise instead of the proper response object, so that I can then insert that into state, I would greatly appreciate it. I've attached some images below the code snippets that I hope will be helpful in providing an answer. I have also left brief descriptions of what they are.
:
class Companies extends React.Component {
constructor(props) {
super(props);
this.state = {
Companies: [],
formData: { label: "", value: 0 },
};
}
componentDidMount = () => {
this.getListCompanies();
};
getListCompanies = () => {
getAll().then(this.listOfCompaniesSuccess);
};
listOfCompaniesSuccess = (config) => {
let companyList = config.items;
this.setState((prevState) => {
return {
...prevState,
Companies: companyList,
};
});
};
onCompListError = (errResponse) => {
_logger(errResponse);
};
mapCompanies = (Companies) => (
<CompaniesList Companies={Companies} remove={remove} />
);
handleSubmit = (values) => {
if (values.companyName === "PPP") {
this.toastError();
//THIS IS FOR TESTING.
} else {
add(values)
.getById(values.item) //I HAVE TRIED IN TWO DIFFERENT PLACES TO GET THE NEW COMPANY IN. HERE
.then(this.newCompanyPush)
.then(this.toastSuccess)
.catch(this.toastError);
}
};
//THIS CODE RIGHT HERE IS THE CODE CAUSING THE ISSUE.
newCompanyPush = (response) => {
let newCompany = getById(response.item); // THIS IS THE OTHER PLACE I HAVE TRIED.
this.setState((prevState) => {
let newCompanyList = [...prevState.Companies];
newCompanyList.push(newCompany);
return {
Companies: newCompanyList,
};
});
};
toastSuccess = () => {
toast.success("Success", {
closeOnClick: true,
position: "top-right",
});
};
toastError = (number) => {
toast.error(`Error, index is ${number}`, {
closeOnClick: true,
position: "top-center",
});
};
This is the axios call I am using.
const getById = (id) => {
const config = {
method: "GET",
url: companyUrls + id,
withCredentials: true,
crossdomain: true,
headers: { "Content-Type": "application/json" },
};
return axios(config).then(onGlobalSuccess).catch(onGlobalError);
};
After the promise is pushed into the array, this is what it looks like. Which is I guess good news because something is actually rendering in real time.
This is the 'promise' that is being pushed into the array. Please note, when I make the same call in post-man, I get an entirely different response, see below.
This is the outcome I get in postman, when I test the call.

React setState of boolean value not updating

New to React, trying to update the state of an object where on property already has a set boolean value. However, it seems like the state is not updating.
I understand that state is update asynchronously, maybe that could coming into play here? I don't believe I can use the setState method that takes an object and callback function because I need access the the previous state.
Here is my initial state:
items: [
{
id: 0,
title: 'Dev Grub',
selected: false
},
...
]
And here is my event handler:
handleCardClick(id, card) {
this.setState((preState, props) => ({
[preState.items[id].selected]: [preState.items[id].selected] ? false : true
}));
console.log('new state: ', this.state.items[id].selected);
}
I've also tried this instead of the ternary: ![card.selected]
updating just a property at the second level of the state won't work. use something like below:
handleCardClick(id, card) {
let items = [...state.items];
items[id].selected = items[id].selected ? false : true
this.setState(() => ({
items
}));
}
React setState doesn't work this way, it doesn't update state right away but rather enqueues the change to update it at some point in the future.
If you want to do something as soon as the state has been updated you can use callback parameter
this.setState((preState, props) => ({
[preState.items[id].selected]: [preState.items[id].selected] ? false : true
}), () => console.log('new state: ', this.state.items[id].selected);)
See docs on setState
setState is async, you console log after the state has been changed like ths
handleCardClick = (id, card) => {
this.setState(
{
[this.state.items[id].selected]: [this.state.items[id].selected]
? false
: true,
},
() => console.log('new state: ', this.state.items[id].selected),
);
};

I have to press a button twice to fill react state from firebase realtime database

In my react component, when the component mounts, I want to run the loadData() function to populate an array of people objects in the state.
This component is called UserDisplaySection.js
componentDidMount() {
this.loadData();
}
Which calls the function to load data:
loadData = () => {
const db = fire.database();
const ref = db.ref('User');
let currentState = this.state.people;
ref.on('value', (snapshot) => {
snapshot.forEach( (data) => {
const currentStudent = data.val();
let user ={
"email" : currentStudent.Email,
"name": currentStudent.Name,
"age": currentStudent.Age,
"location": currentStudent.Location,
"course": currentStudent.Course,
"interests": currentStudent.Interests
}
currentState.push(user);
});
});
this.setState({
people: currentState
});
}
From another component called Home.js, I render UserDisplaySection.js with the toggle of a button which sets the state of a boolean called willDisplayUser which is initialised as false by default.
handleDisplayUsers = e => {
e.preventDefault();
this.setState({
willDisplayUsers: !(this.state.willDisplayUsers),
});
}
render() {
return (
<div>
<button className="mainButtons" onClick={this.handleDisplayUsers}>View Users</button> <br />
{this.state.willDisplayUsers ? <UserDisplaySection/>: null}
</div>
);
}
Note: The function works and populates the state with the correct data, but only after I render the UserDisplaySection.js component for a second time and works perfectly until I reload the page.
By doing ref.on() you are actually declaring a listener that "listens for data changes at a particular location", as explained in the doc. The "callback will be triggered for the initial data and again whenever the data changes". In other words it is disconnected from your button onClick event.
If you want to fetch data on demand, i.e. when your button is clicked, you should use the once() method instead, which "listens for exactly one event of the specified event type, and then stops listening".
The root of the problem is that you are setting state outside of the ref.on callback.
Data fetch is async and the callback will be executed later in time. Since set state is outside the callback, it gets called called immediately after registering a listener, but before the data finished fetching. That's why it does not work on initial render/button click: it shows initial state, not waiting for the fetch to finish.
Also, using once instead of on might serve better for your case.
loadData = () => {
const db = fire.database();
const ref = db.ref('User');
let currentState = this.state.people;
ref.once('value', (snapshot) => {
// callback start
snapshot.forEach( (data) => {
const currentStudent = data.val();
let user ={
"email" : currentStudent.Email,
"name": currentStudent.Name,
"age": currentStudent.Age,
"location": currentStudent.Location,
"course": currentStudent.Course,
"interests": currentStudent.Interests
}
currentState.push(user);
});
this.setState({
people: currentState
});
// callback end
});
// setState used to be here, outside of the callback
}

Put calls in a queue and waiting for updating the state in React.js

I've got a function that I run in the forEach loop in the child component, the function has to filter the data and update the state with the result.
The problem is that when I iterate the function with a loop it running all at the same time and within this, the state will not update properly with data.
The question is how to implement it better, probably there is a way with Promise or async /await or maybe something simpler that will make a work. As needed to put in the queue and wait until the state will be updated.
Simplified code is here
component child
this.props.data.forEach((item, i) => {
this.props.update(item);
});
component parent
function update(data) {
let filtered = this.state.data.filter(item => item.uid !== data.uid);
this.setState({data: filtered});
}
If i understand well you need something like this:
update = () => {
let filtered = this.state.data.filter(x => this.props.data.every(y => x.uid !== y.uid))
this.setState({
data: filtered
})
}
Why not iterating through your array in your parent component?
Child:
this.props.update(this.props.data); // pass the entire array
Parent:
function update(data) {
let filtered = data.map(d=> {
return this.state.data.filter(item => item.uid !== d.uid);
}
this.setState({data: filtered});
}

Categories

Resources