How to re-animate react-spring animation using hooks on button click? - reactjs

Following simple component from the official examples:
import {useSpring, animated} from 'react-spring'
function App() {
const props = useSpring({opacity: 1, from: {opacity: 0}})
return <animated.div style={props}>I will fade in</animated.div>
}
Question
How do I animate the fadeIn-effect (or any other animation) again for example when I click on a button or when a promise is resolved?

You can basically make two effect with useSpring and an event.
You can change the style for example the opacity depending on the state of an event.
You can restart an animation on state change. The easiest way to restart is to rerender it.
I created an example. I think you want the second case. In my example I rerender the second component with changing its key property.
const Text1 = ({ on }) => {
const props = useSpring({ opacity: on ? 1 : 0, from: { opacity: 0 } });
return <animated.div style={props}>I will fade on and off</animated.div>;
};
const Text2 = () => {
const props = useSpring({ opacity: 1, from: { opacity: 0 } });
return <animated.div style={props}>I will restart animation</animated.div>;
};
function App() {
const [on, set] = React.useState(true);
return (
<div className="App">
<Text1 on={on} />
<Text2 key={on} />
<button onClick={() => set(!on)}>{on ? "On" : "Off"}</button>
</div>
);
}
Here is the working example: https://codesandbox.io/s/upbeat-kilby-ez7jy
I hope this is what you meant.

Related

odd behavior with framer motion animate on presence

I'm working ona project management application , and I'm developing this feature where the project header can go into editing state where user can edit project title , this header has two child components HeaderContent and EditProjectForm :
>ProjectHeader
-->HeaderContent
-->EditProjectForm
my problem now is that when I fade out the HeaderContent the faded In EditProjectForm is intially pushed down then it jumps to its place , it seems that this happens because even though HeaderContent was faded out it still was affecting the dom structure
here is a short screen recording I just uploaded to further make things clear https://www.youtube.com/watch?v=UerYDuEcUWQ
Header component
const ProjectHeader=()=>{
const [isEditingState, setisEditingProject] = useState({value:false,triggerFrom:"PANEL_HEADER"})
return <div>
<HeaderContent {...{isEditingProject, setisEditingProject}} />
<EditProjectForm {...{isEditingProject, setisEditingProject}} />
</div>
}
HeaderContent
const HeaderContent =({isEditingProject, setisEditingProject})=>{
const [render, setrender] = useState(true)
useEffect(() => {
let ref=null
// if(isEditingProject.triggerFrom =="EDIT_PROJECT_FORM") if I have don this whenever I change isEditingProject.value from this component this setTimeout below will be fired and I want to only be fired when this compoent is unmounted
if(isEditingProject.triggerFrom =="EDIT_PROJECT_FORM"){
ref= setTimeout(() => {
setrender(!isEditingProject.value)
}, 200);//wait until edit form finishes its fade_out animtion
}
return ()=>ref && clearTimeout(ref)
}, [isEditingProject])
return <AnimatePresence initial={false} >
{
render
&&(<motion.div
animate={{opacity:1 ,y:0}}
initial={{opacity:1 ,y:0}}
exit ={{opacity:0 ,y:10}}
transition={{
duration:.2,
opacity: { type: "spring", stiffness: 100 },
}}>
//.. header links and buttons and the title
<button onClick={e=>{
setrender(false)
setisEditingProject({...isEditingProject,value:true,triggerFrom:"PANEL_HEADER"})
}} >edit</button>
}
</AnimatePresence >
}
EditProjectForm
const EditProjectForm =({isEditingProject, setisEditingProject})=>{
const [render, setrender] = useState(true)
useEffect(() => {
let ref =null
// if(isEditingProject.triggerFrom =="PANEL_HEADER") if I haven't don this whenever I change isEditingProject.value from this component this setTimeout below will be fired and I want to only be fired when this compoent is unmounted
if(isEditingProject.triggerFrom =="PANEL_HEADER"){
ref=setTimeout(() => {
setrender(isEditingProject.value)
}, 200);
}
return ()=>ref && clearTimeout(ref)
}, [isEditingProject.value])
return <AnimatePresence>
{
render && <motion.form
animate={{ opacity:1 ,y:0 }}
initial={{ opacity:1 ,y:10 }}
exit ={{ opacity:0 ,y:-10}}
transition={{
duration:.2,
opacity: { type: "spring", stiffness: 100 },
}}
>
/.. title input
<button onClick={e=>{
setrender(false)
setisEditingProject({...isEditingProject,value:true,triggerFrom:"EDIT_PROJECT_FORM"})
}} >edit</button>
</motion.form>
}
</AnimatePresence>
}
It's problem with css, not with framer. My advise is to wrap your component with div absolute position, where top:0;
Maybe, you have some flex divs which trigger this "strange" behaviour

How to execute two animations sequentially, using react-spring?

I tried chaining two springs (using useChain), so that one only starts after the other finishes, but they are being animated at the same time. What am I doing wrong?
import React, { useRef, useState } from 'react'
import { render } from 'react-dom'
import { useSpring, animated, useChain } from 'react-spring'
function App() {
const [counter, setCounter] = useState(0)
const topRef = useRef()
const leftRef = useRef()
const { top } = useSpring({ top: (window.innerHeight * counter) / 10, ref: topRef })
const { left } = useSpring({ left: (window.innerWidth * counter) / 10, ref: leftRef })
useChain([topRef, leftRef])
return (
<div id="main" onClick={() => setCounter((counter + 1) % 10)}>
Click me!
<animated.div id="movingDiv" style={{ top, left }} />
</div>
)
}
render(<App />, document.getElementById('root'))
Here's a codesandbox demonstrating the problem:
https://codesandbox.io/s/react-spring-usespring-hook-m4w4t
I just found out that there's a much simpler solution, using only useSpring:
function App() {
const [counter, setCounter] = useState(0)
const style = useSpring({
to: [
{ top: (window.innerHeight * counter) / 5 },
{ left: (window.innerWidth * counter) / 5 }
]
})
return (
<div id="main" onClick={() => setCounter((counter + 1) % 5)}>
Click me!
<animated.div id="movingDiv" style={style} />
</div>
)
}
Example: https://codesandbox.io/s/react-spring-chained-animations-8ibpi
I did some digging as this was puzzling me as well and came across this spectrum chat.
I'm not sure I totally understand what is going on but it seems the current value of the refs in your code is only read once, and so when the component mounts, the chain is completed instantly and never reset. Your code does work if you put in hardcoded values for the two springs and then control them with turnaries but obviously you are looking for a dynamic solution.
I've tested this and it seems to do the job:
const topCurrent = !topRef.current ? topRef : {current: topRef.current};
const leftCurrent = !leftRef.current ? leftRef : {current: leftRef.current};
useChain([topCurrent, leftCurrent]);
It forces the chain to reference the current value of the ref each time. The turnary is in there because the value of the ref on mount is undefined - there may be a more elegant way to account for this.

Updating functional component props doesn't change styling

I have this function component for rendering a span as a bar. When I render the component, I pass the prop scrolled as false. Then, jQuery updates the attribute to true when I have scrolled 150 pixels. I know it's not good practice to use jQuery, and I am migrating from it. For now, I want to get this to work as I am still learning functional components.
export default function Button(props) {
const [scrolled, setScrolled] = useState( props.scrolled );
useEffect(() => {
setScrolled(props.scrolled);
}, [scrolled]);
return (
<HamburgerButton scrolled={scrolled}>
<p>{scrolled}</p>
<span></span>
</HamburgerButton>
);
}
And this styled component definition:
const HamburgerButton = styled.div`
...
span {
background: ${props => props.scrolled === 'false' ? props.theme.white : props.theme.black};
}
...
`;
When I scroll, I see the attribute scrolled has changed in the DOM from 'false' to 'true' but the spans stay white. Also, the paragraph tag with {scrolled} doesn't change from false.
Issue
Your code memoizes the props.scrolled value in the hooks.
export default function Button(props) {
const [scrolled, setScrolled] = useState( props.scrolled ); // state initialized
useEffect(() => { // hook called first render
setScrolled(props.scrolled); // state updated with same value
}, [scrolled]); // state value never changes during life of component so effect hook never recomputes
return (
<HamburgerButton scrolled={scrolled}>
<p>{scrolled}</p>
<span></span>
</HamburgerButton>
);
}
Solution
You can directly pass prop to HamburgerButton
export default function Button(props) {
return (
<HamburgerButton scrolled={props.scrolled}>
<p>{props.scrolled}</p>
<span></span>
</HamburgerButton>
);
}
Or use the hooks and use the correct dependency
export default function Button(props) {
const [scrolled, setScrolled] = useState( props.scrolled );
useEffect(() => {
setScrolled(props.scrolled); // update state
}, [props.scrolled]); // with the value that changes here
return (
<HamburgerButton scrolled={scrolled}>
<p>{scrolled}</p>
<span></span>
</HamburgerButton>
);
}
Use a positive comparison for the true branch of a ternary and leverage javascript's truthy/falsey values. If scrolled is true, or any other truthy value, render black, if fasley, (false, 0, null, undefined) render white.
const HamburgerButton = styled.div`
...
span {
background: ${props => props.scrolled ? props.theme.black : props.theme.white};
}
...
`;
or
const HamburgerButton = styled.div`
...
span {
background: ${props => props.theme[props.scrolled ? 'black' : 'white']};
}
...
`;

Images Rerendering inside Styled Component when Chrome Dev Tools is open

This is a bit of a strange one and not sure why it's happening exactly.
When the component mounts, I call a function that in my application makes an HTTP request to get an array of Objects. Then I update 3 states within a map method.
enquiries - Which is just the response from the HTTP request
activeProperty - Which defines which object id is current active
channelDetails - parses some of the response data to be used as a prop to pass down to a child component.
const [enquiries, setEnquiries] = useState({ loading: true });
const [activeProperty, setActiveProperty] = useState();
const [channelDetails, setChannelDetails] = useState([]);
const getChannels = async () => {
// In my actual project,this is an http request and I filter responses
const response = await Enquiries;
const channelDetailsCopy = [...channelDetails];
setEnquiries(
response.map((e, i) => {
const { property } = e;
if (property) {
const { id } = property;
let tempActiveProperty;
if (i === 0 && !activeProperty) {
tempActiveProperty = id;
setActiveProperty(tempActiveProperty);
}
}
channelDetailsCopy.push(getChannelDetails(e));
return e;
})
);
setChannelDetails(channelDetailsCopy);
};
useEffect(() => {
getChannels();
}, []);
Then I return a child component ChannelList that uses styled components to add styles to the element and renders child elements.
const ChannelList = ({ children, listHeight }) => {
const ChannelListDiv = styled.div`
height: ${listHeight};
overflow-y: scroll;
overflow-x: hidden;
`;
return <ChannelListDiv className={"ChannelList"}>{children}</ChannelListDiv>;
};
Inside ChannelList component I map over the enquiries state and render the ChannelListItem component which has an assigned key on the index of the object within the array, and accepts the channelDetails state and an onClick handler.
return (
<>
{enquiries &&
enquiries.length > 0 &&
!enquiries.loading &&
channelDetails.length > 0 ? (
<ChannelList listHeight={"380px"}>
{enquiries.map((enquiry, i) => {
return (
<ChannelListItem
key={i}
details={channelDetails[i]}
activeProperty={activeProperty}
setActiveProperty={id => setActiveProperty(id)}
/>
);
})}
</ChannelList>
) : (
"loading..."
)}
</>
);
In the ChannelListItem component I render two images from the details prop based on the channelDetails state
const ChannelListItem = ({ details, setActiveProperty, activeProperty }) => {
const handleClick = () => {
setActiveProperty(details.propId);
};
return (
<div onClick={() => handleClick()} className={`ChannelListItem`}>
<div className={"ChannelListItemAvatarHeads"}>
<div
className={
"ChannelListItemAvatarHeads-prop ChannelListItemAvatarHead"
}
style={{
backgroundSize: "cover",
backgroundImage: `url(${details.propertyImage})`
}}
/>
<div
className={
"ChannelListItemAvatarHeads-agent ChannelListItemAvatarHead"
}
style={{
backgroundSize: "cover",
backgroundImage: `url(${details.receiverLogo})`
}}
/>
</div>
{activeProperty === details.propId ? <div>active</div> : null}
</div>
);
};
Now, the issue comes whenever the chrome dev tools window is open and you click on the different ChannelListItems the images blink/rerender. I had thought that the diff algorithm would have kicked in here and not rerendered the images as they are the same images?
But it seems that styled-components adds a new class every time you click on a ChannelListItem, so it rerenders the image. But ONLY when the develop tools window is open?
Why is this? Is there a way around this?
I can use inline styles instead of styled-components and it works as expected, though I wanted to see if there was a way around this without removing styled-components
I have a CODESANDBOX to check for yourselves
If you re-activate cache in devtool on network tab the issue disappear.
So the question becomes why the browser refetch the image when cache is disabled ;)
It is simply because the dom change so browser re-render it as you mentioned it the class change.
So the class change because the componetn change.
You create a new component at every render.
A simple fix:
import React from "react";
import styled from "styled-components";
const ChannelListDiv = styled.div`
height: ${props => props.listHeight};
overflow-y: scroll;
overflow-x: hidden;
`;
const ChannelList = ({ children, listHeight }) => {
return <ChannelListDiv listHeight={listHeight} className={"ChannelList"}>{children}</ChannelListDiv>;
};
export default ChannelList;
I think it has to do with this setting to disable cache (see red marking in image)
Hope this helps.

Modal component renders Twice on open

I'm using react-spring to animate a Modal based on #reach/dialog. The Modal can have any children. In the children I'm fetching some data based on some prop.
The problem is that the fetch call is made two times on opening the modal. I think it has probably to do with how I'm managing the state and that is causing re-renders.
I'v tried memoizing the children inside the modal and it didn't work, so I think that the problem is outside of the Modal component.
Here is something close to my code and how it is working https://codesandbox.io/s/loving-liskov-1xouh
EDIT: I already know that if I remove the react-spring animation the double rendering doesn't happen, but I want to try keeping the animation intact.
Do you think you can help me to identify where is the bug? (Also some tips on good practice with hooks are highly appreciated).
it renders three times because your return component has transitions.map since you have three item inside the
from: { opacity: 0 }
enter: { opacity: 1 }
leave: { opacity: 0 }
the {children} was called two times when the isOpen is true
you can fix the issue with just removing the from: { opacity: 0 } and leave: { opacity: 0 }
so change your modal.js => transitions
const transitions = useTransition(isOpen, null, {
enter: { opacity: 1 }
});
I checked and it is rendered twice because of animation in a Modal component when an animation is finished, modal is rendered second time when I commented out fragment responsible for animation, Modal renders only once.
const Modal = ({ children, toggle, isOpen }) => {
// const transitions = useTransition(isOpen, null, {
// from: { opacity: 0 },
// enter: { opacity: 1 },
// leave: { opacity: 0 }
// });
console.log("render");
const AnimatedDialogOverlay = animated(DialogOverlay);
// return transitions.map(
// ({ item, key, props }) =>
// item && (
return (
<AnimatedDialogOverlay isOpen={isOpen}>
<DialogContent>
<div
style={{
display: `flex`,
width: `100%`,
alignItems: `center`,
justifyContent: `space-between`
}}
>
<h2 style={{ margin: `4px 0` }}>Modal Title</h2>
<button onClick={toggle}>Close</button>
</div>
{children}
</DialogContent>
</AnimatedDialogOverlay>
);
// )
// );
};
The problem is, that at the end of the animation AnotherComponent remounts. I read similar problems about react-spring. One way could be, that you lift out the state from AnotherComponent to the index.js. This way the state will not lost at remount and you can prevent refetching the data.
const AnotherComponent = ({ url, todo, setTodo }) => {
useEffect(() => {
if (todo.length === 0) {
axios.get(url).then(res => setTodo(res.data));
}
});
....
}
Here is my version: https://codesandbox.io/s/quiet-pond-idyee

Resources