Prevent rerender using React hooks and onScroll or onWheel - reactjs

I have a component where I use the onWheel event to detect scrolling in all directions (this works). My problem is preventing this component to rerender so I can utilize throttle from underscore.js:
Example
import React, {useState} from 'react';
import { throttle } from 'underscore';
const App = () => {
const [position, setPosition] = useState({x: 0, y: 0});
const updatePosition = throttle((e) => {
e.preventDefault(); // Required for left/right swiping.
setPosition({
x: position.x + e.deltaX,
y: position.y + e.deltaY,
});
}, 1000);
return (
<div className="viewport" onWheel={updatePosition}>
<Box x={position.x} y={position.y} />
</div>
);
};
export default App;
The throttle function does not work here, since every time the state updates the component rerenderes.

Can you please try the below one. I just wrapped the throttle with the new function.
import { throttle } from "underscore";
import Box from "./Box";
const App = () => {
const [position, setPosition] = useState({ x: 0, y: 0 });
function updatePosition(e) {
e.preventDefault(); // Required for left/right swiping.
throttle(e => {
setPosition({
x: position.x + e.deltaX,
y: position.y + e.deltaY
});
}, 1000)(e);
}
return (
<div className="viewport" onWheel={updatePosition}>
<Box posX={position.x} posY={position.y} />
</div>
);
};
export default App;

You can throttle rendering of a component using the throttle function from underscore by creating a new component called ThrottledBox.
export default function App() {
const [position, setPosition] = React.useState({ x: 0, y: 0 });
function handleOnWheen(e) {
e.preventDefault(); // Required for left/right swiping.
setPosition({
x: position.x + e.deltaX,
y: position.y + e.deltaY
});
}
return (
<div className="viewport" onWheel={handleOnWheen}>
<ThrottledBox x={position.x} y={position.y} />
</div>
);
}
const ThrottledBox = throttle((props) => <Box {...props}/>, 1000);
https://codesandbox.io/s/zealous-booth-x8lfd

Related

Swipeable react component always starts from same [0.0] position

I'm trying to develop a swipe component using React w/ Redux and hammerjs. The problem is that each time I move my component the position.x and position.y start from [0,0] and not from the current state they are at.
Here's the code:
import React, { useState, useEffect } from 'react';
import Hammer from 'hammerjs';
import WeatherForecast from './WeatherForecast';
const SwipeableCard = ({ children }) => {
const [gesture, setGesture] = useState(null);
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const element = document.getElementById('swipeable-card');
const hammer = new Hammer(element);
hammer.on('pan', (event) => {
setPosition({
x: event.deltaX + position.x,
y: event.deltaY + position.y,
});
});
setGesture(hammer);
return () => {
hammer.off('pan');
setGesture(null);
};
}, []);
return (
<div
id="swipeable-card"
className="App-swipeable"
style={{
transform: `translate(${position.x}px, ${position.y}px)`,
}}
>
<WeatherForecast />
</div>
);
};
export default SwipeableCard;

Change attribute from object using requestAnimationFrame in React

I trying to update human.x using requestAnimationFrame and useState. I want to add elements called "humans" and use requestAnimationFrame to update the position to humans walk in "world" (using css properties). The method addHumans work but requestAnimationFrame update and empty my list.
World.tsx
import { useState, useEffect } from 'react'
import Human from './Human'
import IHuman from './Human'
type IHuman = {
id: number;
x: number;
y: number;
color: string;
}
export default function World() {
const [humans, setHumans] = useState<IHuman[]>([])
const addHuman = () => {
setHumans([...humans, {id: Math.random(), x: 0, y: 0, color: 'red'}])
}
const animate = () => {
setHumans(humans?.map((human, i) => {
return {...human, x: human.x + 1}
}));
// console.log(humans)
requestAnimationFrame(animate);
}
useEffect(() => {
requestAnimationFrame(animate);
}, []); // run once
return (
<>
<button onClick={addHuman}>new human</button>
<main>
{humans?.map(human => {
return <Human key = {human.id} x = {human.x} y = {human.y} color = {human.color} />
})}
</main>
</>
)
}
Human.tsx
export default function Human(props: any) {
return (
<div key={props.id} className="human" style={{ top: props.y + 'px', left: props.x + 'px', backgroundColor: props.color }}>{props.id}</div>
)
}
This is called a stale state. Update your code like this:
setHumans(humans => humans?.map((human, i) => {
return {...human, x: human.x + 1}
}));

Animation from sub-component is not displayed

Problem :
Animation is not played from the parent component.
Context :
I have two components.
The first one (Parent component):
export default function LevelSecondItem (props) {
const { Data, moveTo, Identifiant, Score, GAME } = props;
return (
<View style={styles.main_container}>
<ProgressBarLight
percentage={40}
total={3}
items={1}
label={Data.description}
/>
<View style={styles.second_container}>
<ButtonCustom text={"Commencer"} onPress={() => moveTo(Data.puzzle,Identifiant+"-"+Data.id,GAME) }/>
</View>
</View>
)
}
The second component (sub-component - child) :
const ProgressBarLight = (props) => {
const fadeAnim = useRef(new Animated.Value(0)).current // Initial value for opacity: 0
useEffect(() => {
Animated.timing(
fadeAnim,
{
toValue: 100,
duration: 10000,
useNativeDriver:false
}
).start();
}, [fadeAnim])
return (<Animated.View><Animated.Text>{fadeAnim}</Animated.Text></Animated.View>)
}
Comment :
When saving the file, react is updating the app. In this way, I'm able to see the animated value. The value is changing. But the main component is not showing the animation. I don't know why.
You need to reset the animated value to zero by using animation.setValue(0) . I was not able to find this function...
I try to use setAnimation(0) and this is thowing an error.
import React, { useState, useEffect } from 'react'
import { Text, View, StyleSheet, Animated } from 'react-native'
import {GREY80_VALUE,GREEN_VALUE} from '../GlobalConstant'
export default function CardIndicator(props) {
const { currentTotalIndex, currentTotalObject } = props;
const [animation, setAnimation] = useState(new Animated.Value(0));
const anim = ()=>{
Animated.timing(
animation,
{
toValue: 1,
duration: 1000,
useNativeDriver: false,
}
).start(()=>{
Animated.timing(
animation,
{
toValue: 0,
duration: 1000,
useNativeDriver: false,
}
)
});
}
useEffect(()=>{
animation.setValue(0)
anim()
},[currentTotalIndex])
const boxInterpolation = animation.interpolate({
inputRange: [0, 1],
outputRange:[GREEN_VALUE , GREY80_VALUE]
})
const animatedStyle = {
backgroundColor: boxInterpolation
}
return (<Animated.View style={{padding:10,borderRadius:5,...animatedStyle}}><Text style={{fontFamily:"RobotoMono-Bold",fontSize: 16}}>{currentTotalIndex+" / "+currentTotalObject}</Text></Animated.View>)
}
you can use react native animatable library it's very easy to use
just like this
import * as Animatable from 'react-native-animatable';
const FadeInView = (props) => {
return (<View><Animatable.Text animation="fadeIn">{"your text here"}</Animatable.Text></View>)
}
you can find more props of this library and use it

Invalid hook call. Hooks can only be called inside of the body of a function component. While cropping an image and preview that image with react js

Basically, I want to add cropping functionality. If user select a file then, user have choice to crop the image if he/she want. When I preview cropped image.
Error: Invalid hook call. Hooks can only be called inside of the body
of a function component. This could happen for one of the following
reasons: 1. You might have mismatching versions of React and the
renderer (such as React DOM) 2. You might be breaking the Rules of
Hooks 3. You might have more than one copy of React in the same app
import React, { useEffect, useState, useRef } from 'react';
import ReactCrop from 'react-image-crop';
import 'react-image-crop/dist/ReactCrop.css';
// import Styles from './Image.module.css';
const Image = (props) => {
const [crop, setCrop] = useState({
aspect: 3/4,
unit: 'px',
x: 0,
y: 0,
width: 500,
height: 500
});
const [file, setFile] = useState(null);
const [imgPreview, setImgPreview] = useState(null);
const canvasRef = useRef(null);
const filePicker = (e) => {
setFile(e.target.files[0]);
};
function image64toCanvasRef (cnvRef, image64, pixelCrop) {
const canvas = cnvRef;
canvas.width = pixelCrop.width;
canvas.height = pixelCrop.height;
const ctx = canvas.getContext('2d');
const image = new Image(); // On this line throwing error
image.src = image64
image.onload = () => {
ctx.drawImage(
image,
pixelCrop.x,
pixelCrop.y,
pixelCrop.width,
pixelCrop.height,
0,
0,
pixelCrop.width,
pixelCrop.height
)
}
}
useEffect(() => {
if (file) {
const fileReader = new FileReader();
fileReader.onload = () => {
setImgPreview(fileReader.result);
}
fileReader.readAsDataURL(file);
}
}, [file]);
const handleOnCropChanged = (crop) => {
// console.log('handleOnCropChanged: ', crop);
const state = {
...crop,
x: crop.x,
y: crop.y,
width: crop.width,
height: crop.height
}
setCrop(state);
};
const handleOnCropComplete = (crop, pixelCrop) => {
image64toCanvasRef(canvasRef.current, imgPreview, pixelCrop);
}
return (
<div
style={{
margin: '10px 28px',
}}
>
{
imgPreview ? (
<div>
<ReactCrop
src={imgPreview}
crop={crop}
keepSelection
locked
onChange={(crop) => handleOnCropChanged(crop)}
onComplete={handleOnCropComplete}
onImageLoaded={handleOnImageLoaded}
/>
</div>
) : (
<input type='file' onChange={filePicker} />
)
}
<div>
<canvas
ref={canvasRef}
></canvas>
</div>
</div>
)
};
export default Image;

How to test mousemove drag and drop with react-testing-library and framer-motion

I am trying to test the drag and drop functionality using react-testing-libary. The drag and drop functionality comes from framer-motion and the code is in reacy. From what I understand it uses the mousedown, mousemove and mouseup events to do this. I want to test drag and drop functionality of the following basic component:
export const Draggable: FC<DraggableInterface> = ({
isDragging,
setIsDragging,
width,
height,
x,
y,
radius,
children,
}) => {
return (
<motion.div
{...{ isDragging }}
{...{ setIsDragging }}
drag
dragConstraints={{
left: Number(`${0 - x}`),
right: Number(
`${width - x}`,
),
top: Number(`${0 - y}`),
bottom: Number(
`${height - y}`,
),
}}
dragElastic={0}
dragMomentum={false}
data-test-id='dragabble-element'
>
{children}
</motion.div>
);
};
And I have a snippet of the test as follows:
it('should drag the node to the new position', async () => {
const DraggableItem = () => {
const [isDragging, setIsDragging] = useState<boolean>(true);
return (
<Draggable
isDragging={isDragging}
setIsDragging={() => setIsDragging}
x={0}
y={0}
onUpdateNodePosition={() => undefined}
width={500}
height={200}
>
<div
style={{
height: '32px',
width: '32px'
}}
/>
</Draggable>
);
};
const { rerender, getByTestId } = render(<DraggableItem />);
rerender(<DraggableItem />);
const draggableElement = getByTestId('dragabble-element');
const { getByTestId, container } = render(
<DraggableItem />
);
fireEvent.mouseDown(draggableElement);
fireEvent.mouseMove(container, {
clientX: 16,
clientY: 16,
})
fireEvent.mouseUp(draggableElement)
await waitFor(() =>
expect(draggableElement).toHaveStyle(
'transform: translateX(16px) translateY(16px) translateZ(0)',
),
);
However, I cannot get the test to pass successfully as the transform value I test for is set to none. It does not update it the value with the updated CSS. I think there is some sort of async issue or animation delay so the mousemove is not detected and the value of the transform does not change. Would anyone know how to get the test to work or a way to test the mousemove changes?
Any advice or guidance on how I can solve this would be greatly appreciated!
It looks like you are invoking mouseMove() on the container instead of your draggable item. The container here refers to a root div containing your DraggableItem but is not the item itself (API). Therefore, events are being fired on the root div and not the item.
Here is a simple working example for testing draggable elements (for passers-by looking to test mouse down, move, and up events on draggable elements):
//
// file: draggable-widget.tsx
//
import $ from 'jquery'
import * as React from 'react'
type WidgetProps = { children?: React.ReactNode }
export default class DraggableWidget extends React.Component<WidgetProps> {
private element: React.RefObject<HTMLDivElement>
constructor(props: WidgetProps) {
super(props)
this.element = React.createRef()
}
show() { if (this.element.current) $(this.element.current).show() }
hide() { if (this.element.current) $(this.element.current).hide() }
getLocation() {
if (!this.element.current) return { x: 0, y: 0 }
return {
x: parseInt(this.element.current.style.left),
y: parseInt(this.element.current.style.top)
}
}
private onDraggingMouse(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
let location = this.getLocation()
let offsetX = e.clientX - location.x
let offsetY = e.clientY - location.y
let mouseMoveHandler = (e: MouseEvent) => {
if (!this.element.current) return
this.element.current.style.left = `${e.clientX - offsetX}px`
this.element.current.style.top = `${e.clientY - offsetY}px`
}
let reset = () => {
window.removeEventListener('mousemove', mouseMoveHandler)
window.removeEventListener('mouseup', reset)
}
window.addEventListener('mousemove', mouseMoveHandler)
window.addEventListener('mouseup', reset)
}
render() {
return (
<div ref={this.element} className="draggable-widget">
<div className="widget-header"
onMouseDown={e => this.onDraggingMouse(e)}>
<button className="widget-close" onClick={() => this.hide()}
onMouseDown={e => e.stopPropagation()}></button>
</div>
</div>
)
}
}
Then for the test logic:
//
// file: draggable-widget.spec.tsx
//
import 'mocha'
import $ from 'jquery'
import * as React from 'react'
import { assert, expect } from 'chai'
import { render, fireEvent } from '#testing-library/react'
import Widget from './draggable-widget'
describe('draggable widget', () => {
it('should move the widget by mouse delta-xy', () => {
const mouse = [
{ clientX: 10, clientY: 20 },
{ clientX: 15, clientY: 30 }
]
let ref = React.createRef<Widget>()
let { container } = render(<Widget ref={ref} />)
let element = $(container).find('.widget-header')
assert(ref.current)
expect(ref.current.getLocation()).to.deep.equal({ x: 0, y: 0 })
fireEvent.mouseDown(element[0], mouse[0])
fireEvent.mouseMove(element[0], mouse[1])
fireEvent.mouseUp(element[0])
expect(ref.current.getLocation()).to.deep.equal({
x: mouse[1].clientX - mouse[0].clientX,
y: mouse[1].clientY - mouse[0].clientY
})
})
})
Found this section in the react-testing-library docs
https://testing-library.com/docs/dom-testing-library/api-events/#fireeventeventname
Scroll down to the dataTransfer property section - apparently this is what we should be using to test drag-and-drop interactions

Resources