React - Update the css of a component multiple times a second (60) - javascript

I was testing my websocket implementation by sending the mouse positions of my connected clients and rendering them with a red <div> box. When I added the "visualizer" to the connected clients the framerate was cut in half for each new client that connected.
I am not sure how to optimize something like this, I've tried minimizing the amount of looping (which helped a bit, but after some time it became as slow as my first approach).
The pointer is updated (debounced to) 60 times a second, what would I need to do to let that pointer update 60 times per second for at least four clients ?
I would like to keep those pointers, even though they are not part of the main application. React is probably not meant for this kind of thing, the question then is what should I have used instead ?
First Aproach
const MousePointer = ({ GameLobby }) => {
console.log(GameLobby);
if (!GameLobby) return null;
return Object.values(GameLobby).map((data, i) => {
const pos = data.currentMousePosition ? data.currentMousePosition : [0, 0];
const backgroundColor = 'red';
return (
<div
key={i}
css={{
backgroundColor,
height: '15px',
width: '15px',
boxSizing: 'border-box',
position: 'absolute',
left: pos[0],
top: pos[1],
}}></div>
);
});
};
One less loop
const MousePointer = ({ GameLobby }) => {
if (!GameLobby) return null;
const {
[Object.keys(GameLobby)[0]]: {
backgroundColor = 'red',
currentMousePosition: pos = [0, 0],
},
} = GameLobby;
return (
<div
key={1}
css={{
backgroundColor,
height: '15px',
width: '15px',
boxSizing: 'border-box',
position: 'absolute',
left: pos[0],
top: pos[1],
}}></div>
);
};

Yeah, rerendering at that rate will cause performance problems. You should try attaching a callback ref to the div and updating only the properties that need to change using setInterval.
In order to check the most updated version of GameLobby without rerendering, you will have to refer it some other way (it can't be a prop). One very easy (and extremely questionable) way to do this is by sticking it on the window object or creating another global. I have also seen people add variables as instance properties of their components.
You might also just choose to handle the cursor ghosts outside of your React tree, right when you receive the GameLobby object. Probably easy enough to just append absolutely positioned divs directly to the DOM.
Note that these patterns shouldn't be generalized for other things you build in React, I would categorize them as "dirty tricks" that you need in rare situations (usually related to animation).

Related

In RechartsJs, how to add margin on ReferenceLine label?

This is the result I want to achieve.
But I can't find a way to add margin between the ReferenceLine label and the bar components.
This is my chart component
<BarChart
width={500}
height={300}
data={data}
>
<XAxis hide dataKey="name" />
{
data.map((entry, index) => (
<ReferenceLine key={`cell-${index}`} strokeWidth={0} x={entry.name} label={entry.name} />
))
}
<Bar dataKey="pv" fill="#8884d8">
{
data.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.pv <= 0 ? "#FF645C" : "#29DB92"} />
))
}
</Bar>
</BarChart>
You can see the full example on my sandbox.
https://codesandbox.io/s/bar-chart-with-positive-negative-forked-g3kvz5?file=/src/App.tsx:673-1176
Important: This solution is technically not "correct" if you care for the integrity of your data because it modifies the data so every entry does not start at zero, but because in this case we do not want to show an y-axis anyway I think this can be a simple solution:
const transformData = (margin: number) => {
return data.map((item) => {
if (item.pv >= 0) {
return {
name: item.name,
pv: [margin, item.pv + margin]
};
} else {
return {
name: item.name,
pv: [-margin, item.pv - margin]
};
}
});
};
This simple function maps over every entry of the data and depending of the original pv value returns a new object with the new position of the
corresponding entry. The new pv value is an array where the first entry is the new starting position and the second one is the end position/length of the entry. The new starting position is ether the value of margin (When the original pv is positiv) or -margin (When the original pv is negativ). To keep the original length of the entries the second value of the new array is the original pv +/- the margin.
The App component in this example than has to be modified like this:
export default function App() {
const barData = useMemo(() => transformData(1000), []);
return (
<BarChart width={500} height={300} data={barData}>
<XAxis hide dataKey="name" />
{barData.map((entry, index) => (
<ReferenceLine
key={`cell-${index}`}
strokeWidth={0}
x={entry.name}
label={entry.name}
/>
))}
<Bar dataKey="pv" fill="#8884d8">
{barData.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={entry.pv[0] <= 0 ? "#FF645C" : "#29DB92"}
/>
))}
</Bar>
</BarChart>
);
}
Note: Since the new pv value of each entry is no longer a simple number but an array we need to change the fill property of each Cell component from entry.pv <= 0 ? "#FF645C" : "#29DB92" to entry.pv[0] <= 0 ? "#FF645C" : "#29DB92". You also can play with the margin value to fit your purpose. I choose 1000 here because I thought it looks nice :)
Link to the full code: https://codesandbox.io/s/bar-chart-with-positive-negative-forked-cu6d7q?file=/src/App.tsx:971-1026
The problem is it's not really just a margin change. A bar chart still uses a coordinate system. If you add space around the origin it becomes really a different type of chart, even though the difference is subtle. You effectively need 2 different Y axes for this, to split the negative and positive values. So it's a bit tricky to come up with a "right way" to approach this.
Rechart just uses 1 axis. Given the current implementation, trying to modify the component to do this anyway will be highly prone to bugs if Rechartjs's implementation changes. Changing your data, as suggested in the other answer, is probably the safest approach at the moment. But it's still not a very nice solution.
You could also create 2 separate bar charts. But it's also not nice and adds a lot of risk for mismatches.
I see you already opened an issue on Github, which seems the right place to discuss this in detail. It's a very sensible looking use case that a component library should at least have some kind of answer to. Even if that answer is "no that's deliberately unsupported". But it may very well also be "sure that's easy to add as an option". Some patience for this answer can save you a lot of time and hassle.
Rechart has some pretty extensive documentation, pretty sure they're happy to fill in gaps like this.

Moving arrow with react native

i'm new to react native and i'm trying to do a moving arrow after a variable get a value, I thought about using a switch case and changing the style, but it seemed impossible to change the padding properties, how could i solve this?
Your question needs a lot of information to be answered thoroughly so I will assume you use hooks, I will try to guide you by giving you an example.
The whole principle is to place your arrow in an 'absolute' position and animate it with an animation. Then, all you need to do is to set the value of the variable 'arrowValue', example : setArrowValue(0.3). Your arrow will then places itself at 30% (according to your interpolation) from the left of your container.
Here is a code snippet to show you the right way :
import {StyleSheet, View, Animated} from "react-native";
export default function ArrowMover(props)
{
// This is the value you will set and your arrow will automatically be placed
const [arrowValue, setArrowValue] = React.useState(0); // From 0 to 1 for example
// This is your animation
// It is the value you have to change to make your arrow move
const [animation, setAnimation] = React.useState(new Animated.Value(0));
// This is an interpolation of your animation
// See this as a black box, it allows you to output value (and ranges of values)
// based on the value of your animation
const animateArrowPosition = animation.interpolate({
inputRange: [0, 1],
outputRange: ["0%", "100%"],
});
// This is where everthing happens automatically
React.useEffect(() =>
{
moveArrowTo(arrowValue);
}, [arrowValue]); // Anytime 'arrowValue' changes, execute the 'moveArrowTo' function
const moveArrowTo = (newArrowValue) =>
{
Animated.timing(animation, { toValue: newArrowValue, duration: 500, }).start(() =>
{
// Do something (or nothing) once the animation finished
});
};
return(
<View style={s.container}>
// Your animation interpolation will change along with 'animation' to follow your interpolation
// of the value
<Animated.View style={[s.arrow, {left:animateArrowPosition}]}> // Notice the 'Animated.View'
<View>
// Your arrow (icon, image, etc...)
</View>
</Animated.View>
</View>
);
}
let s = StyleSheet.create(
{
container:
{
// Your container style
},
arrow:
{
// Your arrow style
height:30,
aspectRatio:1,
position:'absolute',
// left:0 (no need for that, your animation is taking care of the 'left' property
},
}

Why is some information missing when cloning a DOM node?

I am trying to clone a DOM node and add it to the dom with the original but with a few properties transformed.
I am working with Angular but I believe that should not be a factor in this as I am basically querying the DOM directly.
Here is my code for querying the DOM for the component, cloning it, and adding it back to the DOM in a new Absolute position.
const selector = `[hero-id='${heroId}']`;
const component = this.document.querySelector(selector);
console.log('The component HTML here is', component)
if (component) {
const rect: DOMRect = component.getBoundingClientRect();
const { top, left } = rect;
const newPosition = {
left: (left || 0) + window?.scrollX,
top: (top || 0) + window?.scrollY,
height: component.clientHeight,
width: component.clientWidth
}
console.log('The component textContent here is', component?.textContent)
console.log('The component innerHTML here is', component?.innerHTML)
console.log(`Returning`, JSON.stringify(newPosition))
const newNode = component.cloneNode(true);
this.renderer.setStyle(newNode, 'position', 'absolute');
this.renderer.setStyle(newNode, 'top', `${newPosition.top}px`);
this.renderer.setStyle(newNode, 'left', `${newPosition.left}px`);
this.renderer.setStyle(newNode, 'width', `${newPosition.width}px`);
this.renderer.setStyle(newNode, 'height', `${newPosition.height}px`);
this.document.getElementsByTagName('body')[0].appendChild(newNode)
But after doing this some of the information in the original component is missing in the Cloned component. Most importantly the text.
By reading through many articles this should be at the textContent or innerHTML property of the node. From the console logs we can see this is not the case.
I have not found a similar case and am wondering if what I could be doing wrong.
Thanks

React JS animations based on JSON data

I am using React/Redux and am storing animation data in JSON and trying to get it to display on a React page.
I am using setTimeout (for pauses) and setInterval (for animation movement). However, I seem to be having trouble understanding how to implement the animations correctly and think I'm going about things totally the wrong way.
JSON data:
"objects": [
{
"title": "puppy",
"image_set": [
{
"image": "images/puppy_sitting.png",
"startx": 520,
"starty": 28,
"pause": 1000
},
{
"image": "images/puppy_walking.png",
"startx": 520,
"starty": 28,
"endx": 1,
"endy": 1,
"time": 1000
},
{
"image": "images/puppy_crouching.png",
"startx": 1,
"starty": 1,
"endx": 500,
"endy": 400,
"time": 2000
}
]
},
{
"title": "scorpion",
"image_set": [
{
"image": "images/scorping_sleeping.png",
"startx": 100,
"starty": 400,
"pause": 5000
},
{
"image": "images/scorpion_walking.png",
"startx": 100,
"starty": 400,
"endx": 500,
"endy": 400,
"time": 7000
},
{
"image": "images/scorpion_walking.png",
"startx": 500,
"starty": 400,
"endx": 100,
"endy": 400,
"time": 2000
},
{
"image": "images/scorpion_walking.png",
"startx": 100,
"starty": 400,
"endx": 200,
"endy": 400,
"time": 7000
},
{
"image": "images/scorpion_walking.png",
"startx": 200,
"starty": 400,
"endx": 100,
"endy": 400,
"time": 1000
}
]
}
]
Each object can have several images related to them. The animations will continue to repeat non-stop. Each object should move concurrently with each of the other objects so that I can create a scene of various animals and objects moving around it.
Animation code:
I'm pretty sure I'm barking up the wrong tree here, but my code looks something like this:
// image_set is the list of images for a specific object
// object_num is the array index corresponding to the JSON objects array
// selected is the array index corresponding to which image in the image_set will be displayed
runAnimation(image_set, object_num, selected){
// Uses prevState so that we keep state immutable
this.setState(prevState => {
let images = [...prevState.images];
if (!images[object_num]){
images.push({image: null, x: 0, y: 0})
}
images[object_num].image = image_set[selected].image;
images[object_num].x = this.getFactoredX(image_set[selected].startx);
images[object_num].y = this.getFactoredY(image_set[selected].starty);
return {images: images};
})
if (image_set[selected].endx && image_set[selected].endy && image_set[selected].time){
let x = this.getFactoredX(image_set[selected].startx)
let y = this.getFactoredY(image_set[selected].starty)
let startx = x
let starty = y
let endx = this.getFactoredX(image_set[selected].endx)
let endy = this.getFactoredY(image_set[selected].endy)
let time = image_set[selected].time
let x_increment = (endx - x) / (time / 50)
let y_increment = (endy - y) / (time / 50)
let int = setInterval(function(){
x += x_increment
y += y_increment
if (x > endx || y > endy){
clearInterval(int)
}
this.setState(prevState => {
let images = [...prevState.images]
if (images[object_num]){
images[object_num].x = x
images[object_num].y = y
}
return {images: images};
})
}.bind(this),
50
)
}
if (image_set[selected].pause && image_set[selected].pause > 0){
selected++
if (selected == image_set.length){
selected = 0
}
setTimeout(function() {
this.runAnimation(image_set, object_num, selected)
}.bind(this),
image_set[selected].pause
)
}
else {
selected++
if (selected == image_set.length){
selected = 0
}
setTimeout(function() {
this.runAnimation(image_set, object_num, selected)
}.bind(this),
50
)
}
}
Redux and this.props.data
Redux brings in the data as props. So, I have a function called from my componentDidMount and componentWillReceiveProps functions that passes the original image set into the loadAnimationFunction.
My render()
In my render() function I have something like this:
if (this.state.images.length > 1){
animated = this.state.images.map((image, i) => {
let x_coord = image.x
let y_coord = image.y
return (
<div key={i} style={{transform: "scale(" + this.state.x_factor + ")", transformOrigin: "top left", position: "absolute", left: x_coord, top: y_coord}}>
<img src={`/api/get_image.php?image=${image.image}`} />
</div>
)
})
}
x_factor / y_factor
Throughout my code there is also reference to x and y factor. This is because the background that the animations appear in may be scaled smaller or larger. Therefore I also scale the position of the starting and ending x/y coordinates for each animation as well as scale the animated images themselves.
time and pause time
Time indicates the time in ms that the animation should take. Pause time indicates how long in ms to pause before moving to the next animation.
The problem
The code does not move the animations smoothly and they seem to jump around sporadically.
Also, when I click the mouse anywhere on the page it causes the animations to jump to another position. Why would clicking the mouse affect the animation?
One thing I've noticed is that if I have the console open for debugging purposes, this really slows down the animations.
What can I do to my code so that the animations work as expected?
You are trying to animate your element using a setInterval doing a setState of the coordinates and with an absolute position. All of these cannot achieve great performance.
First, setInterval should never be used for animations, and you should prefer requestAnimationFrame as it will allow 60fps animations since the animation will be run before the next repaint of the browser.
Second, doing a setState would re-render your whole component which could potentially have an impact on the rendering timing as I assume your component doesn't render only your images. You should try to avoid at maximum to re-render things that haven't changed, so try to isolate your images for the animations.
Lastly, when you position your element with left and top properties, but you should stick to that, positioning, and not animating as the browser would do the animation pixel by pixel and would not be able to create good performances. Instead, you should use CSS translate(), as it can do sub-pixel calculation and will work on the GPU instead, allowing you to achieve 60fps animations. There is a good article on that by Paul Irish.
That being said, you should probably use react-motion which would allow you a smooth animation:
import { Motion, spring } from 'react-motion'
<Motion defaultStyle={{ x: 0 }} style={{ x: spring(image.x), y: spring(image.y) }}>
{({ x, y }) => (
<div style={{
transform: `translate(${x}px, ${y}px)`
}}>
<img src={`/api/get_image.php?image=${image.image}`} />
</div>
)}
</Motion>
There is also the React Transition Group, Transition could move your elements using a translate animation like explained above. You should also go take a look at the react animations docs here.
Worth a try too, is React Pose, which is pretty easy to use and also performs quite well with a clean API. Here is the get started page for React.
Here is a quick demo using your concept with a sit/walking/running cycle. Notice how react-motion is the only one to handle the transition in between the frames without hardcoding the duration of the transition, which would go against a fluid UI, the state only handles going through the different steps.
Quoting the react-motion Readme:
For 95% of use-cases of animating components, we don't have to resort to using hard-coded easing curves and duration. Set up a stiffness and damping for your UI element, and let the magic of physics take care of the rest. This way, you don't have to worry about petty situations such as interrupted animation behavior. It also greatly simplifies the API.
If you are not satisfied with the default spring, you can change the dampling and stiffness parameters. There's an app which could help you get the one which satisfy you the most.
Source
React is not exactly meant to be used for animations. I'm not saying you can't animate react components, but it's not part of the problem domain react tries to solve. What it does do is provide you with a nice framework to have the several UI pieces interact with each other. I.e. when creating a game for instance, you'll use react and redux to create and manage the screens, buttons etc. however the game itself, would be separately contained and not use react.
Just a long-winded way to say that if you want to use animations react will not suffice, it's better to use something like greensock's animation library: https://greensock.com/
They provide a tutorial on how to use it in conjunction with react: https://greensock.com/react
Let css do the transitions. Use transform: translate instead of top and left.
The animations you have in your sample are very easy to express with css transition, transition-delay, and transform.
I would put my effort in converting the JSON to css (using a cssInJs solution that allows you to generate the classes on the fly) and apply those classes to the images.
something like this(working example with your JSON sample): https://stackblitz.com/edit/react-animate-json
const App = () =>
<div>
{objects.map(object =>
<Item item={object} />)
}
</div>
Item.js:
class Item extends React.Component {
state = { selected: 0, classNames: {} }
componentDidMount() {
this.nextImage();
this.generateClassNames();
}
generateClassNames = () => {
const stylesArray = this.props.item.image_set.flatMap((image, index) => {
const { startx, starty, endx = startx, endy = starty, time } = image;
return [{
[`image${index}_start`]: {
transform: `translate(${startx}px,${starty}px)`,
transition: `all ${time || 0}ms linear`
}
}, {
[`image${index}_end`]: { transform: `translate(${endx}px,${endy}px)` }
}]
});
const styles = stylesArray.reduce((res, style) => ({ ...res, ...style }), {})
const { classes: classNames } = jss.createStyleSheet(styles).attach();
this.setState({ classNames });
}
nextImage = async () => {
const { image_set } = this.props.item;
let currentImage = image_set[this.state.selected];
await wait(currentImage.pause);
await wait(currentImage.time);
this.setState(({ selected }) =>
({ selected: (selected + 1) % image_set.length }), this.nextImage)
}
render() {
const { selected, classNames } = this.state;
const startClassName = classNames[`image${selected}_start`];
const endClassName = classNames[`image${selected}_end`];
return <img
className={`${startClassName} ${endClassName}`}
src={this.props.item.image_set[selected].image}
/>
}
}
const wait = (ms) => new Promise(res => setTimeout(res, ms));
I believe that your fundamental problem lies in the way React/Redux handle state. React may batch multiple update requests together to make rendering more efficient. Without further diagnostic measures, my guess is that the state handling after setState will just respond too rigidly.
The solution would to update your animation outside the state system, either using a ready-made framework or simply by taking care of the animation yourself; get a reference to the element and update it instead of re-rendering the element every time the state is updated.
without going deep about animations in JS (there are already plently of valid answers here) you should consider how you render your images:
<div key={i} style={{transform: "scale(" + this.state.x_factor + ")", transformOrigin: "top left", position: "absolute", left: x_coord, top: y_coord}}>
<img src={`/api/get_image.php?image=${image.image}`} />
</div>
You should actually see a warning when compiling this (or was it in the docs?) because you use the loop index as the key. This should lead to an image object being rendered in different divs as more images are added/removed. This is especially critically if you have a css-transition effect on the div.
TLDR: use some identifier as the key instead of the variable i (may be generate one when you create the animation?)
Also if you have a css transition on the div, you should remove it, because together with the changes from setInterval the transition calculation won't be able to keep up with the changes.

Moving div by passing redux state to dynamic react-pose props in react

I've cobbled together this little sandbox to demonstrate: https://codesandbox.io/s/64xv97y45n
The purple div in the upper left hand corner is supposed to move as letters are typed. When a letter is typed, the currIndex (the currently active box) value on the redux store is incremented or decremented accordingly. The reducer then uses currIndex to compute the currentCoords or the div's new absolute position (for the purposes of attached sandbox, 'slightly more to the right'). The currentCoords store property is then passed on as a prop to control the dynamic pose of the purple div. But the div refuses to update its pose. Redux DevTools tells me currentCoords is updating properly, or at least well enough for it to move a little. What gives?
Relevant logic:
const reducer = (state = initState, action) => {
switch (action.type) {
case "KEYPRESS":
return {
...state,
number: [...state.number, action.key],
currIndex: ++state.currIndex,
currentCoords: state.boxCoords[state.currIndex]
};
<SNIP>
const Cursor = posed.div({
visible: {
opacity: 1,
x: ({ xPos }) => xPos,
y: ({ yPos }) => yPos
},
hidden: {
opacity: 0
}
});
<SNIP>
<Cursor
className="cursor"
initialPose="visible"
xPos={this.props.currentCoords.x}
yPos={this.props.currentCoords.y}
/>
If you want to transition your posed element without changing the current pose you need to pass a poseKey to react on. Also according to documentation of initialPose property:
Once the component mounts, it will transition from this pose into pose.
That is why have must pass pose property to the posed component otherwise initialPose will be reset. So basically <Cursor> component should be rendered like this:
<Cursor
className="cursor"
initialPose="visible"
pose="visible" // you can hold the pose in your state and use it here like this.state.pose
poseKey={this.props.currentCoords.x}
xPos={this.props.currentCoords.x}
yPos={this.props.currentCoords.y}
/>

Categories