How to update a state while scrolling - reactjs

I have a state and a ref like that :
const [myValue, setMyValue] = useState();
const scrollViewRef = useRef();
I have a scrollview with multiple views in it :
<ScrollView
style={styles.optionsContainer}
horizontal={true}
ref={scrollViewRef}
>
<View>
// Some stuff
</>
<View>
// Some stuff
</>
<View>
// Some stuff
</>
</ScrollView>
When the value of myValue state changes, the position of the corresponding view changes thanks to the ref :
useEffect(() => {
if (myValue < 24) {
scrollViewRef.current.scrollTo({ x: 0, y: 0, animated: true })
}
if (myValue > 24 && myValue < 50) {
scrollViewRef.current.scrollTo({ x: 190, y: 0, animated: true });
}
if (myValue > 49 && myValue < 75) {
scrollViewRef.current.scrollTo({ x: 480, y: 0, animated: true });
}
}, [myValue]);
What I want to do is to keep that feature, but add the reverse possibility : if I scroll by hand, I want to update the value of myValue state.
How can I achieve that ?

Add scroll event listener, initialize it in useEffect, and invoke your updateState functions when scrolls.
useEffect(() => {
window.addEventListener('scroll', handleScroll);
}, [])
const handleScroll = () => {
setMyValue(window.scrollTop)
}

What you can do is add onScroll event for scrollView
const onScroll = (event) =>{
if(event.nativeEvent.contentOffset.x <190){
setMyValue(22)
}
if(event.nativeEvent.contentOffset.x < 480){
setMyValue(50)
}
}
return(<ScrollView onScroll={onScroll} >)
Hope it helps, feel free for doubts

Related

React Native Flatlist Error onViewableItemsChanged

I have a horizontal scrolling flatlist in my app. But onViewableItemsChanged keeps throwing an error. 'Changing onViewableItemsChanged on the fly is not supported.' I write the codes below. Please help me!
const flatListRef = useRef(null);
const [currentSectionIndex, setCurrentSectionIndex] = useState(0);
const onButtonPress = useCallback(() => {
if (currentSectionIndex === searchedData?.length - 1) {
// What you want to do at the end of the list.
} else if (flatListRef.current) {
// Go to the next item
flatListRef.current.scrollToIndex({
index: currentSectionIndex + 1,
});
setCurrentSectionIndex(currentSectionIndex + 1);
}
}, [currentSectionIndex, searchedData?.length]);
const onLeftButtonPress = useCallback(() => {
flatListRef.current.scrollToIndex({
index: currentSectionIndex - 1,
});
setCurrentSectionIndex(currentSectionIndex - 1);
}, [currentSectionIndex, searchedData?.length]);
const viewabilityConfig = useRef({
itemVisiblePercentThreshold: 100,
waitForInteraction: true,
minimumViewTime: 5,
})
const onViewableItemsChanged = useCallback(({ viewableItems }) => {
if (viewableItems.length >= 1) {
setCurrentSectionIndex(viewableItems[0].index);
}
}, [])
<FlatList
ref={flatListRef}
horizontal
pagingEnabled // cause snapping to items
data={data}
keyExtractor={(item,index)=>index.toString()}
renderItem={renderItem}
viewabilityConfig={viewabilityConfig.current}
onViewableItemsChanged={onViewableItemsChanged}
/>
{currentSectionIndex!==searchedData?.length-1&&!enteredString?<>
<View> //splinter
<TouchableOpacity onPress={()=>onButtonPress()}>
<Entypo name={'chevron-thin-right'} size={38} color={'#fff'}/>
</TouchableOpacity>
</View>
</>:null}

React Link framer motion animation with AnimatePresence

I have a Navigation component in which the Menu Items float in separately on load and float out on click.
When I added Router and changed the items to Links, the exit animation didn't work because it loaded the new Route component right away.
I want to keep the items individual animation with Link functionality.
Here is the link:
https://codesandbox.io/s/elastic-leaf-fxsswo?file=/src/components/Navigation.js
Code:
export const Navigation = () => {
const navRef = useRef(null);
const onResize = () => {
setIsColumn(window.innerWidth <= 715);
};
const [clickOnMenu, setClick] = useState(false);
const [itemtransition, setTransition] = useState(
Array(menuItems.length).fill(0)
);
const [isColumn, setIsColumn] = useState(window.innerWidth <= 715);
const click = (e) => {
const copy = [...itemtransition];
const index = e.target.id;
setTransition(copy.map((e, i) => (Math.abs(index - i) + 1) / 10));
setTimeout(() => setClick(true), 50);
};
useEffect(() => {
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}, []);
return (
<AnimatePresence exitBeforeEnter>
{!clickOnMenu && (
<Nav ref={navRef}>
{menuItems.map((e, i) => {
const text = Object.keys(e)[0];
const value = Object.values(e)[0];
return (
<Item
id={i}
key={value}
animate={{
x: 0,
y: 0,
opacity: 1,
transition: { delay: (i + 1) / 10 }
}}
initial={{
x: isColumn ? 1000 : 0,
y: isColumn ? 0 : 1000,
opacity: 0
}}
exit={{
x: isColumn ? -1000 : 0,
y: isColumn ? 0 : -1000,
opacity: 0,
transition: { delay: itemtransition[i] }
}}
onClick={click}
>
{/*<Link to={`/${value}`}>{text}</Link>*/}
{text}
</Item>
);
})}
</Nav>
)}
</AnimatePresence>
);
};
In the sandbox in Navigation.js 69-70. row:
This is the desired animation.
69. {/*<Link to={`/${value}`}>{text}</Link>*/}
70. {text}
But when I use Link there is no exit animation
69. <Link to={`/${value}`}>{text}</Link>
70. {/*text*/}
Is there a workaround or I should forget router-dom.
Thank you in forward!
This may be a bit hackish, but with routing and transitions sometimes that is the nature. I suggest rendering the Link so the semantic HTML is correct and add an onClick handler to prevent the default navigation action from occurring. This allows any transitions/animations to go through. Then update the click handler of the Item component to consume the link target and issue an imperative navigation action on a timeout to allow transitions/animations to complete.
I used a 750ms timeout but you may need to tune this value to better suit your needs.
Example:
...
import { Link, useNavigate } from "react-router-dom";
...
export const Navigation = () => {
const navRef = useRef(null);
const navigate = useNavigate(); // <-- access navigate function
...
const click = target => (e) => { // <-- consume target
const copy = [...itemtransition];
const index = e.target.id;
setTransition(copy.map((e, i) => (Math.abs(index - i) + 1) / 10));
setTimeout(() => {
setClick(true);
}, 50);
setTimeout(() => {
navigate(target); // <-- navigate after some delta
}, 750);
};
...
return (
<AnimatePresence exitBeforeEnter>
{!clickOnMenu && (
<Nav ref={navRef}>
{menuItems.map((e, i) => {
const text = Object.keys(e)[0];
const value = Object.values(e)[0];
return (
<Item
...
onClick={click(`/${value}`)} // <-- pass target to handler
>
<Link
to={`/${value}`}
onClick={e => e.preventDefault()} // <-- prevent link click
>
{text}
</Link>
</Item>
);
})}
</Nav>
)}
</AnimatePresence>
);
};
...

How to refactor React mousemove Class to functional Component?

I have a button which closes a navigation. This button follows the mouse. Everything is working, but I have a depricationwarning, which I wanna get rid of, but don't know exactly how. (I only know that useEffect is playing a certain role:
Here is the class:
import React from "react"
class NavigationCloseMouseButton extends React.Component {
static defaultProps = {
visible: true,
offsetX: 0,
offsetY: 0,
}
state = {
xPosition: 0,
yPosition: 0,
mouseMoved: false,
listenerActive: false,
}
componentDidMount() {
this.addListener()
}
componentDidUpdate() {
this.updateListener()
}
componentWillUnmount() {
this.removeListener()
}
getTooltipPosition = ({ clientX: xPosition, clientY: yPosition }) => {
this.setState({
xPosition,
yPosition,
mouseMoved: true,
})
}
addListener = () => {
window.addEventListener("mousemove", this.getTooltipPosition)
this.setState({ listenerActive: true })
}
removeListener = () => {
window.removeEventListener("mousemove", this.getTooltipPosition)
this.setState({ listenerActive: false })
}
updateListener = () => {
if (!this.state.listenerActive && this.props.visible) {
this.addListener()
}
if (this.state.listenerActive && !this.props.visible) {
this.removeListener()
}
}
render() {
return (
<div
onClick={this.props.toggleNavigation}
className="tooltip color-bg"
style={{
display:
this.props.visible && this.state.mouseMoved ? "block" : "none",
opacity: this.props.visible && this.state.mouseMoved ? "1" : "0",
top: this.state.yPosition + this.props.offsetY,
left: this.state.xPosition + this.props.offsetX,
}}
>
Close Menu
</div>
)
}
}
export default NavigationCloseMouseButton
And this is what I've so far, but results with errors:
ReferenceError: getTooltipPosition is not defined
import React, { useState, useEffect } from "react"
const NavigationCloseMouseButton = () => {
const defaults = {
visible: true,
offsetX: 0,
offsetY: 0,
}
const defaultState = {
xPosition: 0,
yPosition: 0,
mouseMoved: false,
listenerActive: false,
}
const [defaultProps, setDefaultProps] = useState(defaults)
const [state, setState] = useState(defaultState)
useEffect(() => {
// Update the document title using the browser API
addListener()
}, [])
getTooltipPosition = ({ clientX: xPosition, clientY: yPosition }) => {
setState({
xPosition,
yPosition,
mouseMoved: true,
})
}
addListener = () => {
window.addEventListener("mousemove", getTooltipPosition)
setState({ listenerActive: true })
}
removeListener = () => {
window.removeEventListener("mousemove", getTooltipPosition)
setState({ listenerActive: false })
}
updateListener = () => {
if (!state.listenerActive && props.visible) {
addListener()
}
if (state.listenerActive && !props.visible) {
removeListener()
}
}
return (
<div
onClick={props.toggleNavigation}
className="tooltip color-bg"
style={{
display: props.visible && state.mouseMoved ? "block" : "none",
opacity: props.visible && state.mouseMoved ? "1" : "0",
top: state.yPosition + props.offsetY,
left: state.xPosition + props.offsetX,
}}
>
Close Menu
</div>
)
}
export default NavigationCloseMouseButton
Setting Defaults
You can destructure individual props from the props object (the argument of the function component). While destructuring, you can use the = operator to set a default value for when this prop is not set.
const NavigationCloseMouseButton = ({ visible = true, offsetX = 0, offsetY = 0, toggleNavigation }) => {
Updating a Listener
I'm sure there a lots of great answers about this so I won't go into too much detail.
You want to handle adding and removing the listener from inside your useEffect. You should use a useEffect cleanup function for the final remove. We don't want to be adding and removing the same listener so we can memoize it with useCallback.
I'm not sure what you are trying to do with listenerActive. This could be a prop, but it also seems a bit redundant with visible. I don't know that we need this at all.
Calculating Offset
I also don't know that it makes sense to pass offsetX and offsetY as props. We need the mouse to be on top of the tooltip in order for it to be clickable. We can measure the tooltip div inside this component and deal with it that way.
// ref to DOM node for measuring
const divRef = useRef<HTMLDivElement>(null);
// can caluculate offset instead of passing in props
const offsetX = -.5 * (divRef.current?.offsetWidth || 0);
const offsetY = -.5 * (divRef.current?.offsetHeight || 0);
Animation
Setting the style property display as "block" or "none" makes it hard to do any sort of CSS transition. Instead, I recommend that you handle style switching by changing the className. You could still set display: block and display: none on those classes, but I am choosing to use transform: scale(0); instead.
Code
const NavigationCloseMouseButton = ({
visible = true,
toggleNavigation
}) => {
// state of the movement
const [state, setState] = useState({
xPosition: 0,
yPosition: 0,
mouseMoved: false
});
// memoized event listener
const getTooltipPosition = useCallback(
// plain event, not a React synthetic event
({ clientX: xPosition, clientY: yPosition }) => {
setState({
xPosition,
yPosition,
mouseMoved: true
});
},
[]
); // never re-creates
useEffect(() => {
// don't need to listen when it's not visible
if (visible) {
window.addEventListener("mousemove", getTooltipPosition);
} else {
window.removeEventListener("mousemove", getTooltipPosition);
}
// clean-up function to remove on unmount
return () => {
window.removeEventListener("mousemove", getTooltipPosition);
};
}, [visible, getTooltipPosition]); // re-run the effect if prop `visible` changes
// ref to DOM node for measuring
const divRef = useRef(null);
// can caluculate offset instead of passing in props
const offsetX = -.5 * (divRef.current?.offsetWidth || 0);
const offsetY = -.5 * (divRef.current?.offsetHeight || 0);
// don't show until after mouse is moved
const isVisible = visible && state.mouseMoved;
return (
<div
ref={divRef}
onClick={toggleNavigation}
// control most styling through className
className={`tooltip ${isVisible ? "tooltip-visible" : "tooltip-hidden"}`}
style={{
// need absolute position to use top and left
position: "absolute",
top: state.yPosition + offsetY,
left: state.xPosition + offsetX
}}
>
Close Menu
</div>
);
};
Other Uses
We can easily make this NavigationCloseMouseButton into a more flexible MovingTooltip by removing some of the hard-coded specifics.
Get the contents from props.children instead of always using "Close Menu"
Accept a className as a prop
Change the name of toggleNavigation to onClick
Code Sandbox Demo

How to process mouseMove event in parent in React?

I am trying to implement drag and drop in React and using SVG elements. The problem is mouseMove does not get triggered if the user moves the mouse too fast. It basically loses the dragging quite frequently. To solve this I think I need to handle the mouseMove in the parent but not sure how to do this with React. I tried several different approaches to no avail.
I tried addEventListener('mousemove', ...) on the parent using a ref, but the problem is that the clientX is a different coordinate system than the current component. Also, the event handler does not have access to any of the state from the component (event with arrow functions). It maintains a stale reference to any state.
I tried setting the clientX and clientY in a context on the parent and then pulling it in from the DragMe component but it is always undefined the first time around for some strange reason even though I give it a default value.
Here's the code I'm working with:
const DragMe = ({ x = 50, y = 50, r = 10 }) => {
const [dragging, setDragging] = useState(false)
const [coord, setCoord] = useState({ x, y })
const [offset, setOffset] = useState({ x: 0, y: 0 })
const [origin, setOrigin] = useState({ x: 0, y: 0 })
const xPos = coord.x + offset.x
const yPos = coord.y + offset.y
const transform = `translate(${xPos}, ${yPos})`
const fill = dragging ? 'red' : 'green'
const stroke = 'black'
const handleMouseDown = e => {
setDragging(true)
setOrigin({ x: e.clientX, y: e.clientY })
}
const handleMouseMove = e => {
if (!dragging) { return }
setOffset({
x: e.clientX - origin.x,
y: e.clientY - origin.y,
})
}
const handleMouseUp = e => {
setDragging(false)
setCoord({ x: xPos, y: yPos })
setOrigin({ x: 0, y: 0 })
setOffset({ x: 0, y: 0 })
}
return (
<svg style={{ userSelect: 'none' }}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseUp}
>
<circle transform={transform} cx="0" cy="0" r={r} fill={fill} stroke={stroke} />
</svg>
)
}
After much experimentation I was able to addEventListener to the parent canvas. I discovered that I needed to useRef in order to allow the mousemove handler to see the current state. The problem I had before was that the handleParentMouseMove handler had a stale reference to the state and never saw the startDragPos.
This is the solution I came up with. If anyone knows of a way to clean this up it would be much appreciated.
const DragMe = ({ x = 50, y = 50, r = 10, stroke = 'black' }) => {
// `mousemove` will not generate events if the user moves the mouse too fast
// because the `mousemove` only gets sent when the mouse is still over the object.
// To work around this issue, we `addEventListener` to the parent canvas.
const canvasRef = useContext(CanvasContext)
const [dragging, setDragging] = useState(false)
// Original position independent of any dragging. Updated when done dragging.
const [originalCoord, setOriginalCoord] = useState({ x, y })
// The distance the mouse has moved since `mousedown`.
const [delta, setDelta] = useState({ x: 0, y: 0 })
// Store startDragPos in a `ref` so handlers always have the latest value.
const startDragPos = useRef({ x: 0, y: 0 })
// The current object position is the original starting position + the distance
// the mouse has moved since the start of the drag.
const xPos = originalCoord.x + delta.x
const yPos = originalCoord.y + delta.y
const transform = `translate(${xPos}, ${yPos})`
// `useCallback` is needed because `removeEventListener`` requires the handler
// to be the same as `addEventListener`. Without `useCallback` React will
// create a new handler each render.
const handleParentMouseMove = useCallback(e => {
setDelta({
x: e.clientX - startDragPos.current.x,
y: e.clientY - startDragPos.current.y,
})
}, [])
const handleMouseDown = e => {
setDragging(true)
startDragPos.current = { x: e.clientX, y: e.clientY }
canvasRef.current.addEventListener('mousemove', handleParentMouseMove)
}
const handleMouseUp = e => {
setDragging(false)
setOriginalCoord({ x: xPos, y: yPos })
startDragPos.current = { x: 0, y: 0 }
setDelta({ x: 0, y: 0 })
canvasRef.current.removeEventListener('mousemove', handleParentMouseMove)
}
const fill = dragging ? 'red' : 'green'
return (
<svg style={{ userSelect: 'none' }}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
>
<circle transform={transform} cx="0" cy="0" r={r} fill={fill} stroke={stroke} />
</svg>
)
}
Just to isolate the code that works here:
First go ahead and figure out what your top-level parent is in your render() function, where you may have something like:
render() {
return (
<div ref={(parent_div) => { this.parent_div = parent_div }}}>
// other div stuff
</div>
)
}
Then, using the ref assignment as from above, go ahead and assign the event listener to it like so:
this.parent_div.addEventListener('mousemove', function (event) {
console.log(event.clientX)
}

How to remove React hook in certain conditions?

Can't find a way to remove a hook when going to the next page section.
I've created a "useMousePosition" hook that tracks mouse position and returns mouse coordinates winch I use to transform some <div/>'s position. When scrolling down the page there's no need to transform the <div/> so I want to remove this useMousePosition hook.
useMouseHook
function useMousePosition() {
let [mousePosition, setMousePosition] = useState({
x: null,
y: null
});
function handleMouseMove(e) {
setMousePosition({
x: e.pageX,
y: e.pageY
});
}
useEffect(() => {
window.addEventListener("mousemove", handleMouseMove);
return () => {
window.removeEventListener("mousemove", handleMouseMove);
};
}, []);
return mousePosition;
}
Use hook in a component like so
let { x, y } = useMousePosition();
I need to remove a hook when user has scrolled to the next page section (Component with hook doesn't unmount)
The way I understood the question is that, you want to stop tracking the mouse movement.
If my understanding is correct, you can pass a flag to start/top tracking the mouse movement.
This demo shows that you can turn on/off the mouse tracking and
You can follow along
You can simply pass a variable, which you can check within your useEffect.
function useMousePosition(shouldTrack = true) {
let [mousePosition, setMousePosition] = useState({
x: null,
y: null
});
function handleMouseMove(e) {
setMousePosition({
x: e.pageX,
y: e.pageY
});
}
useEffect(() => {
if (!shouldTrack) return;
window.addEventListener("mousemove", handleMouseMove);
return () => {
window.removeEventListener("mousemove", handleMouseMove);
};
}, [shouldTrack]);
return mousePosition;
}
function App() {
const [useMouse, setUseMouse] = useState(true);
let { x, y } = useMousePosition(useMouse);
useEffect(() => {
console.log(`x, y`, x, y);
}, [x, y]);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<button onClick={() => setUseMouse(_ => !_)}>
Tracking Mouse Movement is {useMouse ? "On" : "Off"}
</button>
</div>
);
}
Clicking on the button toggles the track status.
And for "removing the hook", you can't as it's embedded in your Function Component.
You can at least prevent the "side effect" from running using a condition.
⚠ Note that useEffect has a dependency as [shouldTrack].
You need to specify conditions in handleMouseMove
In the next solution, you stop render outside the pink border and remove the listener under the black line.
Note: added useCallback and dep array because of unnecessary renderings.
const isInsideBox = ({ pageX, pageY }) =>
LEFT <= pageX && pageX <= RIGHT && TOP <= pageY;
function useMousePosition() {
let [mousePosition, setMousePosition] = useState({
x: null,
y: null
});
const handleMouseMove = useCallback(
e => {
isInsideBox(e) && // Add Condition for Border
setMousePosition({
x: e.pageX,
y: e.pageY
});
e.pageY >= BOTTOM && // Add Condition for Black Line
window.removeEventListener("mousemove", handleMouseMove);
},
[setMousePosition]
);
useEffect(() => {
window.addEventListener("mousemove", handleMouseMove);
return () => {
window.removeEventListener("mousemove", handleMouseMove);
};
}, [handleMouseMove]);
return mousePosition;
}

Resources