I have read through the entire react-spring docs and there doesn't seem to be a clear way to do this.
My attempt:
import React, { useRef, useState } from "react"
import { animated, useSpring } from "react-spring"
const App = () => {
const scrollDestinationRef = useRef()
const [elementScroll, setElementScroll] = useState(false)
const buttonClickHandler = () => setElementScroll(prevState => !prevState)
const scrollAnimation = useSpring({
scroll: elementScroll
? scrollDestinationRef.current.getBoundingClientRect().top
: 0
})
return (
<main>
{/* Click to scroll to destination */}
<animated.button
onClick={buttonClickHandler}
scrollTop={scrollAnimation.scroll}
style={{
height: "1000px",
width: "100%",
backgroundColor: "tomato"
}}
>
Scroll to destination
</animated.button>
{/* Scroll destination */}
<div
ref={scrollDestinationRef}
style={{
height: "200px",
width: "200px",
backgroundColor: "green"
}}
></div>
</main>
)
}
export default App
I'm using a ref and hooks for my attempt.
The useRef is attached the scroll destination in-order to find its offset top from the website's ceiling.
I use useState to toggle between the state on click to trigger the scroll.
I use useSpring to trigger an animation that goes from 0 to the scroll destination's scroll top a.k.a. getBoundingClientRect().top.
Can anyone assist in solving this?
There doesn't to be much explanation online, thanks!
useSpring returns a function to set/update animated values. You can use that function to assign a new value to your animated variable. Then, you can use the onFrame property to update the scroll position.
Define your spring like this:
const [y, setY] = useSpring(() => ({
immediate: false,
y: 0,
onFrame: props => {
window.scroll(0, props.y);
},
config: config.slow,
}));
Then use setY function to start the spring, like this:
<button
onClick={() => {
setY({ y: scrollDestinationRef.current.getBoundingClientRect().top });
}}
>
Click to scroll
</button>
When you click the button it will assign a new value to y variable in your spring, and onFrame function will be called upon every update.
Note that we call window.scroll function from onFrame property in useSpring.
See working demo here.
Finally after getting through https://github.com/pmndrs/react-spring/issues/544, answer from Yannick Schuchmann here worked for me, I just had to change onFrame to onChange
I made a custom hook for the solution: https://github.com/TomasSestak/react-spring-scroll-to-hook
react-sring version: 9.0.0-rc.3
const targetElement = useRef(null)
const [, setY] = useSpring(() => ({ y: 0 }))
let isStopped = false
const onWheel = () => {
isStopped = true
window.removeEventListener('wheel', onWheel)
}
const scrollToTarget = () => {
const element = targetElement.current
const value = window.scrollY + element.getBoundingClientRect().top
window.addEventListener('wheel', onWheel)
setY({
y: value,
reset: true,
from: { y: window.scrollY },
onRest: () => {
isStopped = false
window.removeEventListener('wheel', onWheel)
},
onFrame: props => {
if (!isStopped) {
window.scroll(0, props.y)
}
}
})
}
This version also allows the user to break out of the forced scrolling by using wheel event. (I personally hate forced scrolling :D)
useSpring(fn) does return a stop method besides the set. But I couldn't make it work with that. I posted it to a related github issue. If you read this, maybe there's a good answer for that already :)
For now it uses a little workaround with isStopped flag.
UPDATE:
Seems like there is actually something fishy with stop fn which should be addressed in v9 canary. Haven't tested since v9 is not stable yet.
Related
i have this function component as part of my project. i haven't called any hook inside loops, conditions, or nested functions as stated in the react document. i also have placed my hooks before any return statement so that react can reach each of them. yet i get this error which says Uncaught Error: Rendered fewer hooks than expected. This may be caused by an accidental early return statement.
any idea what the problem is ?
function ScrollRenderingCom(props) {
const { children, aproximateHeight = 200} = props;
const [isIntersecting, setIsIntersecting] = useState(false);
const fakeComponent = useRef(null);
const callback = useCallback((entries) => {
const [entry] = entries;
if (entry.isIntersecting) {
setIsIntersecting(true)
}
}, []);
useEffect(() => {
const options = {
root: null,
rootMargin: '0px',
threshold: 0,
}
const observer = new IntersectionObserver(callback, options);
observer.observe(fakeComponent.current);
return () => {
observer.disconnect();
}
}, [callback]);
return (
isIntersecting ? children
:
<div
ref={fakeComponent}
className={children.type().props.className}
style={{ visibility: 'hidden',
minHeight: aproximateHeight + 'px' }}
>
</div>
)
}
and this is the component stack trace that react prints to the console which specifies the error occurred in this component
The above error occurred in the ScrollRenderingCom component:
at ScrollRenderingCom (http://localhost:3000/static/js/bundle.js:3468:5)
at div
at PostSection1
at Home
at Routes (http://localhost:3000/static/js/bundle.js:229775:5)
at div
at Router (http://localhost:3000/static/js/bundle.js:229708:15)
at BrowserRouter (http://localhost:3000/static/js/bundle.js:228517:5)
at App
Update:
from negative reflections, i noticed that my sample code is insufficient to reproduce the issue. hence i provide the above component with a single Child component so that it can be run and inspected in isolation.
here is the Child component
function Child() {
const [successMessage, setSuccessMessage] = useState(false);
useEffect(() => {
if (successMessage) {
alert('this app works correctly')
}
return () => {setSuccessMessage(false)}
})
const handleOnClick = () => {
setSuccessMessage(true);
}
return(
<div className="child"
style={{width: '500px',
height: '200px',
backgroundColor: 'green',
color: 'white'
}}
>
<h1>this component should be rendered when
the empty blue div element completely
enters the viewport
</h1>
<h3>if you see the above message click Yes</h3>
<button onClick={handleOnClick} >Yes</button>
</div>
)
}
and here is the final live demo that you can inspect the problem.
please let me know of any further deficiencies in my question and give me a chance to fix them before downing me
By calling children.type you create that component and that's causing hooks to be run conditionally.
Try changing:
children.type().props.className
to
children.props.className
No need to create the component here
https://codesandbox.io/s/demo-forked-egofes?file=/src/ScrollRenderingCom.js:816-825
In my home screen I want to auto hide my header in 2 seconds, then I will have a button to show the header when pressed. I have tried with HomeStack.Screen but could not achieve it, I have to create my custom header called HeaderHomeComponent.js and imported it on my homescreen, still I could not achieve it. Please I need help on this issue.
Here is my code:
const [showHeader, setShowHeader] = useState(true);
const onRecord = async () => {
if (isRecording) {
camera.current.stopRecording();
} else {
setTimeout(() => setIsRecording && camera.current.stopRecording(), 23*1000);
const data = await camera.current.recordAsync();
}
};
const visibility = () => {
setTimeout(() => setShowHeader(false), 2000);
}
return (
<View style={styles.container}>
<RNCamera
ref={camera}
type={cameraType}
flashMode={flashMode}
onRecordingStart={() => setIsRecording(true)}
onRecordingEnd={() => setIsRecording(false)}
style={styles.preview}
/>
<HeaderHomeComponent />
You can create a function with useeffect.
Make sure you passs show and handleClose functions from Parent. (Example given below).
const MessageBox = (props) => {
useEffect(() => {
if (props.show) {
setTimeout(() => {
props.handleClose(false);
}, 3000);
}
}, [props.show]);
return (
<div className={`messageBox ${props.show ? "show" : null}`}>
{props.message}
</div>
);
};
UseEffect will be called everytime props.show state will change. And we only want our timer to kick in when the show becomes true, so that we can hide it then.
Also, now to use this, it's simple, in any component.
const [showMessageBox, setShowMessageBox] = useState(false);
return(
<MessageBox
show={showMessageBox}
handleClose={setShowMessageBox} />
);
Also, make sure to handle css, part as well for show and hide.
Simple Example below.
.messageBox {
display: none;
}
.messageBox.show {
display: block;
}
Hope this helps, :-)
You need to do something like this as Mindaugas Nakrosis mentioned in comment
const [showHeader, setShowHeader] = useState(true);
useEffect(() => {
setTimeout(() => setShowHeader(false), 2000);
}, []);
In return where your header is present
{
showHeader && <HeaderHomeComponent/>;
}
I think the approach gonna fit "auto hide and show in 2 seconds", is using Animetad opacity, and giving fix height or/and z-index (as fit you) to the element
// HeaderHomeComponent.js
const animOpacity = useRef(new Animated.Value(1)).current // start with showing elem
//change main view to
<Animated.View
style={{ ...yourStyle... ,
opacity: animOpacity,
}}
>
and then for creating the animation somewhere
() => {
Animated.timing(animOpacity, {
toValue: +(!animOpacity), // the numeric value of not current
duration: 2000, // 2 secs
}).start();
}}
The hieraric location of the declaration of the ref should control usage as calling the effect. maybe you can create useEffect inside the header that can determine if it should be visible or not depends navigation or some other props.
hope its helpful!
I'm attempting to create an animation in which one element fades based upon the scroll position of another. I was able to get the scrolling element to work using React Spring, but I'm struggling to wrap my head around how to leverage state hooks without conditionals and only being able to set state at a component top level.
SandBox
const HomeView = () => {
const ref = useRef();
const [isVisible, setVisible] = useState(true);
const [{ offset }, set] = useSpring(() => ({ offset: 0 }));
const calc = (o) => {
if (o < 1004) {
return `translateY(${o * 0.08}vh)`;
} else {
// this won't work b/c im trying to useState in a Fn
setVisible(false);
return `translateY(${1012 * 0.08}vh)`;
}
};
const handleScroll = () => {
const posY = ref.current.getBoundingClientRect().top;
const offset = window.pageYOffset - posY;
set({ offset });
};
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
});
return (
<div ref={ref} className="home-view" style={homeViewStyles}>
<div style={topSectionStyles} className="top-content-container"></div>
<animated.div
className="well-anim"
style={{
width: "100vw",
height: "500px",
transform: offset.interpolate(calc),
zIndex: 300,
top: "340px",
position: "absolute"
}}
>
<h1>Well,</h1>
</animated.div>
{/* Trying to fade this component when above animated.div is right above it */}
<h2 style={{ paddingTop: "90px" }} fade={isVisible}>
Hello There!
</h2>
{/***************************************/}
</div>
);
};
You are almost there. I think the problem here is with the fade attribute. The setVisible function is invoked all right. I would introduce a second spring to deal with the opacity with the h2 element, based on the state of the isVisible variable.
const {opacity} = useSpring({opacity: isVisible ? 1 : 0});
<animated.h2 style={{ paddingTop: "90px", opacity }} >
Hello There!
</animated.h2>
https://codesandbox.io/s/wild-dew-tyynk?file=/src/App.js
I have the following Redux Reducer to handle the offset of an infinite scroll component:
const offset = handleActions(
{
[questionListTypes.ON_QUESTIONS_SCROLL]: state => state + QuestionsLoadChunkTotal,
[combineActions(questionListTypes.RESET_QUESTIONS_OFFSET)]: () => {
document.getElementById('question-list-infinite-scroll').scrollTop = 0;
return 0;
},
},
0,
);
When the offset of the component resets I want to scroll the HTML element to the top. I have added the following line in the reducer to handle this:
document.getElementById('question-list-infinite-scroll').scrollTop = 0;
This doesn't feel right to me to put it here because it has nothing to do with my state. Is there a better way to handle this situation?
You may use a Redux middleware, which purpose is to handle side effects.
It receives every action that goes through and enables us to have any side effect.
const scrollReseter = store => next => action => {
next(action);
if (action.type === combineActions(questionListTypes.RESET_QUESTIONS_OFFSET)) {
document.getElementById('question-list-infinite-scroll').scrollTop = 0;
}
}
See https://redux.js.org/advanced/middleware/
You can use a ref to get a reference to the DOM element and use an effect to manipulate that element when a certain value in the state changes.
Here is an example using local state:
const App = () => {
//this would be data that comes from state
// maybe with useSelector or with connect
const [scrollToTop, setScrollToTop] = React.useState(0);
//create a ref to the element you want to scroll
const scrollRef = React.useRef();
//this would be an action that would set scrollToTop with a new
// value
const goToTop = () => setScrollToTop((val) => val + 1);
//this is an effect that runs every time scrollToTop changes
// it will run on mount as well so when scrollToTop is 0 it
// does nothing
React.useEffect(() => {
if (scrollToTop) {
scrollRef.current.scrollTop = 0;
}
}, [scrollToTop]);
return (
<div
ref={scrollRef}
style={{ maxHeight: '250px', overflow: 'scroll' }}
>
{[...new Array(10)].map((_, key) => (
<h1
key={key}
onClick={goToTop}
style={{ cursor: 'pointer' }}
>
click to scroll to top
</h1>
))}
</div>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
I'm trying to migrate my app from old one to the new React-trancition-group API, but it's not so easy in case of using the manual <Transition> mode for transition creation of the particular React component.
My animation logic is:
we have an array of the components, each child of him comes one by one in the <TransitionGroup> API by onClick action. Where every new income component smoothly replace and hide previous one, which is already present in <Transition> API.
I almost finish unleash this tangle in react-trancition-group .v2, but one thing is still not solved - the component, that already been in <Transition> API does not disappear after the new one is overlayed him, which was automatically happen in react-trancition-group .v1 instead. So now they all just stack together...
So, maybe you can look on my code and luckly say where is my problem located...
I'll be grateful for any help. Thanks for your time
My code:
import React, { Component } from 'react'
import { findDOMNode } from 'react-dom'
import { TweenMax, Power1 } from 'gsap'
import { Transition } from 'react-transition-group'
class Slider extends Component {
_onEntering = () => {
const { id, getStateResult, isCurrentRow, removeAfterBorder, calcHeight } = this.props
const el = findDOMNode(this)
const height = calcHeight(el)
TweenMax.set(el.parentNode, {
height: `${height}px`
})
TweenMax.fromTo(
el,
0.5,
{
y: -120,
position: 'absolute',
width: `${100}%`,
zIndex: 0 + id
},
{
y: 0,
width: `${100}%`,
zIndex: 0 + id,
ease: Power1.easeIn
}
)
}
_onEntered = () => {
const { activeButton, removeAfterBorder, getCurrentOutcome } = this.props
findDOMNode(this)
}
_onExiting = () => {
const el = findDOMNode(this)
TweenMax.to(el, 2, {
onComplete: () => {
el.className = ''
}
})
}
_onExited = () => {
const { getStateResult } = this.props
getStateResult(true)
}
render() {
const { children } = this.props
return (
<Transition
in={true}
key={id}
timeout={2000}
onEntering={() => this._onEntering()}
onEntered={() => this._onEntered()}
onExiting={() => this._onExiting()}
onExited={() => this._onExited()}
unmountOnExit
>
{children}
</Transition> || null
)
}
}
export default Slider
```
So, you problem is very typically. As you written here: the component, that already been in <Transition> API does not disappear after the new one is overlayed him - it's happen because you does not changing the status flag in for Transition Component. You set always true for him, it's not a right.
By the way, you need to understand what is you trying to do in your code. There is a big difference between methods <Transition></Transition> and <TransitionGroup><CSSTransition></CSSTransition><TransitionGroup>. You need to use pure <Transition></Transition> API only for very rare cases, when you need explicitly manipulate animation scenes.
As I see in you code you trying to replace one component by the other and in this case you need to use the second method that I have provided above.
So, try this:
import { TransitionGroup, CSSTransition } from 'react-transition-group'
...some code
return (
<TransitionGroup>
<CSSTransition
key={id}
timeout={2000}
onEntering={() => this._onEntering()}
onEntered={() => this._onEntered()}
onExiting={() => this._onExiting()}
onExited={() => this._onExited()}
unmountOnExit
>
{children}
</CSSTransition> || null
</TransitionGroup>
)
It should help you and start to render Compenents by normal overlaying each other.