Here I have a timer minute state and a scroll container, where the state value changes based on the scroll position and vice versa:
const Container = ({ minutes, setMinutes}) => {
draggableElement = useRef(null);
const handleScroll = () => {
const scrollableWidth =
draggableElement.current.container.current.scrollWidth -
draggableElement.current.container.current.offsetWidth;
const amountScrolled = draggableElement.current.scrollLeft;
const rangeValue = (amountScrolled / scrollableWidth) * 30;
setMinutes(Math.round(rangeValue));
}
useEffect(() => {
draggableElement.current.container.current.scrollLeft = minutes * 20;
}, [minutes]);
return (
<ScrollContainer
className="container"
ref={draggableElement}
onScroll={handleScroll}
id="scrolling-container"
vertical={false}
style={{
display: "grid",
gridTemplateColumns: "repeat(100, 0.25em)",
//border: "1px solid yellow",
width: "4em",
height: "50%",
alignItems: "center",
cursor: "grab",
marginBottom: "1rem",
}}
>
<Tick >1</Tick>
<Tick >2</Tick>
<Tick >3</Tick>
<Tick >4</Tick>
<Tick >5</Tick>
//more ticks here
</ScrollContainer>
);
};
on top of that, I also have counter buttons where you can increase/decrease the minute state value:
<AddButton onClick={()=> setMinutes(minutes + 1)}></AddButton>
<SubtractButton onClick={()=> setMinutes(minutes - 1)}></SubtractButton>
Now, everything seemed to be working just fine, until I tried to rapidly click the AddButton/SubtractButton. I have observed the following problems:
If I rapidly click it without using the scroll container yet, the state becomes NaN.
If I rapidly click it after scrolling to a specific position, the state value resets to the value based on the scroll position. Example: I scroll to position value 3, state value becomes 3. I click AddButton rapidly, state becomes 4 for a split second then resets to 3.
As I mentioned before, it works just fine if I click at a slow pace. Also, scrolling rapidly doesn't cause any problems.
How can I fix this?
Related
Is it possible to register onScroll event on a react element which does not have scrollbar / overflow ?
I wish to create zoom on a picture with mouse wheel scrolling.
I tried
<div style={{position: "relative", width: "100%", height:"100%"}} onScroll={ () => console.log("TEST")}>
this wont fire event, neither will onWheel. If I change event to onClick everything works.
I know it's possbile, because there is a React components which make it work, I tried react wheel handler and it works. But how do they make it register mouse wheel events when there is no overflow or scrollbars?
EDIT:
I got it to work when I manually added eventlistener to componentDidMount, this is my current component. Is there a way I could make synthetic event onWheel work? I tried to add onWheel to div's and img but it wont fire.
class Zoom extends Component {
state = {
size: 20,
}
componentDidMount() {
window.addEventListener('wheel', this.handleScroll, true);
}
handleScroll = e => {
e.preventDefault();
console.log(e)
if (e.deltaY > 0 && this.state.size < 50) {
this.setState({size: this.state.size + 1})
} else if (e.deltaY < 0 && this.state.size > 20) {
this.setState({size: this.state.size - 1})
}
}
render = () =>
<div className="livetods-product-image-container" style={{
top: openAsModal() ? '25%' : '20%',
left: openAsModal() ? '30%' : '',
right: openAsModal() ? '30%' : '',
width: this.state.size + "%"
}}>
<div className="livetods-product-image-button-container" >
<div></div>
<h6 className="livetods-product-image-name">{this.props.name}</h6>
<span className="livetods-modal-header-close-button" onClick={e => this.props.hideImage()}>×</span>
</div>
<div style={{position: "relative", width: "100%", height:"100%"}}>
<div style={{position: "absolute", padding: "5px"}}>
<FontAwesomeIcon icon={faSearchPlus} color={'red'} style={{ width: '20px', height: '20px', pointerEvents:"none"}}/>
</div>
<img src={this.props.src} style={{borderRadius: "5px"}}/>
</div>
</div>
}
Use the onWheel event and its properties instead of onScroll to achieve the same result.
onScroll event no longer bubbles up in react. This question is pretty similar.
onWheel works for both trackpad scrolling as well as mouse wheel scrolling.
Here is a simple example that shows onWheel working with your provided code sample.
UPDATE:
I have changed the codesanbox example to reflect your class component. onWheel works in it too now. The issue you could be facing earlier was a warning like this one:
Warning: This synthetic event is reused for performance reasons. If you're seeing this, you're accessing the method `isPropagationStopped` on a released/nullified synthetic event. This is a no-op function. If you must keep the original synthetic event around, use event.persist(). See https://reactjs.org/docs/legacy-event-pooling.html for more information.
The workaround to which is adding e.persist() to your onScroll function, as shown in my example.
A simple use-case is to allow a user to either click buttons to paginate in a slider, or drag. Both events call the same paginate function with a param to either go forward or back--simple stuff.
However, the trigger from drag seems to cause bizarre behavior where the slider wants to start the animation from several slides back as if it ignores the updated props. This doesn't happen when using the buttons and both use the same simple paginate call.
Any tips appreciated.
Minimal example:
export default function App() {
const [position, setPosition] = useState<number>(0);
const paginate = (direction: Direction) => {
setPosition((prev) => {
return direction === Direction.Forward
? Math.max(-800, prev - 200)
: Math.min(0, prev + 200);
});
};
return (
<div className="App">
<Slider>
<Wrapper
animate={{ x: position }}
transition={{
x: { duration: 1, type: "tween" }
}}
drag="x"
dragConstraints={{
top: 0,
left: 0,
right: 0,
bottom: 0
}}
onDragEnd={(e, { offset, velocity }) => {
const swipe = swipePower(offset.x, velocity.x);
if (swipe < -swipeConfidenceThreshold) {
paginate(Direction.Forward);
} else if (swipe > swipeConfidenceThreshold) {
paginate(Direction.Back);
}
}}
>
<Slide>1</Slide>
<Slide className="alt">2</Slide>
<Slide>3</Slide>
<Slide className="alt">4</Slide>
<Slide>5</Slide>
</Wrapper>
</Slider>
<button onClick={() => paginate(Direction.Back)}>prev</button>
<button onClick={() => paginate(Direction.Forward)}>next</button>
</div>
);
}
Codesandbox Demo
I have to say, this problem is quite interesting. However, I think I figured out a way for you to handle this. One thing I noticed is that if you comment out
onDragEnd={(e, { offset, velocity }) => {
// const swipe = swipePower(offset.x, velocity.x);
// if (swipe < -swipeConfidenceThreshold) {
// paginate(Direction.Forward);
// } else if (swipe > swipeConfidenceThreshold) {
// paginate(Direction.Back);
// }
}}
the entire onDragEnd prop function, this example still doesn't work, since by the looks of things, the draggable component is not respecting your offset.
I realized that at this point, the problem is the internal state of the component is out of sync with your state. And would you look at that, the Framer Motion API actually provides a way to inspect this.
https://www.framer.com/api/motion/motionvalue/#usemotionvalue
It's the hook useMotionValue() which allows us to see what's actually happening. Turns out, our value is being set wrong when the user starts dragging:
useEffect(
() =>
motionX.onChange((latest) => {
console.log("LATEST: ", latest);
}),
[]
);
We can see this, because the state "jumps" to 200 as soon as we start dragging.
So fixing in theory is easy, we just need to make sure to let that value "know" about our offset, and that way it's gonna start with the proper offset in mind!
Anyway, that was my thought process, here's the solution, all you need to do is set the left constraint to make it work:
dragConstraints={{
top: 0,
left: position,
right: 0,
bottom: 0
}}
And tada! This makes it work. Here's my working solution: https://codesandbox.io/s/lingering-waterfall-2tsfi?file=/src/App.tsx
The error I receive is:
[Invariant Violation: [369,"RCTImageView",31,{"source":[{"__packager_asset":true,"width":52,"height":80,"uri":"...assets/assets/glass.png?platform=ios&hash=b11854c2a0e54026f26338d7c7cbf7fc?platform=ios&dev=true&hot=false&minify=false","scale":1}],"width":"<>","height":"<>","overflow":"hidden","marginTop":4,"marginHorizontal":"<>","resizeMode":"contain"}] is not usable as a native method argument. It appears that the width, height and marginHorizontal are not available on render - I calculate these values based on screen size and the error disappears when I remove the dynamic styling. This error only happens when I use useEffect. I make the dynamic styling based on the width of a container:
<View
style={styles.container}
onLayout={(event) => {
setContainerWidth(event.nativeEvent.layout.width);
}}
>
{glasses.map(() => glassContainer())}
</View>
This is the component which uses the dynamic styling.
const ratio = 78 / 50;
const width = containerWidth / 12;
const glassContainer = () => (
<View style={styles.singleGlassContainer}>
<Image
source={require("../../assets/glass.png")}
style={[
styles.glass,
{
width: width,
height: width * ratio,
marginHorizontal: containerWidth / 72,
},
]}
resizeMode="contain"
/>
</View>
);
Then finally I use useEffect like below
useEffect(() => {
renderGlasses();
}, [hydrationTarget]);
The renderGlasses function:
const renderGlasses = () => {
//Calculates amounth of glasses to reach target
const targetGlasses = Math.ceil(hydrationTarget / glassVolume);
for (let i = 0; i < targetGlasses; i++) {
setGlasses((prevArray) => [...prevArray, i]);
}
};
I think what goes wrong is useEffect fires before the dynamic styling is calculated, but I might be wrong here. Anyone knows what is causing the error, and how to fix it? If any additional code is needed please let me know.
I want to show an AdMobInterstitial-Ad in my App before pushing to the next screen.
For now, I am using this method:
show the Ad (which takes 1-2 seconds to load)
pushing to the next screen
onpress={ () =>{
(title.slice(0, 4) === "X-01"
? navigation.push(...)
: navigation.push(...));
showAsyncAD();
}
}
The ad loads asynchroniously so the app pushes to the next screen, then shows the ad.
I want it to first load the ad then push to the next screen.
I tried this:
onpress={() =>
showAsyncAD().then(
() => title.slice(0, 4) === "X-01"
? navigation.push(...)
: navigation.push(...)
);
}
}
This does indeed work but until the ad comes up the user can press buttons which he is not allowed to. He can actually just press the button again and get to the screen before the ad starts to play.
How can I freeze the App until the ad comes up so the user wont do anything he is not allowed to?
If a loading circle is possible with this it would be the best option!
Thanks for any inputs!!!
EDIT: I want to make the whole App to freeze until the ad is shown
You can use state value which shows async action processing
const [processing, setProcessing] = useState(false);
setProcessing(true)
showAsyncAD().then(
() => {
setProcessing(false)
title.slice(0, 4) === "X-01"
? navigation.push(...)
: navigation.push(...)
})
And disable button(s) that you don't want to be clicked while async action is performed. for example;
<TouchableOpacity disabled={processing}>
</TouchableOpacity>
UPDATE
If you want to disable the whole screen, you can show a transparent overlay View on the screen. It will have the full width and height of the screen.
return (
<View style={{
position: 'relative',
...
}}>
// your views here
{processing && <View style={{
position: 'absolute',
width: '100%',
height: '100%',
backgroundColor: 'transparent'
}} />}
</View>
)
I am trying to set up a slider inside a horizontal ScrollView that would allow me to scroll the page faster. I am able to link position of the page to the value of the slider, so that when I scroll the page, the thumb of the slider moves accordingly.
I am using React Native Slider and a ScrollView.
Here is the result that I am unfortunately having.
I am quite new to RN, so I am probably missing something important here.
class Comp extends Component {
state = {
width : 0,
value : 0,
cursor : 0
}
moveTheCursor = (val) => {
this.scrollView.scrollTo({x: val, y: 0, animated: true})
}
scrollTheView = (event) => {
this.setState({
value : Math.floor(event.nativeEvent.contentOffset.x),
cursor : Math.floor(event.nativeEvent.contentOffset.x)
})
}
checkWidth = ({nativeEvent}) => {
arrayWidth.push(nativeEvent.layout.width)
let ww = (arrayWidth[0] * this.props.data.length) - Math.round(Dimensions.get('window').width);
this.setState({
width : ww,
})
}
render() {
return (
<React.Fragment>
<ScrollView
ref={ref => this.scrollView = ref}
horizontal
style={styles.car}
scrollEventThrottle={1}
onScroll={this.scrollTheView}
showsHorizontalScrollIndicator={false}
decelerationRate={0}
//snapToInterval={200} //your element width
snapToAlignment={"center"}
>
</ScrollView>
<Slider
style={styles.containerSlide}
thumbImage={require("./../../assets/img/buttons/thumb.png")}
trackImage={require("./../../assets/img/buttons/bar.png")}
minimumValue={0}
maximumValue={this.state.width}
onValueChange={this.moveTheCursor}
value={this.state.cursor}
/>
</React.Fragment>
);
}
}
The problem is, when I use the thumb of the slider to scroll the page, it triggers the scroll that inevitably resets the position of the slider thumb, so it is not behaving correctly (flickers but it is mostly inaccurate).
Is there a way to fix this loop?
You can achieve desired behaviour using Slider's onSlidingStart and onSlidingComplete + ScrollView's scrollEnabled
It can look like
....
const [isSliding, setIsSliding] = useState(false);
....
<ScrollView scrollEnabled={!isSliding}>
<Slider
onSlidingStart={() => setIsSliding(true)}
onSlidingComplete={() => setIsSliding(false)}
/>
</ScrollView>