Intelligent use of shouldComponentUpdate to help browser stay responsive - reactjs

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;
}

Related

jest enzyme reactJS - method executes regardless of return

I have the following method I want to test:
handleDayClick(e, day, {disabled}) {
if (disabled) {
// Do not update the state if the day is disabled
return;
}
this.setState({ selectedDay: day });
this.props.dateClick(day);
};
So I wrote a mock function, passed it as a prop and figured I would test state and if the function gets called:
it('handleDayClick() should NOT set state.selectedDay, call props.dateClick when disabled', () => {
let mockDateClick = jest.fn();
const sidebar = shallow(<Sidebar dateClick={mockDateClick}/>);
let e = 'whatever';
let day = 'tomorrow';
sidebar.instance().handleDayClick(e, day, true);
expect(sidebar.state().selectedDay).toBe(undefined);
expect(mockDateClick).toNotHaveBeenCalled();
});
The thing is that the state().selectedDay gets set to tomorrow. The value before calling the method is 2017-01-12T18:18:13.216Z, so I am pretty sure the method does not exit on the render function.
What am I doing wrong?
In your test, you are doing: sidebar.instance().handleDayClick(e, day, true);
while handleDayClick is destructuring an object with the disabled key
handleDayClick(e, day, {disabled}) {
}
Try to call it like this : sidebar.instance().handleDayClick(e, day, { disabled: true });
In the future you might want to use tools like Flow to prevent errors https://flowtype.org/docs/react.html
you would have been able to detect it earlier by doing something like this :
handleDayClick(e: {}, day: string, {disabled}: boolean) {
}

How to communicate an action to a React component

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);
}
}

React change state embedded object

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});

React Flux: How do I auto-sort stored array when a new item is added?

I'm trying to figure out the best way of updating a stored array when a new item is added to it.
Right now I'm sorting it with the code below (sortMissions() is called in the AppDispatcher.register code), but it feels inefficient since the array will be sorted every time one of my switch cases is called, even if it's irrelevant to the _missions array (e.g. if _incomplete_missions changes, I'll still be sorting _missions).
//MissionStore.js
// Define initial data points
var _missions = [], _incomplete_missions = [];
// Add a mission to _missions
function addMission(mission){
_missions.push(mission);
}
// Sort _missions as desired
function sortMissions(){
_missions = _(_missions).chain().sortBy('name').sortBy('points').value();
}
...
var MissionStore = _.extend({}, EventEmitter.prototype, {
...
// Return Mission data
getMissions: function() {
return _missions;
},
// emit change event
emitChange: function() {
this.emit('change');
},
// add change listener
addChangeListener: function(callback) {
this.on('change', callback);
},
// remove change listener
removeChangeListener: function(callback) {
this.removeListener('change', callback);
}
});
AppDispatcher.register(function(payload) {
var action = payload.action;
var text;
switch(action.actionType) {
case MissionActionConstants.MISSION_SAVE:
addMission(action.mission);
break;
default:
return true;
}
sortMissions();
MissionStore.emitChange();
return true;
});
I thought about just sorting _missions in MissionStore.getMissions() and no where else, but that will result in sorting it every time getMissions() called, whether anything changed or not.
I also thought about inserting sortMissions() in every dispatcher case that _missions would change, but that seems like I'm duplicating myself.
Ideally I'd like to subscribe to changes just on the _missions array (from within the same store) and sort _missions only when it changes, but I'm not sure how I would do that.
Thanks!
Maybe you should do the sorting in the controller-view, not in the store.
This way, you maintain only one collection of missions, and this could be immutable data. I recommend ImmutableJS for that.
Then, if MissionsStore.getMissions() !== this.state.missions, you do the sort and then pass the sorted collection to this.setState().
Otherwise, I think you're looking at maintaining a separate cached collection for every type of sort, which seems like a lot to maintain. But it's certainly a viable alternative.

CheckAll/UncheckAll issue with Subscribe ? Knockout

I been trying to do checkbox Checkall and UnCheckall using subscribe and i'm partially successful doing that but i am unable to find a fix in couple of scenarios when i am dealing with subscribe .
Using subscribe :
I am here able to checkAll uncheckAll but when i uncheck a child checkbox i.e test1 or test2 i need my parent checkbox name also to be unchecked and in next turn if i check test1 the parent checkbox should be checked i.e keeping condition both child checkboxes are checked .
For fiddle : Click Here
ViewModel :
self.selectedAllBox.subscribe(function (newValue) {
if (newValue == true) {
ko.utils.arrayForEach(self.People(), function (item) {
item.sel(true);
});
} else {
ko.utils.arrayForEach(self.People(), function (item) {
item.sel(false);
});
}
});
The same scenario can be done perfectly in easy way using computed but due some performance issues i need to use subscribe which is best way it wont fire like computed onload .
Reference : Using computed same thing is done perfectly check this Fiddle
I tried to use change event in individual checkbox binding but its a dead end till now.
Any help is appreciated .
Your subscription only applies to edits on the selectedAllBox. To do what you want, you'll need subscriptions on every Person checkbox as well, to check for the right conditions and uncheck the selectedAllBox in the right situations there.
It strikes me as odd that this would be acceptable but using computed() is not. Maybe you should reconsider that part of your answer. I would much rather compute a "isAllSelected" value based on my viewModel state, then bind the selectedAllBox to that.
I solved a similar problem in my own application a couple of years ago using manual subscriptions. Although the computed observable method is concise and easy to understand, it suffers from poor performance when there's a large number of items. Hopefully the code below speaks for itself:
function unsetCount(array, propName) {
// When an item is added to the array, set up a manual subscription
function addItem(item) {
var previousValue = !!item[propName]();
item[propName]._unsetSubscription = item[propName].subscribe(function (latestValue) {
latestValue = !!latestValue;
if (latestValue !== previousValue) {
previousValue = latestValue;
unsetCount(unsetCount() + (latestValue ? -1 : 1));
}
});
return previousValue;
}
// When an item is removed from the array, dispose the subscription
function removeItem(item) {
item[propName]._unsetSubscription.dispose();
return !!item[propName]();
}
// Initialize
var tempUnsetCount = 0;
ko.utils.arrayForEach(array(), function (item) {
if (!addItem(item)) {
tempUnsetCount++;
}
});
var unsetCount = ko.observable(tempUnsetCount);
// Subscribe to array changes
array.subscribe(function (changes) {
var tempUnsetCount = unsetCount();
ko.utils.arrayForEach(changes, function (change) {
if (change.moved === undefined) {
if (change.status === 'added') {
if (!addItem(change.value))
tempUnsetCount++;
} else {
if (!removeItem(change.value))
tempUnsetCount--;
}
}
});
unsetCount(tempUnsetCount);
}, null, 'arrayChange');
return unsetCount;
}
You'll still use a computed observable in your viewmodel for the the select-all value, but now it'll only need to check the unselected count:
self.unselectedPeopleCount = unsetCount(self.People, 'Selected');
self.SelectAll = ko.pureComputed({
read: function() {
return self.People().length && self.unselectedPeopleCount() === 0;
},
write: function(value) {
ko.utils.arrayForEach(self.People(), function (person) {
person.Selected(value);
});
}
}).extend({rateLimit:0});
Example: http://jsfiddle.net/mbest/dwnv81j0/
The computed approach is the right way to do this. You can improve some performance issues by using pureComputed and by using rateLimit. Both require more recent versions of Knockout than the 2.2.1 used in your example (3.2 and 3.1, respectively).
self.SelectAll = ko.pureComputed({
read: function() {
var item = ko.utils.arrayFirst(self.People(), function(item) {
return !item.Selected();
});
return item == null;
},
write: function(value) {
ko.utils.arrayForEach(self.People(), function (person) {
person.Selected(value);
});
}
}).extend({rateLimit:1});
http://jsfiddle.net/mbest/AneL9/98/

Resources