Hi, based on #117 described, the modal works now.
But when I'm trying to render something in children and update it.
The transition will be triggered something like leave event, just like the following gif:

Issue example: https://codesandbox.io/s/jp8n2k0qz3
I have no idea about it 馃槙 .
Do you have any idea to fix it?
@jigsawye if you don't give Transition keys it takes the function-child itself as a key, but since you re-create it on every render, that key changes, so for transition it looks like as if the modal actual vanished and now there's a new one instead. In other words it behaves as expected.
Two easy fixes:
A. use a reliable key, in your case the key describes if the modal is there or not:
<Transition
keys={this.state.show}
B. move the modal into a fixed reference (i would prefer this). Remember, primitives (Spring, Transition, etc) will move props they don't recognize to the child component, so you can forward locals (setState & checked):
const Modal = ({ set, checked, ...style }) => (
<animated.div style={{ ...style, ...content }}>
<div>Modal Content {checked && 'Checked'}</div>
<input type="checkbox" onChange={e => set({ checked: e.target.checked })} /> Click Me
<button onClick={() => set({ show: false })}>close</button>
</animated.div>
)
class App extends React.PureComponent {
state = { show: false, checked: false }
render() {
return (
<div>
<button onClick={() => this.setState({ show: true })}>open</button>
<Transition
native
from={{ opacity: 0, transform: 'translateY(-100px)' }}
enter={{ opacity: 1, transform: 'translateY(0)' }}
leave={{ opacity: 0, transform: 'translateY(-100px)', pointerEvents: 'none' }}
set={args => this.setState(args)}
checked={this.state.checked}>
{this.state.show && Modal}
</Transition>
</div>
)
}
}
PS. Try to use the "native" flag whenever you can, especially for modals, you don't want React render that thing 60 times per second. With native set the modal will only render once and animate directly in the dom. 馃槈
@drcmda Thanks for your help, it works! 馃帀
And I already tried native in my modal, everything looks great 馃憤
@drcmda Hey, thank you for this great library! I'm having the same issue as described here and in https://github.com/drcmda/react-spring/issues/61, neither solution worked for me yet. I'm trying to transition the children prop, like so:
<Transition
native
from={{ opacity: 0 }}
enter={{ opacity: 1 }}
leave={{ opacity: 0 }}
keys={Number(this.props.open)}
>
{this.props.open && (styles => <animated.div style={styles}>{this.props.children}</animated.div>)}
</Transition>
When the open prop changes before the transition had a chance to finish, the children prop gets duplicated. The keys prop doesn't help, and I'm not sure how to extract children into a fixed reference as you explained above. How would you suggest making this work?
Transition is always a stack because it guarantees exit transitions. Otherwise exit would abruptly stop and jump to enter state. If you want fade-in/out on a single element or between two you need them to be positioned absolutely, so that they can sit on top of one another, otherwise they鈥檒l push each other away, which is more useful in a list.
Ok, I've got a single element with absolute position. Is there a way to prevent duplication in the case of a single child that's dynamic content? I tried setting a static value for keys without success, whenever the transition toggles visibility before finishing, the single element gets duplicated
Sure, if you have a single Element i'd use a Spring. It's also faster since it technically leaves the node in the render tree, so you save yourself from an extra mount/unmount. With Transition you can't interrupt a child that's transitioning out. Very old react-spring versions functioned that way, but it always led to visual jank and interruptions. You could copy the Transition primitive and break animation under certain conditions - but really, i'd just use a spring instead.
Thanks for your help, @drcmda, and sorry for the delay! I was deep into it, trying to make it work.
Spring on its own didn't work out of the box since I also needed the mount/unmount behavior. I ended up writing a thin transition wrapper around the Spring primitive that allows for the animation to be interrupted and reverted in real-time, while also taking care of mounting and unmounting鈥擜PI looks just like http://reactcommunity.org/react-transition-group/transition/. Works really nicely, the Spring primitive is so powerful that I didn't have to write that much code. So, thank you again for this wonderful library 馃挅
Nice! I鈥檇 love to see this new primitive ...
You bet. Still a couple of rough edges, especially around handleRest where state is sometimes set on an unmounted component, but otherwise does the job. If you have feedback on how to make this better, I'll definitely take it.
import { Spring, SpringProps } from "react-spring"
import { safeInvoke } from "../utils"
type TransitionAnimatedStage = "entering" | "exiting"
type TransitionRestingStage = "entered" | "exited"
type TransitionStage = TransitionAnimatedStage | TransitionRestingStage
export type TransitionRenderer = (styles: React.CSSProperties) => React.ReactNode
export interface TransitionProps {
children: TransitionRenderer
toggle?: boolean
lazy?: boolean
from: React.CSSProperties
to: React.CSSProperties
config?: SpringProps<{}>["config"]
onEnter?: () => void
onEntering?: () => void
onEntered?: () => void
onExit?: () => void
onExiting?: () => void
onExited?: () => void
}
interface TransitionState {
mounted: boolean
stage: TransitionStage
}
export class Transition extends React.Component<TransitionProps, TransitionState> {
public static defaultProps = {
toggle: false,
lazy: true,
config: { tension: 150, friction: 10 },
}
public state: TransitionState = {
mounted: false,
stage: "exited",
}
public componentDidMount() {
const { toggle } = this.props
if (toggle) {
this.setState({ mounted: true, stage: "entering" })
}
}
public componentDidUpdate(prevProps: TransitionProps) {
if (!prevProps.toggle && this.props.toggle) {
this.setState({ stage: "entering" })
} else if (prevProps.toggle && !this.props.toggle) {
this.setState({ stage: "exiting" })
}
}
render() {
const { children, toggle, from, to, config, lazy } = this.props
const { mounted, stage } = this.state
const stageSpring: Record<TransitionAnimatedStage, SpringProps<{}>> = {
entering: {
from,
to,
},
exiting: {
from: to,
to: from,
},
}
return (
// When we're lazy and the transition is inactive (i.e. unmounted and toggled off), we don't
// render anything.
!(lazy && !mounted && !toggle) && (
<Spring native config={config} {...stageSpring[stage]} onStart={this.handleStart} onRest={this.handleRest}>
{children}
</Spring>
)
)
}
private handleStart = () => {
const { toggle, onEnter, onEntering, onExit, onExiting } = this.props
const event = toggle ? onEnter : onExit
const stage = toggle ? "entering" : "exiting"
const callback = toggle ? onEntering : onExiting
safeInvoke(event)
this.setState({ mounted: true, stage }, callback)
}
private handleRest = () => {
const { toggle, onEntered, onExited } = this.props
const stage = toggle ? "entered" : "exited"
const callback = toggle ? onEntered : onExited
this.setState({ stage }, callback)
if (stage === "exited") {
// Unmount after a small timeout to prevent setting state on an unmounted component.
setTimeout(() => this.setState({ mounted: false }), 10)
}
}
}
Most helpful comment
You bet. Still a couple of rough edges, especially around
handleRestwhere state is sometimes set on an unmounted component, but otherwise does the job. If you have feedback on how to make this better, I'll definitely take it.