I have some nested React components, the inner of which is a large svg graph with hundreds if not thousands line and rect elements. To enable some application-wide behaviour and appearance changes I thought to change the className of the uppermost component. The problem is, that the whole application re-renders if I do that.
I understand that this behaviour is somewhat intended, in the sense of a unidirectional render flow, but I thought React would be smarter about what it can reuse and keep DOM changes to a minimum.
Minimal example here: https://jsbin.com/rabofewawu/1/edit?html,js,output
As you can see, the line pattern in the SVG changes every time you press 'here' though I only want the background color to change.
A similar, but more extreme, example arises when I try to zoom and pan the svg by changing the transform property of an inner g element. Using d3, I simply change the attribute. With react, my render function gets called, the updated state results in an updated transform property, and the whole group is re-renderd from scratch instead of changing the DOM attribute.
Am I missing something? Whats the React way to achieve what I'm trying to do?
Take this simple harmless looking render function:
render(){
return <div>{Date.now()}</div>;
}
In React's mental model this would always display the current number every millisecond. React conceptually updates infinite times per second. The inputs of render here are everything in the world, but we happen to just use the clock. Given the same world, we get the same output from render and thus it's idempotent.
Well crap... we don't have infinitely fast computers so we need to compromise. Instead of render's input being everything we restrict it to state and props (and context).
In this restricted setting use of Math.random or Date.now breaks the rules. If you need to use the output of these, it must go through state or props first. How would this look? Well we can use a deterministic random number generator and store the seed in state. Here's a modified version of your component doing that:
var MyComponent = React.createClass({
displayName:"MyComponent",
getInitialState(){
return {
seed: Math.floor(Math.random()*0xffffff)
};
},
render: function() {
// make a random number generator with the given seed
var rng = new Chance(this.state.seed);
function random(x){
return rng.floating({min: 0, max: x, fixed: 7})
}
var s=100, lines = [];
for (var i=0; i<100; i++) {
var line = { x1: random(s), y1: random(s), x2: random(s), y2: random(s) };
lines.push(React.createElement("line", line));
}
return React.createElement("svg", { height: s, width: s}, lines);
}
});
Rendering less than infinite times per second, and rendering only certain components is an optimization. Optimizations should not affect the behavior of the program.
If you wanted different random numbers, you could set the seed to a different random number. Using real Math.random() here is okay because state is the result or i/o operations in your code, and you're invoking this setState in response to some other i/o (e.g. a click handler).
Time is similar; if you want the current time to be rendered, use a setTimeout and setState with the current time. You can then display it how you like in render, including passing it down to other components and doing any kind of math on it you like.
You can control this with the component's shouldComponentUpdate function. By default this always returns true (so the component will always re-render).
There's a little bit of documentation for this function here https://facebook.github.io/react/docs/component-specs.html#updating-shouldcomponentupdate
Related
What to do with components that re-render frequently?
What I'm trying to do is to have a value entered in a TextInput and have that value exposed in multiple squares (Views).
For example, if I type 12345, it will appear in the view as [1][2][3][4][5].
However, we'll draw 5 views initially, and if the input is longer than 5 characters, we'll draw 5 new views and keep repeating.
data = "12345".split("");
data.map(item => <View>{item}</View>
and touch a view to move the cursor to that location to continue typing. For example, if you touch [3] and then type a, it will look like [1][2][3][a][4][5].
As a result, the app is slowing down as all those views are re-rendered, resulting in the error below.
Please report: Excessive number of pending callbacks: 501. Some pending callbacks that might have leaked by never being called from native code
It's hard to use memo because it keeps re-rendering (because it changes the values inside the view).
What can I do to fix this?
I am currently building a Vue application utilizing a map. The functionality is basically this. I have a position on the map and an algorythm to determine weather that position is over water or over land. If it is over water I want to reiterate over random positions nearby until my position is over land and return those coordinates. So basically my (very rudimentary for testing it out first) code looks similar to this
snippet from template
<mapbox-marker :lngLat="getPositionOverLand([0,5])" />
corresponding code
const getPositonOverLand = (coords: any, tries=100) => {
const newCoords = [coords[0], coords[1]] as [number, number];
let thisTry = 0;
while(isOverWater(newCoords) && thisTry < tries){
newCoords[0] = coords[0] + (Math.random() - 0.5)*0.5
newCoords[1] = coords[1] + (Math.random() - 0.5)*0.5
thisTry++;
}
if(thisTry === tries)
throw new Error('No position on land nearby could be found');
coords[0] = newCoords[0];
coords[1] = newCoords[1];
}
Now I debugged the algorythm and it works just fine. Usually after 5 or so attempts a position on land has been found and is returned. I also implemented a parameter for maximum tries so if a position on the open ocean e.g. is beeing put in an error is thrown after a certain number of attempts.
Now my problem in vue is this. If I change the coordinates in my function (it does not matter if I use a return statement or change the coordinates in the array) the function will be triggered again leaving me with an infinite loop.
Is there any possibility to prevent that behaviour and make sure the algorythm will only be run once or did I miss something else.
Strangly I also tried to just return the given coords just for testing and this does not cause an infinite loop.
Not sure what's triggering the reactivity. From the code posted, there is no way to tell since there are no reactive variables in sight, so it might be caused by something further upstream.
However, I see this: <mapbox-marker :lngLat="getPositionOverLand([0,5])" /> as a code smell when it comes to vue, because it has the potential to cause the type of issues you are seeing.
When you are passing a function result into as a parameter the function will re-run every time any of the parents triggers a re-render. And if you have anything in that function that triggers a re-render (which doesn't seem to be the case here, but I'm not seeing a return so I know it's not shown in entirety).
The better way to do this is to use a computed, or a data that gets updated through a watch that way the value is stored and a change is only made as needed.
Alright,
as pointed out already the problem is that every mutation on the position will trigger another reender (which still does not explain though why a direct return of the parameter itsself in the function didn't result in the same behaviour).
However I was able to solve my problem. Now I will post a little more code than included in my original question since another component with asynchronous behavior is involved. So my html looks like this
<mapbox-map :accessToken="myToken" #loaded="updateMyPosition">
<mapbox-marker :lngLat="myPosition" />
</mapbox-map>
Now the position needs to be a ref which will be updated once the map is loaded and available. This will ensure the data is only mutated once instead of the function beeing run over and over again.
I also changed the getPositionOverLand function a little bit but the functionality is basically the same as before. I just preferred the recursive variant over the while loop.
const myPosition = ref([10,10]);
const getPositionOverLand = (coords, currentTry=0, maxTries=100) => {
if(!isOverWater(coords))
return coords;
let newCoords = [
coords[0] + (Math.random() - 0.5)*0.5,
coords[1] + (Math.random() - 0.5)*0.5
];
if(currentTry >= maxTries)
throw new Error('No position on land could be found');
return (isOverWater(newCoords) && currentTry < maxTries)
? getPositonOverLand(coords, currentTry+1, maxTries)
: newCoords;
}
const updateMyPosition = map => {
myPosition.value = getPositionOverLand(myPosition.value);
}
Now while this is working I still think it is not pretty. I would prefer to use the function instead of mutation the ref because in that way working with a v-for for example would be significantly cleaner as it would be to iterate over that array in code and change each value.
For now I will make this the answer but if someone comes up with a cleaner way to do this I'd be more than happy to change the accepted answer ;)
I have data which updates every 10 seconds and I would like to check that all the data is valid before progressing with updates. I am currently getting false data intermittently which occurs as a negative number in one of the values. If one of the objects has a negative value then I don't trust the whole set and don't want to update any elements.
Ideally I don't want to update some items and then bail once the incorrect value occurs, but rather, determine if the whole set is good before updating anything
I'm not sure how d3 can manage this but I've tried with this and it seems to work. But it doesn't seem particularly in keeping with the elegance of D3 so I think there's probably a correct and better way to do it. But maybe not?!
var dataValid = true;
abcItems.each(function (d, i) {
if (0 > dd.Number1 - dd.Number2) dataValid = false;
});
if (dataValid) {
abcItems.each(function (d, i) {
// updating elements here
});
} else {
console.log("negative value occurred");
}
Is there a better way to manage this through D3?
A little bit more context:
The data (JSON provided via a RESTful API) and visualisation (a bar chart) are updating every 10 seconds. The glitch in the API results in incorrect data once every hour or so at the most (sometimes it doesn't happen all day). The effect of the glitch is that the bars all change dramatically whereas the data should only change by ones or twos each iteration. In the next fetch of data 10 seconds later the data is fine and the visualisation comes right.
The data itself is always "well-formed" it's just that the values provided are incorrect. Therefore even during the glitch it is safe to bind the data to elements.
What I want to do, is skip the entire iteration and update phase if the data contains one of these negative values.
Perhaps also worth noting is that the items in the data are always the same, that is to say the only "enter" phase that occurs is on page load and there are no items that exit (though I do include these operations to capture any unexpected fluctuations in the data). The values for these items do change though.
Looking at your code it seams you already have bound the dataset to your DOM elements abcItems.each(...).
Why not bail out of the update function when the data is not valid.
d3.json("bar-tooltip.json", function(dataset) {
if (!dataset.every(d => d.Number2 <= d.Number1)) return;
// do the update of the graph
});
The example assumes you call d3.json() froma function that is called every update interval, but you can use a different update method.
Desired outcome
I want to have a reusable component, consisting of a title and a number.
The title is simple enough, as it is just a string, which I can send as a prop using
<ReactDOM.render(<MyComponent title="floorp"/>, docu...);
But the number will be calculated in a different way for each instance of the component.
Question
Is there a way to cleanly write such a component?
My thoughts
I'm thinking that inside the component, I would have a
calculate: function () {
switch(title) {
case 'floorp':
//...
case 'not_floorp':
//...
}
}
I will be needing at least six different cases, possibly (actually, probably) more later, so scalability is an important factor.
This is the way to do it, or am I overlooking something?
I would seperate the calculator logic from your component and just let the component worry about rendering views. Just make the component dependent on a calculator so it scales and is reusable.
Here is an example that has all your calculators in one file, but you certainly don't have to do this way, each calculator could be in its own file if you wanted (hence this approach gives you flexibility).
calculators.js
var CALCULATORS = {
add: function(a,b){ return a + b; },
subtract: function(a,b){ return a - b; },
suprise: function(){ return Math.random(); }
};
Then you would just pass a calculator to your component like so:
<ReactDOM.render(<MyComponent title="floorp" calculator={CALCULATORS.add}/>, docu...);
Inside your component:
calculate: function () {
//This will equate to '30' since we passed it the 'add' calculator above
return this.props.calculator.call(this, 10, 20);
}
I think I disagree slightly with #jennas's approach. I think the component should only receive simple strings/integers for title and number, and the calculation logic should be done in a parent component or container somewhere above in the hierarchy.
Consider this, you're writing a different function for every type of calculation anyway, why bother passing it into the component? Why not just make the calculation outside of the component and pass in a plain value?
This achieves two goals:
You have one more dumb component, which is good because:
Dumb components are simpler and easier to test and share
Consumers of the component don't need to know how it works, they just need to know what it needs. For example, what values will the function need to make calculations on?
You congregate your app logic in more central places, where it can be more easily managed
However, to adequately answer the question I think it would be helpful to know:
What is this component?
What context is it in?
Is it a todo item with a "due in 2 days" notice?
Is it a draggable whose x and y coordinates are calculated on mouse move?
Who else will be using it? Just you? Other people?
Years ago, I heard about a nice 404 page and implemented a copy.
In working with ReactJS, the same idea is intended to be implemented, but it is slow and jerky in its motion, and after a while Chrome gives it an "unresponsive script" warning, pinpointed to line 1226, "var position = index % repeated_tokens.length;", with a few hundred milliseconds' delay between successive calls. The script consistently goes beyond an unresponsive page to bringing a computer to its knees.
Obviously, they're not the same implementation, although the ReactJS version is derived from the "I am not using jQuery yet" version. But beyond that, why is it bogging? Am I making a deep stack of closures? Why is the ReactJS port slower than the bare JavaScript original?
In both cases the work is driven by minor arithmetic and there is nothing particularly interesting about the code or what it is doing.
--UPDATE--
I see I've gotten a downvote and three close votes...
This appears to have gotten response from people who are (a) saying something sensible and (b) contradicting what Pete Hunt and other people have said.
What is claimed, among other things, by Hunt and Facebook's ReactJS video, is that the synthetic DOM is lightning-fast, enough to pull 60 frames per second on a non-JIT iPhone. And they've left an optimization hook to say "Ignore this portion of the DOM in your fast comparison," which I've used elsewhere to disclaim jurisdiction of a non-ReactJS widget.
#EdBallot's suggestion that it's "an extreme (and unnecessary) amount of work to create and render an element, and do a single document.getElementById. Now I'm factoring out that last bit; DOM manipulation is slow. But the responses here are hard to reconcile with what Facebook has been saying about performant ReactJS. There is a "Crunch all you want; we'll make more" attitude about (theoretically) throwing away the DOM and making a new one, which is lightning-fast because it's done in memory without talking to the real DOM.
In many cases I want something more surgical and can attempt to change the smallest area possible, but the letter and spirit of ReactJS videos I've seen is squarely in the spirit of "Crunch all you want; we'll make more."
Off to trying suggested edits to see what they will do...
I didn't look at all the code, but for starters, this is rather inefficient
var update = function() {
React.render(React.createElement(Pragmatometer, null),
document.getElementById('main'));
for(var instance in CKEDITOR.instances) {
CKEDITOR.instances[instance].updateElement();
}
save('Scratchpad', document.getElementById('scratchpad').value);
};
var update_interval = setInterval(update, 100);
It is doing an extreme (and unnecessary) amount of work and it is being done every 100ms. Among other things, it is calling:
React.createElement
React.render
document.getElementById
Probably with the amount of JS objects being created and released, your update function plus garbage collection is taking longer than 100ms, effectively taking the computer to its knees and lower.
At the very least, I'd recommend caching as much as you can outside of the interval callback. Also no need to call React.render multiple times. Once it is rendered into the dom, use setProps or forceUpdate to cause it to render changes.
Here's an example of what I mean:
var mainComponent = React.createElement(Pragmatometer, null);
React.render(mainComponent,
document.getElementById('main'));
var update = function() {
mainComponent.forceUpdate();
for(var instance in CKEDITOR.instances) {
CKEDITOR.instances[instance].updateElement();
}
save('Scratchpad', document.getElementById('scratchpad').value);
};
var update_interval = setInterval(update, 100);
Beyond that, I'd also recommend moving the setInterval code into whatever React component is rendering that stuff (the Scratchpad component?).
A final comment: one of the downsides of using setInterval is that it doesn't wait for the callback function to complete before queuing up the next callback. An alternative is to use setTimeout with the callback setting up the next setTimeout, like this
var update = function() {
// do some stuff
// update is done to setup the next timeout
setTimeout(update, 100);
};
setTimeout(update, 100);