why setState hook in the map set last object only? - hook

I have 2 components in react-admin the child one set the state of the parent one.
the child component has useEffect to trigger the following method in parent component
const [approved, setAproved] = useState([]);
useEffect(() => {
console.log(JSON.stringify(approved))
}, [approved])
const setapprovedamount = (id, approvedAmount) => {
if(approved.length!==0)
{
// if the child run 3 times
// this line runs 3 times
console.log("set Aproved with" +id+ " and amount : "+approvedAmount)
// but this line update only the last object
setAproved(
approved.map(item =>
(item.id == id)
? {...item, totalApproved : approvedAmount}
: item
)
)
}
else
{
setAproved(approved =>[...approved, {
id: id,
totalApproved: approvedAmount
}] )
}
}
useEffect(() => {
console.log(JSON.stringify(approved))
}, [approved])
So **for example** if I have 3 times load, the Console is:
file.js:168 set Aproved with44 and amount : 799.71
file.js:168 set Aproved with45 and amount : 845.98
file.js:168 set Aproved with46 and amount : 890.83
file.js:96
[{"id":44,"totalApproved":null},{"id":45,"totalApproved":null},{"id":46,"totalApproved":890.83}]
so the method runs 3 times as it is triggered but, it is setting only the last value rendered in the state array in the parent component

Because of how State and Lifecycle works.
If you wanted to fire multiple and chained state updates throughsetState, you need to let the component complete its cycle at each state update - first things first.
Therefore, the basic solution is to imply the use of chained setTimeout calls to delegate each state update, letting each function call complete a component, full life cycle.
setTimeout(function() {
setState(firstState)
setTimeout(function() {
setState(secondState)
});
});

Related

Dynamic Table updates "too late" ReactJS

my problem is, that I have a table which should update everytime when the user chooses something from a dropdown component. The problem now is that my table updates "too late" in the frontend. So when the user chooses an option for the first time nothing will happen. Then when the user chooses an option for the second time from the dropdown component, the table will show the data from the option he has picked before. If the user chooses an option for the 3rd time, the table will show the data from the second one and so on.
So how can I fix this? I work with ReactJS and Semantic UI
My Code:
This renders the Row for the existing data
renderTableData() {
return this.state.songs.map((song, index) => {
const { id, nr, songname, link } = song
return (
<Table.Row key={id}>
<Table.Cell>{nr}</Table.Cell>
<Table.Cell>{songname}</Table.Cell>
<Table.Cell>{link}</Table.Cell>
</Table.Row>
)
})
}
The Code in the main render() function of React (Its shown correctly, expect that the data is "outdated":
`<Table>
<Table.Header>
<Table.Row>
<Table.HeaderCell width={1}>Nr</Table.HeaderCell>
<Table.HeaderCell width={2}>Songname</Table.HeaderCell>
<Table.HeaderCell width={1}>Link</Table.HeaderCell>
</Table.Row>
</Table.Header>
{this.renderTableData()}
</Table>`
The code when the option from the dropdown gets changed:
onChangeDropdown(e) {
this.setState({game: e.target.textContent}, ()=>{
this.state.songs.length = 0;
for(var i = 0; i< this.state.musicData.length;i++){
if(this.state.musicData[i].game == this.state.game){
for(var j = 0; j<this.state.musicData[i].songs.length;j++){
this.state.songs.push({id: j+1, nr: j+1, songname: this.state.musicData[i].songs[j].name, link: this.state.musicData[i].songs[j].link})
}
break;
}
}
this.renderTableData()
})
}
The game variable in this.setState is correct and also the for-loop works as expected when the user changes the dropdown option, I already checked it with the debugger
I hope you can help me out there, ty
is not that is updating too late, is that you are mutating the state without using setState so React doesn't know what changed, you should refactor your code to always use setState to update the state, not push, something like this:
onChangeDropdown(e) {
this.setState((currentState) => {
const newSongs = [];
const game = e.target.textContent;
musicData.forEach((data) => {
if (data.game === game) {
musicData.songs.forEach((song, index) => {
newSongs.push({
id: index + 1,
nr: index + 1,
songname: song.name,
link: song.link,
});
});
}
});
return {
...currentState,
game,
songs: newSongs,
};
});
}
I changed your for loops to use forEach, less complexity, easier to read
Here is what I did:
create a empty array to store the selected songs (newSongs)
loop all the music data and then loop all the songs inside each item in music data
add the songs from the selected game into newSongs
return newSongs + game to update the selected game, ...currentState is to preserve the other parts of the state between changes
So every time the dropodown changes, I create a new array and run the logic
The setState callback can return an object to replace whole state, so before that you can do any calculation you need to.
Updating the state in React is asyncronous, that's one of the reasons you can't mutate the state directly and need to use setState any time you need to update it

Chrome Extension | Multiple alarms going off at once

I am creating a task reminder extension. The user has an option to keep adding tasks and set reminders for each task.
I am using chrome.storage to store these tasks and using onChanged listener on storage to create an alarm for each task added to the storage.
But the issue is that if I set a reminder of 2 mins for a task and 3 mins for another task. Then at the end of 2 mins I am getting notification for both the tasks and at the end of 3mins I again get notifications for both the tasks.
background.js
chrome.storage.onChanged.addListener(function(changes, namespace) {
let id = (changes.tasks.newValue.length)-1
let data = changes.tasks.newValue[id]
if(data.task && data.hrs && data.min){
let totalMins = (parseInt(data.hrs*60))+parseInt(data.min)
let alarmTime = 60*1000*totalMins
chrome.alarms.create("remind"+id,{when:Date.now() + alarmTime})
}
chrome.alarms.onAlarm.addListener(()=>{
let notifObj = {
type: "basic",
iconUrl:"./images/logo5.png",
title: "Time to complete you task",
message: data.task
}
chrome.notifications.create('remindNotif'+id, notifObj)
})
popup.js
let hrs = document.querySelector("#time-hrs")
let min = document.querySelector("#time-min")
let submitBtn = document.querySelector("#submitBtn")
let task = document.querySelector("#task")
hrs.value = 0;
min.value = 1
hrs.addEventListener('change',()=>{
if (hrs.value < 0){
hrs.value =0;
}
})
min.addEventListener('change',()=>{
if (min.value < 1){
min.value = 1;
}
})
submitBtn.addEventListener("click", ()=>{
if(task.value){
chrome.storage.sync.get('tasks',(item)=>{
let taskArr = item.tasks ? item.tasks : []
linkArr.push({task:task.value, hrs:hrs.value, min:min.value})
chrome.storage.sync.set({ 'tasks' : taskArr })
})
};
});
manifest.json
{
"name" : "Link Snooze",
"description" : "This extension reminds you to open your saved links",
"manifest_version":2,
"version":"0.1.0",
"icons":{
"16":"./images/logo5.png",
"48":"./images/logo5.png",
"128":"./images/logo5.png"
},
"browser_action":{
"default_popup":"popup.html",
"default_icon":"./images/logo5.png"
},
"permissions":["storage", "notifications","alarms"],
"background" : {
"scripts": ["background.js"],
"persistent" : false
},
"options_page":"options.html"
}
Problem.
You register a new onAlarms listener when the storage changes in addition to the old listeners. All of them run each time one alarm is triggered.
Solution.
When using a non-persistent background script, all API listeners must be registered just once for the same function and it must be done synchronously, not inside an asynchronous callback or await or then(), otherwise the event will be lost when the background script auto-terminates and then wakes up for this event. The convention is to do it at the beginning of the script. The reason it worked for you until now is that the background script is kept alive while the popup is open or while devtools for the background script was open.
Such listeners evidently won't be able to use the variables from an asynchronous callback directly like data.task in your code. The solution is to use a different method of attaching data to an event, for example, create the alarm with a name that already contains the data, specifically data.task.
chrome.alarms.create(data.task, {delayInMinutes: hrs * 60 + min});
onAlarm event provides the alarm as a parameter so you can use its name, see the documentation.
Random hints:
An object can be used as an alarm name if you call JSON.stringify(obj) when creating and JSON.parse(alarm.name) in onAlarm.
In the popup, instead of manually adjusting out-of-range values, use a number input in html:
<input id="time-min" type=number min=0 max=59 step=1>
Then read it as a number: document.querySelector("#time-min").valueAsNumber || 0

React Native: Reach-Navigation and Pouch-DB - db.put not done before "refresh" callback is run

Relative newbie; forgive me if my etiquette and form here aren't great. I'm open to feedback.
I have used create-react-native-app to create an application using PouchDB (which I believe ultimately uses AsyncStorage) to store a list of "items" (basically).
Within a TabNavigator (main app) I have a StackNavigator ("List screen") for the relevant portion of the app. It looks to the DB and queries for the items and then I .map() over each returned record to generate custom ListView-like components dynamically. If there are no records, it alternately displays a prompt telling the user so. In either case, there is an "Add Item" TouchableOpacity that takes them to a screen where they an add a new item (for which they are taken to an "Add" screen).
When navigating back from the "Add" screen I'm using a pattern discussed quite a bit here on SO in which I've passed a "refresh" function as a navigation param. Once the user uses a button on the "Add" screen to "save" the changes, it then does a db.post() and adds them item, runs the "refresh" function on the "List screen" and then navigates back like so:
<TouchableOpacity
style={styles.myButton}
onPress={() => {
if (this.state.itemBrand == '') {
Alert.alert(
'Missing Information',
'Please be sure to select a Brand',
[
{text: 'OK', onPress: () =>
console.log('OK pressed on AddItemScreen')},
],
{ cancelable: false }
)
} else {
this.createItem();
this.props.navigation.state.params.onGoBack();
this.props.navigation.navigate('ItemsScreen');
}
}
}
>
And all of this works fine. The "refresh" function (passed as onGoBack param) works fine... for this screen. The database is called with the query, the new entry is found and the components for the item renders up like a charm.
Each of the rendered ListItem-like components on the "List screen" contains a react-native-slideout with an "Edit" option. An onPress for these will send the user to an "Item Details" screen, and the selected item's _id from PouchDB is passed as a prop to the "Item Details" screen where loadItem() runs in componentDidMount and does a db.get(id) in the database module. Additional details are shown from a list of "events" property for that _id (which are objects, in an array) which render out into another bunch of ListItem-like components.
The problem arises when either choose to "Add" an event to the list for the item... or Delete it (using another function via [another] slideout for these items. There is a similar backward navigation, called in the same form as above after either of the two functions is called from the "Add Event" screen, this being the "Add" example:
async createEvent() {
var eventData = {
eventName: this.state.eventName.trim(),
eventSponsor: this.state.eventSponsor.trim(),
eventDate: this.state.eventDate,
eventJudge: this.state.eventJudge.trim(),
eventStandings: this.state.eventStandings.trim(),
eventPointsEarned: parseInt(this.state.eventPointsEarned.trim()),
};
var key = this.key;
var rev = this.rev;
await db.createEvent(key, rev, eventData);
}
which calls my "db_ops" module function:
exports.createEvent = function (id, rev, eventData) {
console.log('You called db.createEvent()');
db.get(id)
.then(function(doc) {
var arrWork = doc.events; //assign array of events to working variable
console.log('arrWork is first assigned: ' + arrWork);
arrWork.push(eventData);
console.log('then, arrWork was pushed and became: ' + arrWork);
var arrEvents = arrWork.sort((a,b)=>{
var dateA = new Date(a.eventDate), dateB = new Date(b.eventDate);
return b.eventDate - a.eventDate;
})
doc.events = arrEvents;
return db.put(doc);
})
.then((response) => {
console.log("db.createEvent() response was:\n" +
JSON.stringify(response));
})
.catch(function(err){
console.log("Error in db.createEvent():\n" + err);
});
}
After which the "Add Event" screen's button fires the above in similar sequence to the first, just before navigating back:
this.createEvent();
this.props.navigation.state.params.onGoBack();
this.props.navigation.navigate('ItemsDetails');
The "refresh" function looks like so (also called in componentDidMount):
loadItem() {
console.log('Someone called loadItem() with this.itemID of ' + this.itemID);
var id = this.itemID;
let totalWon = 0;
db.loadItem(id)
.then((item) => {
console.log('[LOAD ITEM] got back data of:\n' + JSON.stringify(item));
this.setState({objItem: item, events: item.events});
if (this.state.events.length != 0) { this.setState({itemLoaded: true});
this.state.events.map(function(event) {
totalWon += parseInt(event.eventPointsEarned);
console.log('totalWon is ' + totalWon + ' with ' +
event.eventPointsEarned + ' having been added.');
});
};
this.setState({totalWon: totalWon});
})
.catch((err) => {
console.log('db.loadItem() error: ' + err);
this.setState({itemLoaded: false});
});
}
I'm at a loss for why the List Screen refreshes when I add an item... but not when I'm doing other async db operations with PouchDB in what I think is similar fashion to modify the object containing the "event" information and then heading back to the Item Details screen.
Am I screwing up with Promise chain someplace? Neglecting behavior of the StackNavigator when navigating deeper?
The only other difference being that I'm manipulating the array in the db function in the non-working case, whereas the others I'm merely creating/posting or deleting/removing the record, etc. before going back to update state on the prior screen.
Edit to add, as per comments, going back to "List screen" and the opening "Item Details" does pull the database data and correctly shows that the update was made.
Further checking I've done also revealed that the console.log in createEvent() to print the response to the db call isn't logging until after some of the other dynamic rendering methods are getting called on the "Item Details" screen. So it seems as though the prior screen is doing the get() that loadItem() calls before the Promise chain in createEvent() is resolving. Whether the larger issue is due to state management is still unclear -- though it would make sense in some respects -- to me as this could be happening regardless of whether I've called my onGoBack() function.
Edit/bump: I’ve tried to put async/await to use in various places in both the db_ops module on the db.get() and the component-side loadItem() which calls it. There’s something in the timing of these that just doesn’t jive and I am just totally stuck here. Aside from trying out redux (which I think is overkill in this particular case), any ideas?
There is nothing to do with PDB or navigation, it's about how you manage outer changes in your depending (already mounted in Navigator since they are in history - it's important to understand - so componentDidMount isn't enough) components. If you don't use global state redux-alike management (as I do) the only way to let know depending component that it should update is passing corresponding props and checking if they were changed.
Like so:
//root.js
refreshEvents = ()=> { //pass it to DeleteView via screenProps
this.setState({time2refreshEvents: +new Date()}) //pass time2refreshEvents to EventList via screenProps
}
//DeleteView.js
//delete button...
onPress={db.deleteThing(thingID).then(()=> this.props.screenProps.refreshEvents())}
//EventList.js
...
constructor(props) {
super(props);
this.state = {
events: [],
noEvents: false,
ready: false,
time2refreshEvents: this.props.screenProps.time2refreshEvents,
}
}
static getDerivedStateFromProps(nextProps, currentState) {
if (nextProps.screenProps.time2refreshEvents !== currentState.time2refreshEvents ) {
return {time2refreshEvents : nextProps.screenProps.time2refreshEvents }
} else {
return null
}
}
componentDidMount() {
this._getEvents()
}
componentDidUpdate(prevProps, prevState) {
if (this.state.time2refreshEvents !== prevState.time2refreshEvents) {
this._getEvents()
}
}
_getEvents = ()=> {
//do stuff querying db and updating your list with actual data
}

How to bind data to html in angular 2/4 by using thread or async

I have a Component, which have child component. at ngOnInit() I'm calling Web API and get list of data.
Initial point length of the list is 10, But it will have more.
Need to execute some method (task|process|job) in background to take rest of the data 10 by 10 in a loop which would run parallel to other task in background no matter what the user is currently doing, which component he/she is interacting with. And execute that method so that it doesn't block others.
What is the correct way to do this?
Seems like a recursive call to me !
firstResults: any[] = []; // First 10 results to show to your user
results: any[] = []; // All results
currentPosition = 0; // The current position of your last result fetched
getData() {
this.myService.getResults().subscribe(results => {
if(!this.firstResults.length) {
this.firstResults = results
}
this.results.push(...results);
this.currentPosition += results.length;
this.getData();
});
}
I don't imagine you're trying to do. But if the list is not so largger, you can get all the data and "paginate" the array
allData:any[];
page:number=0; //page is 0,1,2,3,4....
paginateData:any[]
this.httpClient.get("url").subscribe(res=>{
allData=res;
paginateData=allData.slice(10*this.page,10*(this.page+1));
}

How to Subscribe Observable in different Child component in Angular

I have component parent and two child A and B component.
I created a Observable in parent and subscribe in A by passing though #input in A.
On change on parent A function get calls through subscription.
But i want to change the B also.
i have checked links
I want to get the change of parent model in all the childrens
I have solved :
let c: Observable<Array<WorkFlow>> = Observable.create((obs: Observer<Array<WorkFlow>>) => {
this._wfService.getWorkFlows(0, 50).subscribe((data: Array<WorkFlow>) => {
this.workFlows = data;
obs.next(this.workFlows);
}, (err: Response) => {
console.error(err);
});
});
this.workFlowsOb = c.shareReplay(1);
I was returning the c before , now i used c.shareReplay and share to Observable that is shared to all children components

Resources