How to animate child prop changes in react? - javascript

I'm trying to create a component that takes a child prop and when this child prop changes I'd like the old component that was the child prop to animate out of screen and the new component to animate in. I attempted to do this using react-spring using the code below, you can run it here https://codesandbox.io/s/react-spring-issue-898-forked-ril7mc?file=/src/App.js.
What am I missing here? Shouldn't the component animate left when it unmounts and animate in from the right when it mounts?
/** #jsx jsx */
import { useState } from "react";
import { useTransition, animated } from "react-spring";
import { jsx } from "#emotion/core";
function FadeTransition({ children, onClick }) {
const [toggle, setToggle] = useState(true);
const transition = useTransition(toggle, {
from: {
opacity: 0,
transform: "translateX(100%)"
},
enter: {
opacity: 1,
transform: "translateX(0)"
},
leave: { transform: "translateX(-100%)" }
});
return (
<div>
{transition(
(props, item) =>
item && <animated.div style={props}>{children}</animated.div>
)}
<button
onClick={() => {
setToggle(!toggle);
onClick();
}}
style={{ marginTop: "2rem" }}
>
Change
</button>
</div>
);
}
export default function App() {
const [currentComponent, setCurrentComponent] = useState(0);
let components = [<div>Thing 1</div>, <div>Thing 2</div>];
const onClick = () => {
setCurrentComponent((currentComponent + 1) % 2);
};
return (
<div style={{ paddingTop: "2rem" }}>
<FadeTransition onClick={onClick}>
{components[currentComponent]}
</FadeTransition>
</div>
);
}

I didn't use rect-sprint as much,There is issue with toggle state so when thing 1 is rendered animation component (FadeTransition) is rendered with setting up toggle state to true.
Now when you click on change button we are setting up the toggle button to opposite of current state which will be false this time and without unmounting animation component (FadeTransition) it will update dom and so the item which is thing 2.
As we are passing toggle in useTransition and if it's values will be false then animation will not be performed.
So I have updated snippet of yours, Here is the working example codesandbox

Related

Animate Components On Exit With Framer Motion React

I have a button that toggles between two components.
How do I add an animation for each component on exit? Here is my code that doesn't work:
export default function App() {
const [dark, setDark] = useState(false);
const toggle = () => {
setDark(!dark);
};
return (
<div>
<AnimatePresence>
{dark ? (
<motion.h2 exit={{ opacity: 0 }}>Dark</motion.h2>
) : (
<motion.h2 exit={{ opacity: 0 }}>Light</motion.h2>
)}
</AnimatePresence>
<button onClick={toggle}>Toggle</button>
</div>
);
}
Thanks for your help!
This is fixed now. In order for animation to work, you should add initial and animate props. Also each component needs to have a unique key.

Passing React State Between Imported Components

I am trying to pass state from parent to child using React, however both components are imported and therefor the state variables of the parent component are not declared.
I have two components both exported from the same file. The first component is a wrapper for the second. This component has a useEffect function which find its height and width and set these values to hook state.
export const TooltipWrapper = ({ children, ariaLabel, ...props }) => {
const [width, setWidth] = React.useState(0);
const [height, setHeight] = React.useState(0);
const ref = React.useRef(null);
React.useEffect(() => {
if (ref.current && ref.current.getBoundingClientRect().width) {
setWidth(ref.current.getBoundingClientRect().width);
}
if (ref.current && ref.current.getBoundingClientRect().height) {
setHeight(ref.current.getBoundingClientRect().height);
}
});
return <TooltipDiv>{children}</TooltipDiv>;
The next component which is exported from the same file looks like this
export const Tooltip = ({
ariaLabel,
icon,
iconDescription,
text,
modifiers,
wrapperWidth,
}) => {
return (
<TooltipContainer
aria-label={ariaLabel}
width={wrapperWidth}
>
<TooltipArrow data-testid="tooltip-arrow" modifiers={modifiers} />
<TooltipLabel
aria-label={ariaLabel}
>
{text}
</TooltipLabel>
</TooltipContainer>
);
};
The component Tooltip is expecting a prop wrapperWidth. This is where I want to pass in the width hook value from the TooltipWrapper component.
Both components are imported into my App component
import React from "react";
import { GlobalStyle } from "./pattern-library/utils";
import { Tooltip, TooltipWrapper } from "./pattern-library/components/";
function App() {
return (
<div className="App">
<div style={{ padding: "2rem", position: "relative" }}>
<TooltipWrapper>
<button style={{ position: "relative" }}>click </button>
<Tooltip
modifiers={["right"]}
text="changing width"
wrapperWidth={width}
/>
</TooltipWrapper>
</div>
</div>
);
}
Here I am told that width is not defined, which I expect since I'm not declaring width in this file.
Does anyone have an idea of how I can access the width and height state value for the parent component within the App file?
Render Props could work:
Add a renderTooltip prop to <TooltipWrapper>:
<TooltipWrapper renderTooltip={({ width }) => <Tooltip ...existing wrapperWidth={width} />}>
<button style={{ position: 'relative' }}>click</button>
</TooltipWrapper>
NB. ...existing is just the other props you are using with Tooltip
And then update the return of <TooltipWrapper>:
return (
<TooltipDiv>
{children}
props.renderTooltip({ width });
</TooltipDiv>
);

Nuka-carousel react move to certain slide

I've got a question about npm package 'nuka-carousel. How to perform goToSlide on clicked element. I have list of elements with scroll3d setting. If I click on e.g last visible element I would like to scroll carousel so that element would be in a center.
According to their GitHub documentation, you can take control of the carousel just by adding onClick to your control button and then use setState() to change the slideIndex:
import React from 'react';
import Carousel from 'nuka-carousel';
export default class extends React.Component {
state = {
slideIndex: 0
};
render() {
return (
<Carousel
slideIndex={this.state.slideIndex}
afterSlide={slideIndex => this.setState({ slideIndex })}
>
...
</Carousel>
<button onClick={(event) => this.handlesClick(event, index)}> />
);
}
handleClick = (event, index) => {
event.preventDefault();
this.setState({slideIndex: index});
}
}

React stop/start fade-out on mouseEnter and mouseLeave without Jquery

I am trying to show an error message as a toast(React Component) that will fade-out after certain seconds. However When the user hover-in the mouse on the toast while fading, the fade-out should stop and the toast should be restored to its initial state and when he hovers-out the mouse on the toast, the fade-out should start again. It can be achieved by using JQuery like this -
//function to start the fade-out after time - t sec
static fadeOutToast(id, t) {
let toast = document.getElementById(id);
if (toast) {
setTimeout(() => {
FadeAndRemove('#' + id);
}, t * 1000);
}
}
/**
* t1 - time for fadeout animation
*/
static FadeAndRemove(id,t1) {
jQuery(id).fadeOut(t1 * 1000, function () {
jQuery(this).hide();
});
handleReAppear(id);
}
static handleReAppear(id) {
jQuery(id).on("mouseover", function (e) {
jQuery(this).stop(true).fadeIn(0);
});
jQuery(id).on("mouseleave", function (e) {
FadeAndRemove(this);
});
}
Its working perfectly fine. However due to projects constraints I am not supposed to mixup Jquery and react.
I tried to achieve it by manipulating the CSS opacity on mouseEnter and mouseLeave events. The problem I face is the toast never goes away from the page using opacity. Is there any way in which we can detect when the opacity of the toast becomes 0 so that I can remove it from the page just when the opacity becomes 0 ?
Can someone help me in achieving the same without using Jquery ?
For the fading animation I would use React-Spring. With a Spring you can delay the start animation so it will fade-out after the delay.
Then you can add onMouseEnter and onMouseLeave event handler to detect the hovering of the toastr.
With this mouse detection you can toggle the to value of the Spring to opacity 1. That way it won't fade-out if the mouse is over the toast.
For the removal of the toastr you can use onRest of Spring and check if opacity is zero. onRest will be called as soon as the animation will end.
The state management is done inside Toastrcomponent which will render all displayed toasts. This component will also handle the removal of the toast with no opacity.
For click event addToast I'm using a higher order component withToastr so I can add the prop of to the containing component.
For event handling I'm using Eventemitter3. If you're using Redux you could also use it to trigger the toasts.
In the next sections I'll give some details to every component that I've created in the following Codesandbox. (Note: The snippets here are not running - for testing the code please have a look at the sandbox)
ToastrItem component
Responsible for rendering a toast and for the animation.
import React, { PureComponent } from "react";
import { Spring } from "react-spring";
import styled from "styled-components";
import PropTypes from "prop-types";
class ToastrItem extends PureComponent {
static propTypes = {
id: PropTypes.string,
timeout: PropTypes.number,
destroy: PropTypes.func
};
static defaultProps = {
timeout: 5000
};
state = {
hovered: false
};
handleRest = ({ opacity }) => {
if (opacity === 0) {
this.props.destroy(this.props.id);
}
};
handleMouseEnter = () => {
this.setState({
hovered: true
});
};
handleMouseLeave = () => {
this.setState({
hovered: false
});
};
render() {
const { message, index, timeout } = this.props;
const { hovered } = this.state;
return (
<Spring
config={{ duration: 600, delay: timeout }}
from={{ opacity: 1.0 }}
to={{ opacity: hovered ? 1.0 : 0 }}
onRest={this.handleRest}
>
{interpolated => (
<Wrapper>
<ToastBox
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
pos={index}
opacity={interpolated.opacity}
>
{message}
{/*- debug info: {JSON.stringify(interpolated)}*/}
</ToastBox>
</Wrapper>
)}
</Spring>
);
}
}
const Wrapper = styled.div`
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
pointer-events: none;
z-index: 100;
`;
const ToastBox = styled.div.attrs(props => ({
style: {
transform: `translateY(${props.pos * 80}px)`,
opacity: props.opacity
}
}))`
width: 60%;
height: 50px;
line-height: 50px;
margin: 0 auto;
color: white;
padding: 10px;
background: rgba(0, 0, 0, 0.8);
text-align: center;
box-shadow: 3px 3px 5px rgba(0, 0, 0, 0.5);
border-radius: 10px;
pointer-events: auto;
`;
export default ToastrItem;
The Spring is doing the animation as mentioned before. The mouse events enter/leave are setting local state hovered so we can change the animation end opacity - this will avoid the animation.
I've also tried reset prop from React-Spring but that wasn't working as expected.
Toastr component
This component is managing the active toasts. Nothing special here. It's rendering the toasts array that are added with addToast.
addToast is creating a relatively unique key with timestamp and array index. It's needed so React is getting a key prop on the component. We could also use a uuid library here but I think the timestamp-id is OK.
destroy will be called if opacity is 0 then it's filter by key and update the state. The map is just there so we're updating the positions of the toasts.
class Toastr extends PureComponent {
state = {
toasts: []
};
addToast = (message, config) => {
const index = this.state.toasts.length;
const id = `toastr-${Date.now()}-${index}`;
const ToastComponent = (
<ToastrItem
key={id}
id={id}
index={index}
message={message}
timeout={config.timeout || 3000}
destroy={this.destroy}
/>
);
this.setState(state => ({
toasts: [...state.toasts, ToastComponent]
}));
};
destroy = id => {
this.setState(state => ({
toasts: [
...state.toasts
.filter(toast => toast.key !== id)
.map((toast, index) => ({
// map for updating index
...toast,
props: {
...toast.props,
index: index
}
}))
]
}));
};
componentDidMount() {
emitter.on("add/toastr", this.addToast);
}
render() {
const { toasts } = this.state;
return toasts;
}
}
export const withToastr = WrappedComponent => {
return class extends PureComponent {
render() {
return <WrappedComponent addToast={actions.add} />;
}
};
};
Usage in the app
We're adding addToast by using withToastr(App). This will add the prop addToastr to the App component.
Then we're rendering the Toastr component that will manage & render our toasts.
Finally we add a button so we can trigger the toasts.
class App extends Component {
toastr;
render() {
const { addToast } = this.props;
return (
<div className="App">
<Toastr />
<button onClick={() => addToast("Hello", { timeout: 4000 })}>
Show toast
</button>
</div>
);
}
}
const rootElement = document.getElementById("root");
const AppWithToasts = withToastr(App);
ReactDOM.render(<AppWithToasts />, rootElement);
Conclusion
The code is working but I would add native prop to the Spring and I would also check if a transition would be a better fit for the use-case. See the example from MessageHub example from React-spring docs. Should be also possible to prevent the fade-out but I haven't checked.
You might want to think about using the Animatable library. It uses a declarative syntax that's quite easy to incorporate.
import * from 'react-native-animatable';
return(
<Animatable.View animation="fadeOut" duration={2000} delay={1000}>
<View>
{/* YOUR CONTENT */}
</View>
</Animatable.View>
);
https://github.com/oblador/react-native-animatable

In react, how to get noticed when children change?

I am making this class called Scrollable which enables scrolling if the width/height of the children elements exceeds a certain value. Here is the code.
import React, { Component } from 'react';
const INITIAL = 'initial';
class Scrollable extends Component {
render() {
let outter_styles = {
overflowX: (this.props.x? 'auto': INITIAL),
overflowY: (this.props.y? 'auto': INITIAL),
maxWidth: this.props.width || INITIAL,
maxHeight: this.props.height || INITIAL,
};
return (
<div ref={el => this.outterEl = el} style={outter_styles}>
<div ref={el => this.innerEl = el}>
{this.props.children}
</div>
</div>
);
}
};
export default Scrollable;
// To use: <Scrollable y><OtherComponent /></Scrollable>
This works great. Except now I wish to add one more functionality which makes the scrollable always scroll to the bottom. I have some idea of how to do it:
this.outterEl.scrollTop = this.innerEl.offsetHeight;
But this only need to be called when this.props.children height changes. Is there any idea on how to achieve this?
Thanks in advance.
I would recommend a package element-resize-detector. It is not React-specific but you can easily build a high-order component around it or integrate your Scrollable component with it.
Now I have an idea of solving this.
Since I am using react-redux. The problem is that I could not use lifecycle hooks on this Scrollable component since this.props.children might not necessarily be changed when the content is updated.
One way to achieve this is to make Scroll component aware of the corresponding values in the redux state. So that when that relevant value is updated, we can scroll down to the bottom.
Scrollable component:
import React, { Component } from 'react';
const INITIAL = 'initial';
class Scrollable extends Component {
componentWillUpdate(){
if(this.props.autoScroll){
// only auto scroll when the scroll is already at bottom.
this.autoScroll = this.outterEl.scrollHeight - this.outterEl.scrollTop - Number.parseInt(this.props.height) < 1;
}
}
componentDidUpdate(){
if(this.autoScroll) this.outterEl.scrollTop = this.outterEl.scrollHeight;
}
render() {
let styles = {
overflowX: (this.props.x? 'auto': INITIAL),
overflowY: (this.props.y? 'auto': INITIAL),
maxWidth: this.props.width || INITIAL,
maxHeight: this.props.height || INITIAL,
};
return (
<div ref={el => this.outterEl = el} style={styles}>
<div ref={el => this.innerEl = el}>
{this.props.children}
</div>
</div>
);
}
};
export default Scrollable;
Scrollable container:
import { connect } from 'react-redux';
import Scrollable from '../components/Scrollable';
const mapStateToProps = (state, ownProps) => Object.assign({
state: state[ownProps.autoScroll] || false
}, ownProps);
export default connect(mapStateToProps)(Scrollable)
With this, Scrollable's life cycle hooks will be called when the corresponding state changes.

Categories

Resources