I have a simple question, but I'm not sure how to proceed.
Here is my initial state:
getInitialState: function() {
return {
selector:{
params:{
platform:'BITSTAMP',
currency:'USD',
item:'BTC',
interval:'15m',
range:'1d'
}
},
platforms: [],
pairs: [],
allPlatforms: {},
range: ['12h','1d','3d','1w','2w','1m','3m','6m','1y','Max', 'Custom'],
interval: ['1m','15m','1h','6h','12h','24h']
}
},
Let's say I need to change selector.params.platform. If I write:
this.setState({
selector: {
params: {
platform: "somenewplatform"
}
}
});
It will destroy my other params keeping only platform. So what I'm doing is
var newState = this.state;
newState.selector.params.platform = "newplatform";
Then I setState({selector:newState});
Is it the right way to do this? Knowing that newState.selector.params.platform = "newplatform"; means modifying the state directly without going through setState directly, I'm not sure this is the right thing to do.
So if anyone has a better solution I'll be really curious to read it.
You don't want to do
var newState = this.state;
newState.selector.params.platform = "newplatform";
this.setState({selector:newState});
This will set this.state.selector to be your entire state object. It looks like what you want is this:
var newSelector = this.state.selector;
newSelector.params.platform = "newplatform";
this.setState({selector: newSelector});
Related
I have these two methods defined in my React component:
handleAddMetric() {
const metricKey = prompt('Name/key of metric');
const newMetricItem = {
name: metricKey,
value: 100
}
let newMetrics = {};
newMetrics[metricKey] = newMetricItem;
const updatedMetrics = Object.assign({}, this.state.metrics, newMetrics);
this.setState({ metrics: updatedMetrics });
}
handleRemoveMetric(keyName) {
let updatedMetrics = this.state.metrics;
delete updatedMetrics[keyName];
console.log('handleRemoveMetric', this, keyName, updatedMetrics);
this.setState({ metrics: updatedMetrics });
}
Adding new values to this.state.metrics works fine, but deleting:
<button onClick={this.handleRemoveMetric.bind(this, key)}>Delete</button>
...calls my handleRemoveMetric function but doesn’t update the collection.
I first it was some issue with this but that doesn’t seem to be the case.
Any ideas?
Update: The console output is:
handleRemoveMetric Metrics {props: Object, context: Object, refs: Object, updater: Object, state: Object…}componentWillUnmount: function ()context: Objectprops: Objectref: Objectrefs: ObjectsetState: function (data, cb)state: Objectupdater: Object_reactInternalInstance: ReactCompositeComponentWrapperisMounted: (...)replaceState: (...)__proto__: ReactComponent
"myMetricKey"
Object {MRR: Object, moneyInBank: Object, wow: Object}
...so at least the collection is updated locally.
You need to copy it over to a new object.
const metrics = {
...this.state.metrics,
[keyName]: null
};
this.setState({ metrics });
should work.
I have a realtime application that gets lots of data. As a result, during lot of handing of the data, the browser becomes unresponsive. I am researching using Promises, or Rx, but those seem to involve more surgery than I want to commit to at this point.
I read that shouldComponentUpdate can be used for this purpose. Would say using it to update once a second be possible [smart is probably what I am looking for] and what would the code look like inside shouldComponentUpdate:
var HomePage = React.createClass({
getInitialState: function() {
var stocks = {};
feed.onChange(function(stock) {
stocks[stock.symbol] = stock;
this.setState({stocks: stocks, bid: stock, ask: stock, last: stock, type: stock, undsymbol: stock});
}.bind(this));
return {
stocks: stocks
};
},
componentDidMount() {
var props = this.props;
},
shouldComponentUpdate(nextProps, nextState) {
// What code would go in here to render
// all changes, say every 1 second only?
},
etc...
}
Possible? Yes. Best practice? Probably not.
shouldComponentUpdate should return a boolean: true if a new update/render cycle is to be run, false if not.
one way to throttle rendering using shouldComponentUpdate is to do timestamp comparisons:
shouldComponentUpdate() {
const now = Date.now();
if (!this.lastUpdated || now - this.lastUpdated > 1000) {
this.lastUpdated = now;
return true;
}
return false;
}
While my scenario is pretty specific, I think it speaks to a bigger question in Flux. Components should be simple renderings of data from a store, but what if your component renders a third-party component which is stateful? How does one interact with this third-party component while still obeying the rules of Flux?
So, I have a React app that contains a video player (using clappr). A good example is seeking. When I click a location on the progress bar, I want to seek the video player. Here is what I have right now (using RefluxJS). I've tried to strip down my code to the most relevant parts.
var PlayerActions = Reflux.createActions([
'seek'
]);
var PlayerStore = Reflux.createStore({
listenables: [
PlayerActions
],
onSeek(seekTo) {
this.data.seekTo = seekTo;
this.trigger(this.data);
}
});
var Player = React.createClass({
mixins: [Reflux.listenTo(PlayerStore, 'onStoreChange')],
onStoreChange(data) {
if (data.seekTo !== this.state.seekTo) {
window.player.seek(data.seekTo);
}
// apply state
this.setState(data);
}
componentDidMount() {
// build a player
window.player = new Clappr.Player({
source: this.props.sourcePath,
chromeless: true,
useDvrControls: true,
parentId: '#player',
width: this.props.width
});
},
componentWillUnmount() {
window.player.destroy();
window.player = null;
},
shouldComponentUpdate() {
// if React realized we were manipulating DOM, it'd certainly freak out
return false;
},
render() {
return <div id='player'/>;
}
});
The bug I have with this code is when you try to seek to the same place twice. Imagine the video player continuously playing. Click on the progress bar to seek. Don't move the mouse, and wait a few seconds. Click on the progress bar again on the same place as before. The value of data.seekTo did not change, so window.player.seek is not called the second time.
I've considered a few possibilities to solve this, but I'm not sure which is more correct. Input requested...
1: Reset seekTo after it is used
Simply resetting seekTo seems like the simplest solution, though it's certainly no more elegant. Ultimately, this feels more like a band-aid.
This would be as simple as ...
window.player.on('player_seek', PlayerActions.resetSeek);
2: Create a separate store that acts more like a pass-through
Basically, I would listen to a SeekStore, but in reality, this would act as a pass-through, making it more like an action that a store. This solution feels like a hack of Flux, but I think it would work.
var PlayerActions = Reflux.createActions([
'seek'
]);
var SeekStore = Reflux.createStore({
listenables: [
PlayerActions
],
onSeek(seekTo) {
this.trigger(seekTo);
}
});
var Player = React.createClass({
mixins: [Reflux.listenTo(SeekStore, 'onStoreChange')],
onStoreChange(seekTo) {
window.player.seek(seekTo);
}
});
3: Interact with window.player within my actions
When I think about it, this feels correct, since calling window.player.seek is in fact an action. The only weird bit is that I don't feel right interacting with window inside the actions. Maybe that's just an irrational thought, though.
var PlayerActions = Reflux.createActions({
seek: {asyncResult: true}
});
PlayerActions.seek.listen(seekTo => {
if (window.player) {
try {
window.player.seek(seekTo);
PlayerActions.seek.completed(err);
} catch (err) {
PlayerActions.seek.failed(err);
}
} else {
PlayerActions.seek.failed(new Error('player not initialized'));
}
});
BTW, there's a whole other elephant in the room that I didn't touch on. In all of my examples, the player is stored as window.player. Clappr did this automatically in older versions, but though it has since been fixed to work with Browserify, we continue to store it on the window (tech debt). Obviously, my third solution is leveraging that fact, which it technically a bad thing to be doing. Anyway, before anyone points that out, understood and noted.
4: Seek via dispatchEvent
I also understand that dispatching a custom event would get the job done, but this feels way wrong considering I have Flux in place. This feels like I'm going outside of my Flux architecture to get the job done. I should be able to do it and stay inside the Flux playground.
var PlayerActions = Reflux.createActions({
seek: {asyncResult: true}
});
PlayerActions.seek.listen(seekTo => {
try {
let event = new window.CustomEvent('seekEvent', {detail: seekTo});
window.dispatchEvent(event);
PlayerActions.seek.completed(err);
} catch (err) {
PlayerActions.seek.failed(err);
}
});
var Player = React.createClass({
componentDidMount() {
window.addEventListener('seekEvent', this.onSeek);
},
componentWillUnmount() {
window.removeEventListener('seekEvent', this.onSeek);
},
onSeek(e) {
window.player.seek(e.detail);
}
});
5: keep the playing position in state (as noted by Dan Kaufman)
Could be done something like this:
handlePlay () {
this._interval = setInterval(() => this.setState({curPos: this.state.curPos + 1}), 1000)
this.setState({playing: true}) // might not be needed
}
handlePauserOrStop () {
clearInterval(this._interval)
this.setState({playing: false})
}
componentWillUnmount () {
clearInteral(this._interval)
}
onStoreChange (data) {
const diff = Math.abs(data.seekTo - this.state.curPos)
if (diff > 2) { // adjust 2 to your unit of time
window.player.seek(data.seekTo);
}
}
I'm using the Angular-Leaflet directive to display a heatmap, and I want the data to evolve through time. My code looks like:
getData().then(function (data) {
$scope.heat.index = 0;
$scope.heat.data = data;
$scope.layers.overlays.heat = {
name: "Heatmap",
type: "heat",
data: $scope.heat.data[$scope.heat.index], // data is a dictionary, not an array
visible: true
};
$scope.$watch('heat.index', function (new_index) {
$scope.layers.overlays.heat.data = $scope.heat.data[new_index];
});
});
However, when I change data.index (through a slider), nothing happens. What could be going on? I know that Angular-Leaflet supports this behavior because of this Github issue where someone added it.
leafletData.getMap().then(function (map) {
map.eachLayer(function (layer) {
if (layer.options.layerName == 'heatmap') {
layer.setLatLngs(newHeatmapData);
}
})
})
This worked for me.
Leaflet.heat provides a redraw() method but that didn't work for me.
I have a route manager defined like in the following code.
App.routeManager = Ember.RouteManager.create();
I don't want to statically add viewstate to the manager; I want to determine what states there are on load depends on data. How can I do that? I'm looking for something like the following code.
foreach(item in itemArray){
App.routeManager.states.add(Ember.ViewState.create(
route: 'item.route'
));
}
Is it possible?
You basically gave yourself the answer, see http://jsfiddle.net/pangratz666/xNnTP/:
App.stateManager = Ember.StateManager.create({
enableLogging: true,
initialState: 'firstState',
firstState: Ember.State.create({
myEvent: function(manager) {
manager.goToState('addedState');
}
})
});
Ember.run.later(function() {
Ember.setPath(App.stateManager, 'states.addedState', Ember.State.create({
enter: function() {
this._super();
console.log('hello dynamic world');
}
}));
App.stateManager.send('myEvent');
}, 10);