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
Related
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
I am trying to make a timeline of photos in react native with firebase. My intention is that when the user starts the app he / she has to press a button to retrieve the data. This looks like this:
in View:
<TouchableOpacity onPress={() =>this.getMessagesNew(0)}><Text>Haal berichten op</Text></TouchableOpacity>
The function:
getMessagesNew(key){
console.log('triggerd')
this.setState({
amountVideos: 0,
downloadedVideos:0,
})
this.messages = [];
var orders = firebase.database().ref('messages/'+key+'/posts').orderByChild('post').limitToLast(3);
orders.on('value', snapshot => {
snapshot.forEach( (order) => {
console.log('triggerd in loop')
let state = order.val();
if(state != null){
var newelement = {date: state.date, function: state.function,image:state.image,name: state.name, text: state.text, type:state.type, post: state.post,};
if(newelement.type == false){
this.setState({amountVideos: this.state.amountVideos+1})
this.downloadImage(newelement);
}else{
messages.push(newelement);
if(this.state.amountVideos == 0){
this.setState({
loading: false,
messagesAvailable: true,
refresh: this.state.refresh+1,
})
}else{
console.log('videos laden');
this.setState({
refresh: this.state.refresh+1
})
}
console.log('message local val'+this.messages);
console.log('message state val'+this.state.messages);
}
}else{
this.setState({
messagesAvailable: false,
})
}
});
})
}
the function: this.getMessagesNew () gets the last three messages from firebase. This works without any effort.
But the app also allows the user to upload photos to firebase. That all works, and after uploading the user is navigated back to the main screen. And here it goes wrong, the just uploaded image is already shown. While this is not the intention.
I want the user to have to press a button again to retrieve the new photos.
I know that the first part of the function is not reloaded but from the snapshot.foreach.
Is there any way to stop this process and make it happen only when a button is pressed?
you are subscribing to the changes using orders.on('value', ...) thats why things are getting synced whenever you upload an image so for achieving the behaviour get the value once from the database using orders.once('value', ...) and then everything ll work as you expect
here i m rewriting your code
getMessagesNew(key){
console.log('triggerd');
this.setState({
amountVideos: 0,
downloadedVideos:0
});
this.messages = [];
var orders = firebase.database().ref('messages/'+key+'/posts').orderByChild('post').limitToLast(3);
orders.once('value', snapshot => { // observe i have changed "on" to "once"
snapshot.forEach( (order) => {
console.log('triggerd in loop')
let state = order.val();
if(state != null){
var newelement = {date: state.date, function: state.function,image:state.image,name: state.name, text: state.text, type:state.type, post: state.post,};
if(newelement.type == false){
this.setState({amountVideos: this.state.amountVideos+1})
this.downloadImage(newelement);
}else{
messages.push(newelement);
if(this.state.amountVideos == 0){
this.setState({
loading: false,
messagesAvailable: true,
refresh: this.state.refresh+1,
})
}else{
console.log('videos laden');
this.setState({
refresh: this.state.refresh+1
})
}
console.log('message local val'+this.messages);
console.log('message state val'+this.state.messages);
}
}else{
this.setState({
messagesAvailable: false,
})
}
});
})
}
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
Having a hard time figuring out what I'm doing wrong here.. and I'm sure it's something simple I'm missing. Just trying to increment counters on state whenever the data passed into this switch call matches each category, but for some reason the counter doesn't increment...
countCategories(cart) {
cart.map(function(item){
switch (item.type) {
case "beverage": return () => { this.setState({
countBeverage: this.state.countBeverage + 1
}); }
case "fruit": return () => { this.setState({
countFruit: this.state.countFruit + 1
}); }
case "vegetable": return () => { this.setState({
countVegetable: this.state.countVegetable + 1
}); }
case "snack": return () => { this.setState({
countSnack: this.state.countSnack + 1
}); }
default: return console.log("unknown category");
};
}); }
I also tried it this way, but I don't think I have a reference to 'this' when I call it this way:
countCategories(cart) {
cart.map(function(item){
switch (item.type) {
case "beverage": return this.setState({
countBeverage: this.state.countBeverage + 1
})
case "fruit": return this.setState({
countFruit: this.state.countFruit + 1
})
case "vegetable": return this.setState({
countVegetable: this.state.countVegetable + 1
})
case "snack": return this.setState({
countSnack: this.state.countSnack + 1
});
default: return console.log("unknown category");
};
}); }
Thank you so much for your help!
Just do this dude:
let food = [{type: "snack"},{type: "snack"},{type: "snack"},{type: "snack"},
{type: "veggi"},{type: "veggi"},{type: "veggi"},{type: "veggi"}]
let foodCount = {
veggiCount: this.state.veggiCount || 0,
snackCount: this.state.snackCount || 0,
beefCount: this.state.beefCount || 0,
fruitCount: this.state.fruitCount || 0
};
food.map(item => foodCount[item + "Count"]++ )
this.setState(foodCount)
The important thing here, is to setState 1. once, 2. when the calculation has completed. Avoid setting the state in loops or iterations like for(...) setState()
Assuming you're invoking countCategories bound to the component (the value of this is the component), in your first code, which should work, you could change the mapping function to an arrow function, so it keeps the this value of the countCategories function. Another weird thing I've noticed is that you're creating an array of functions by returning functions that should change the state, instead of actually changing the state:
countCategories(cart) {
// Notice the change in the next line
cart.map(item => {
switch (item.type) {
case "beverage":
// Set the state instead of returning a function that sets the state
this.setState({
countBeverage: this.state.countBeverage + 1
});
break;
case "fruit":
this.setState({
countFruit: this.state.countFruit + 1
});
break;
case "vegetable":
this.setState({
countVegetable: this.state.countVegetable + 1
});
break;
case "snack":
this.setState({
countSnack: this.state.countSnack + 1
});
break;
default:
console.log("unknown category");
break;
};
});
}
An important consideration here is that setState is asynchronous so you can't read the value in the same execution cycle and increment it. Instead, I'd suggest creating a set of changes and then apply them in a single setState.
Below I've used map to iterate over the cart and and increment the state values stored in a cloned copy of state (because this.state should be considered immutable). Then once complete, the state is updated.
let newState = Object.assign({}, this.state);
cart.map((item) => {
// adapt type to state string -> snack to countSnack
const type = item.type.replace(/^(.)/, (s) => 'count' + s.toUpperCase());
newState[type] = (newState[type] || 0) + 1;
});
this.setState(newState);
See notes within Component API documentation for details
I'm looking for _.after function just to play around with it.
var renderPoin = _.after(data.models.length, function() {
console.log( 'siap' );
require(["ftscroller"], function () {
$('.dedo').css('width', $li_width);
var containerElement, scroller;
containerElement = document.getElementById('poin-nav');
scroller = new FTScroller(containerElement, {
scrollbars: false,
scrollingY: false,
snapping: true,
scrollResponseBoundary: 8,
scrollBoundary: 0
//contentWidth: $li_width
});
});
});
_.each(data.models, function (poin, i) {
db.transaction(function(trans) {
trans.executeSql("insert into piezo_point(id, nama, lat, lng) values(?,?,?,?)", [poin.get('id'), poin.get('nama'), poin.get('lat'), poin.get('lng')]);
}, self.errorCB, function() {
self.viewList = new Poin({
model: poin,
vent: window.vent
});
self.$el.find('#poin-nav ul').append(self.viewList.render().el);
$li_width += parseInt(self.viewList.$el.css('width'));
if ( (i+1) === data.models.length){
renderPoin;
}
});
}, self);
however the renderPoin above not executed as expected. What am I doing wrong?
thanks in advance
I think that it's just a typo.
You're not calling the renderPoin function because you're missing the () brackets in this part of the code:
if ( (i+1) === data.models.length){
renderPoin;
}
update
I'm not even going to argue that your code is not even calling the desired function.
Take a look at the official docs:
(_.after)... Creates a version of the function that will only be run after first being called count times.
If you don't believe the docs either, try this running this simple test in your browser console:
var f = _.after(1, function(){});
alert(typeof f); // and be amazed
update #2
The whole purpose of the after function is to leave your callback triggering logic behind and just call the function on every loop. The _.after function decides when to actually call the function or not, so you might as well lose the if ( (i+1) === data.models.length){ and replace it with just the function call:
_.each(data.models, function (poin, i) {
db.transaction(function(trans) {
trans.executeSql("insert into piezo_point(id, nama, lat, lng) values(?,?,?,?)", [poin.get('id'), poin.get('nama'), poin.get('lat'), poin.get('lng')]);
}, self.errorCB, function() {
self.viewList = new Poin({
model: poin,
vent: window.vent
});
self.$el.find('#poin-nav ul').append(self.viewList.render().el);
$li_width += parseInt(self.viewList.$el.css('width'));
// just call it!
renderPoin();
});
}, self);