Im trying to get an alert to pop up after a certain number of cards are drawn, but when attempting to use the callback in setState the alert pops before the render occurs.
checkWin() {
if (cards[Object.keys(cards)[12]].number === 0) {
alert("GAME OVER");
}
}
drawCard() {
let cardNumber;
do {
cardNumber = Math.floor(Math.random() * (Object.keys(cards).length));
}
while (cards[Object.keys(cards)[cardNumber]].number === 0);
var card = cards[Object.keys(cards)[cardNumber]];
card.number = card.number - 1;
console.log()
this.setState({
card: card.images[card.number + 1],
total: this.state.total -1,
description: card.description
}, this.checkWin());
}
I have tried putting the function in the callback and it doesn't work either.
edit: the alert comes up before the new card is shown
it works as intended by changing checkWin() to this:
if (cards[Object.keys(cards)[12]].number === 0) {
setTimeout(function(){
alert("GAME OVER");
}, 500);
}
you are doing the callback function wrong. try this
this.setState({
card: card.images[card.number + 1],
total: this.state.total -1,
description: card.description
}, () => this.checkWin() ); ///-> see here
Related
I have some trouble trying to figure out why I can't correctly increment/decrement four counters in an array, which is in the app state. The array in the state is this:
...
constructor(props) {
super(props);
this.state = {
right: 0,
left: 0,
up: 0,
down: 0,
...more variables...
bank: [0, 0, 0, 0] // <- array with 4 counters set to zero
};
}
...
The application fetches JSON data from a Node.js server, and saves the incoming data in the "left", "right", "up" and "down" vars you can see in the state. I select which counter should be incremented/decremented (which I called 'bank') reading the "left" and "right" JSON data, and increment or decrement with "up" and "down". Those signals are working correctly, but the problem is in the logic I think.
This is the function that handles the bank selection (the counter):
bankSelection() {
if (parsedJsonData.right) {
currentBank += 1;
if (currentBank > maxBank) { // restart from zero
currentBank = 0;
}
} else if (parsedJsonData.left) {
currentBank -= 1;
if (currentBank < 0) { // restart from 3
currentBank = 3;
}
}
}
I don't save the bank value in the state, since it shouldn't be rendered. The problem comes with the increment/decrement function:
updateCounters() {
if (parsedJsonData.up) {
if (this.state.bank[currentBank] < 9) {
let bankValue = this.state.bank[currentBank] + 1;
this.setState({ bank: bankValue });
} else {
this.setState({ bank: this.state.bank[currentBank] = 0 });
}
} else if (parsedJsonData.down) {
if (this.state.bank[currentBank] > 0) {
let bankValue = this.state.bank[currentBank] - 1;
this.setState({ bank: bankValue });
} else {
this.setState({ bank: this.state.bank[currentBank] = 9 });
}
}
}
I see the React engine complains on line 7 and 14, saying I shouldn't mutate the state directly, but I'm inside a setState function!
Beside this, when I send a JSON formatted like this:
{
"left": 0,
"right": 0,
"up": 1,
"down": 0
}
the first time the counter in the first bank is updated correctly and shows value 1, but the second time I get this error in the browser's console:
Uncaught (in promise) TypeError: Cannot create property '0' on number '1'
at App.updateCounters (App.js:145:1)
at App.js:112:1
I tryed so many solutions I'm going crazy, any help would me appreciated...
You can find the full code here but bear in mind it's still a work in progress in the render method (the first bank is the only one usable, still testing various solutions).
I hope I gave you all the info to help me fix the error.
I see a few problems with your code.
bank's value is array. You're trying to change it to integer with this.setState({ bank: bankValue })
State should be immutable, if you wish to change the value, you need to create a new array and set the bank state. Something like this:
this.setState({bank: Object.values({...this.state.bank, 1: 100}); // where 1 is the idx where you want to update the value, and 100 is the new value
Finally, I managed to solve the problem.
I managed this using the spread operator and avoiding direct assignment inside the setState method.
updateCounters() {
if (parsedJsonData.up) {
if (this.state.bank[currentBank] < 9) {
// let bankValue = this.state.bank[currentBank] + 1;
var newArrayUp = [...this.state.bank];
newArrayUp[currentBank] += 1;
console.log("array clonato dopo aggiornamento " + [...newArrayUp]);
this.setState({ bank: [...newArrayUp] }, () => {
this.forceUpdate();
});
} else {
var zeroArray = [...this.state.bank];
zeroArray[currentBank] = 0;
console.log(zeroArray[currentBank]);
this.setState({ bank: [...zeroArray] });
this.forceUpdate();
}
} else if (parsedJsonData.down) {
if (this.state.bank[currentBank] > 0) {
var newArrayDown = [...this.state.bank];
newArrayDown[currentBank] -= 1;
console.log("array clonato dopo aggiornamento " + [...newArrayDown]);
this.setState({ bank: [...newArrayDown] }, () => this.forceUpdate());
} else {
var nineArray = [...this.state.bank];
nineArray[currentBank] = 9;
console.log(nineArray[currentBank]);
this.setState({ bank: [...nineArray] }, () => this.forceUpdate());
}
}
}
Now I create a copy of the original array, work on it and then pass it to the setState method.
Thanks for suggestions!
I have sub-components that are updated by injecting state from the parent component.
I need to populate the model using an asynchronous function when the value of the parent component changes.
And I want to draw a new subcomponent after the asynchronous operation is finished.
I checked the change of the parent component value in onbeforeupdate, executed the asynchronous function, and then executed the redraw function, but this gets stuck in an infinite loop.
...
async onbeforeupdate((vnode)) => {
if (this.prev !== vnode.attrs.after) {
// Update model data
await asyncRequest();
m.redraw();
}
}
view() {
return (...)
}
...
As far as I can tell onbeforeupdate does not seem to support async calling style, which makes sense because it would hold up rendering. The use-case for onbeforeupdate is when you have a table with 1000s of rows. In which case you'd want to perform that "diff" manually. Say by comparing items length to the last length or some other simple computation.
This sort of change detection should happen in your model when the parent model changes, trigger something that changes the child model. Then the child view will return a new subtree which will be rendered.
In this small example the list of items are passed directly from the parent to the child component. When the list of items increases, by pressing the load button, the updated list of items is passed to the child and the DOM is updated during the redraw. There is another button to toggle if the decision to take a diff in the view should be done manually or not.
You can see when the views are called in the console.
The second example is the more common/normal Mithril usage (with classes).
Manual Diff Decision Handling
<!doctype html>
<html>
<body>
<script src="https://unpkg.com/mithril/mithril.js"></script>
<div id="app-container"></div>
<script>
let appContainerEl = document.getElementById('app-container');
function asyncRequest() {
return new Promise(function (resolve, reject) {
window.setTimeout(() => {
let res = [];
if (Math.random() < 0.5) {
res.push('' + (new Date().getTime() / 1000));
console.log('Found new items: '+ res[0]);
} else {
console.log('No new items.');
}
resolve(res);
// Otherwise reject()
}, 1000);
});
}
class AppModel {
/* Encapsulate our model. */
constructor() {
this.child = {
items: [],
manualDiff: true,
};
}
async loadMoreChildItems() {
let values = await asyncRequest();
for (let i = 0, limit = values.length; i < limit; i += 1) {
this.child.items[this.child.items.length] = values[i];
}
}
getChildItems() {
return this.child.items;
}
toggleManualDiff() {
this.child.manualDiff = !this.child.manualDiff;
}
getManualDiffFlag() {
return this.child.manualDiff;
}
}
function setupApp(model) {
/* Set our app up in a namespace. */
class App {
constructor(vnode) {
this.model = model;
}
view(vnode) {
console.log("Calling app view");
return m('div[id=app]', [
m(Child, {
manualDiff: this.model.getManualDiffFlag(),
items: this.model.getChildItems(),
}),
m('button[type=button]', {
onclick: (e) => {
this.model.toggleManualDiff();
}
}, 'Toggle Manual Diff Flag'),
m('button[type=button]', {
onclick: (e) => {
e.preventDefault();
// Use promise returned by async function.
this.model.loadMoreChildItems().then(function () {
// Async call doesn't trigger redraw so do it manually.
m.redraw();
}, function (e) {
// Handle reject() in asyncRequest.
console.log('Item loading failed:' + e);
});
}
}, 'Load Child Items')]);
}
}
class Child {
constructor(vnode) {
this.lastLength = vnode.attrs.items.length;
}
onbeforeupdate(vnode, old) {
if (vnode.attrs.manualDiff) {
// Only perform the diff if the list of items has grown.
// THIS ONLY WORKS FOR AN APPEND ONLY LIST AND SHOULD ONLY
// BE DONE WHEN DEALING WITH HUGE SUBTREES, LIKE 1000s OF
// TABLE ROWS. THIS IS NOT SMART ENOUGH TO TELL IF THE
// ITEM CONTENT HAS CHANGED.
let changed = vnode.attrs.items.length > this.lastLength;
if (changed) {
this.lastLength = vnode.attrs.items.length;
}
console.log("changed=" + changed + (changed ? ", Peforming diff..." : ", Skipping diff..."));
return changed;
} else {
// Always take diff, default behaviour.
return true;
}
}
view(vnode) {
console.log("Calling child view");
// This will first will be an empty list because items is [].
// As more items are loaded mithril will take diffs and render the new items.
return m('.child', vnode.attrs.items.map(function (item) { return m('div', item); }));
}
}
// Mount our entire app at this element.
m.mount(appContainerEl, App);
}
// Inject our model.
setupApp(new AppModel());
</script>
</body>
</html>
Normal Usuage
<!doctype html>
<html>
<body>
<script src="https://unpkg.com/mithril/mithril.js"></script>
<div id="app-container"></div>
<script>
let appContainerEl = document.getElementById('app-container');
function asyncRequest() {
return new Promise(function (resolve, reject) {
window.setTimeout(() => {
let res = [];
if (Math.random() < 0.5) {
res.push('' + (new Date().getTime() / 1000));
console.log('Found new items: '+ res[0]);
} else {
console.log('No new items.');
}
resolve(res);
// Otherwise reject()
}, 1000);
});
}
class App {
constructor(vnode) {
this.items = [];
}
async loadMoreChildItems() {
let values = await asyncRequest();
for (let i = 0, limit = values.length; i < limit; i += 1) {
this.items[this.items.length] = values[i];
}
}
view(vnode) {
console.log("Calling app view");
return m('div[id=app]', [
m(Child, {
items: this.items
}),
m('button[type=button]', {
onclick: (e) => {
e.preventDefault();
// Use promise returned by async function.
this.loadMoreChildItems().then(function () {
// Async call doesn't trigger redraw so do it manually.
m.redraw();
}, function (e) {
// Handle reject() in asyncRequest.
console.log('Item loading failed:' + e);
});
}
}, 'Load Child Items')]);
}
}
class Child {
view(vnode) {
console.log("Calling child view");
// This will first will be an empty list because items is [].
// As more items are loaded mithril will take diffs and render the new items.
return m('.child', vnode.attrs.items.map(function (item) { return m('div', item); }));
}
}
// Mount our entire app at this element.
m.mount(appContainerEl, App);
</script>
</body>
</html>
It should work. Maybe something is wrong with updating this.prev
Hi I have a timer running which is like it should show a component for 30sec after every 10 seconds. My code is like this`
import { timer } from "rxjs";
componentWillReceiveProps(nextProps, nextState) {
console.log("RECEIVED PROPS");
if (this.props.venuesBonusOfferDuration === 0) {
this.subscribe.unsubscribe();
}
this.firstTimerSubscription;
this.secondTimerSubscription;
// if (this.state.isMounted) {
if (
nextProps.showBonusObj &&
(!nextProps.exitVenue.exitVenueSuccess || nextProps.enterVenues)
) {
// console.log("isMounted" + this.state.isMounted);
//if (this.state.isMounted) {
let milliseconds = nextProps.venuesBonusOfferDuration * 1000;
this.source = timer(milliseconds);
this.firstTimerSubscription = this.source.subscribe(val => {
console.log("hiding bonus offer");
this.firstTimerSubscription.unsubscribe();
this.props.hideBonusOffer();
this.secondTimerSubscription = bonusApiTimer.subscribe(val => {
console.log("caling timer" + val);
this.props.getVenuesBonusOffer(
this.props.venues.venues.id,
this.props.uid,
this.props.accessToken
);
});
});
//}
} else {
try {
if (this.secondTimerSubscription != undefined) {
this.secondTimerSubscription.unsubscribe();
console.log("secondTimer UNSUBSCRIBED");
}
if (this.firstTimerSubscription != undefined) {
this.firstTimerSubscription.unsubscribe();
console.log("firstTimer UNSUBSCRIBED");
}
} catch (error) {
console.log(
"error when removing bonusoffer timer" + JSON.stringify(error)
);
}
//}
}
}
`
Problem is if I try to unsubscribe this * this.firstTimerSubscription* and this.secondTimerSubscription like this
try {
if (this.secondTimerSubscription != undefined) {
this.secondTimerSubscription.unsubscribe();
console.log("secondTimerunmount UNSUBSCRIBED");
}
if (this.firstTimerSubscription != undefined) {
this.firstTimerSubscription.unsubscribe();
console.log("firstTimerunmount UNSUBSCRIBED");
}
} catch (error) {
console.log("error bonusoffer timer" + JSON.stringify(error));
}
its still prints logs within timer like "hiding bonus offer" and "calling timer".
Can someone please point out the issue. It been a day since am into this.
Any help is appreciated.
The problem is that you subscribe multiple times (whenever component receives props) and reassign newest subscription to firstTimerSubscription or secondTimerSubscription references. But doing that, subscriptions does not magically vanish. To see how it works here is a demo:
const source = timer(1000, 1000);
let subscribe;
subscribe = source.subscribe(val => console.log(val));
subscribe = source.subscribe(val => console.log(val));
setTimeout(() => {
subscribe.unsubscribe();
}, 2000)
Even though you unsubscribed, the first subscription keeps emiting. And the problem is that you lost a reference to it, so you can't unsubscribe now.
Easy fix could be to check whether you already subscribed and unsubscribe if so, before subscribing:
this.firstTimerSubscription ? this.firstTimerSubscription.unsubscribe: false;
this.firstTimerSubscription = this.source.subscribe(...
I wouldn't use a second timer. Just do a interval of 10 seconds. The interval emits the iteration number 1, 2, 3..... You can use the modulo operator on that tick. Following example code (for example with 1 second interval) prints true and false in console. After true it needs 3 seconds to show false. After false it needs 1 second to show true.
interval(1000).pipe(
map(tick => tick % 4 !== 0),
distinctUntilChanged(),
).subscribe(x => console.log(x));
I have a function that calculate a "total price" based on a quantity argument passed from a different component (qty) and uses this to display a div:
calculateTotal(qty) {
var t;
if (qty === 1) {
t = 250 //total = total + 250
}
else if (qty > 1) {
t = (qty * 250); //total = qty * 250
}
else {
t = this.state.total
}
return this.setState( { total: this.state.total + t })
}
It would always display the previous calculation rather than the current. So if I enter 1, and then 2 as the quantity, the first time I enter 1, nothing is displayed, and the second time I press 2, the amount displayed is 250 (when it should be 500)
If anyone has any advice as to what the best course of action would be, it would be greatly appreciated.
If it helps, here is the function in the other component that triggers it (they enter a quantity, it sends that quantity to the function):
handleChange(event) {
const key = Number(event.key)
if (key === "Backspace") {
this.setState({qty: 0})
this.props.handleTotal(0);
} else {
this.setState({qty: key})
this.props.handleTotal(this.state.qty);
}
}
Looks like the problem is in the parent component's handleChange. You're calling setState and then hoping to pass the new value to the next function, but this.state.qty will be unchanged in the next line because setState is async.
handleChange(event) {
const key = Number(event.key)
if (key === "Backspace") {
this.setState({qty: 0})
this.props.handleTotal(0);
} else {
this.setState({qty: key})
// this.props.handleTotal(this.state.qty); // <-- this will return the old value because setState above isn't done running
this.props.handleTotal(key); // <-- try this instead
}
}
calculateTotal(qty) {
var t;
if (qty === 1) {
t = 250 //total = total + 250
}
else if (qty > 1) {
t = (qty * 250); //total = qty * 250
}
else {
t = (this.state.total * 2);
}
this.setState( { total: t });
return t;
}
Please, check if this works out!
I'm trying to make a simonSays game in React, but can't figure out how to set the interval with the componentDidMound(). Any advice or guidance would be greatly appreciated
https://codepen.io/oscicen/pen/rooLXY
// Play specific step
playStep(step) {
this.setState({ clickClass: "button hover" });
this.sounds[step].play();
setTimeout(function(){
this.setState({ clickClass: "button" });
}, 300);
}
// Show all steps
showSteps() {
this.setState({ gameConsole: this.state.round });
let num = 0;
let moves = setInterval(function(){
this.playStep(this.state.steps[num]);
this.setState({ gameConsole: "Wait..." });
num++;
if (num >= this.state.steps.length) {
this.setState({ gameConsole: "Repeat the steps!" });
clearInterval(moves);
}
}, 600);
}
this
isn't known inside callback function.
use .bind(this) everytime you need to access this in deeper scopes. e.g.:
let moves = setInterval(function(){
this.playStep(this.state.steps[num]);
this.setState({ gameConsole: "Wait..." });
num++;
if (num >= this.state.steps.length) {
this.setState({ gameConsole: "Repeat the steps!" });
clearInterval(moves);
}
}.bind(this), 600)
Since you are using normal function in setTimeout and setInterval you need to bind the function or change it to arrow function so that you get this context inside the function and setState will work
Here is updated code for your ref
playStep(step) {
this.setState({ clickClass: "button hover" });
this.sounds[step].play();
setTimeout(function(){
this.setState({ clickClass: "button" });
}.bind(this), 300);
}
// Show all steps
showSteps() {
this.setState({ gameConsole: this.state.round });
let num = 0;
let moves = setInterval(function(){
this.playStep(this.state.steps[num]);
this.setState({ gameConsole: "Wait..." });
num++;
if (num >= this.state.steps.length) {
this.setState({ gameConsole: "Repeat the steps!" });
clearInterval(moves);
}
}.bind(this), 600);
}
Also you have couple of functions declared inside component which does setState so you need those function to either bind manually in constructor or change it to arrow function
Here is updated codepen demo https://codepen.io/anon/pen/bOPyVy