-
Notifications
You must be signed in to change notification settings - Fork 50.4k
Description
Do you want to request a feature or report a bug?
Request a feature
What is the current behavior?
getDerivedStateFromProps does not expose prevProps
What is the expected behavior?
getDerivedStateFromProps should expose prevProps for cleaner implementation of use case mentioned below.
Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?
react: 16.4+
I know there was a similar discussion in the issues here before regarding exposing previous props in getDerivedStateFromProps, but I believe I came across a use case where this can be useful, its very specific, yet it required me to replicate a lot of previous props in the state.
Below is a component I use in react-native to add an animation where screens crossfade and don't just unmount instantly, it also checks if next route is an overlay and preserves screen behind it. As you can see I had to create prevPathname prevData and prevChildren for this to work, which I think is not too terrible, yet results in a lot of repetition.
Perhaps my implementation is missing something to remove the repetition or maybe I am not understanding why we are not exposing prevProps?
// @flow
import React, { Component } from 'react'
import { Animated } from 'react-native'
import { durationNormal, easeInQuad, easeOutQuad } from '../services/Animation'
import type { Node } from 'react'
type Props = {
pathname: string,
data: ?{ overlay: boolean },
children: Node,
authenticated: boolean
}
type State = {
prevPathname: ?string,
prevChildren: Node,
prevData: ?{ overlay: boolean },
animation: Animated.Value,
activeChildren: Node,
pointerEvents: boolean,
authAnimation: boolean
}
class RouteFadeAnimation extends Component<Props, State> {
state = {
prevPathname: null,
prevChildren: null,
prevData: null,
animation: new Animated.Value(0),
activeChildren: null,
pointerEvents: true,
authAnimation: true
}
static getDerivedStateFromProps(nextProps: Props, prevState: State) {
const { pathname, data, children } = nextProps
const { prevPathname, prevData, prevChildren } = prevState
// This will be returned always to store "previous" props in state, so we can compare against them in
// future getDerivedStateFromProps, this is where I'd like to use prevProps
const prevPropsState = {
prevChildren: children,
prevPathname: pathname,
prevData: data
}
// Check if pathname changed, i.e we are going to another view
if (pathname !== prevPathname) {
// Check if current visible view is a modal, if it is, we go to default return
if (!prevData || !prevData.overlay) {
// Check if future view is not a modal
if (!data || !data.overlay) {
// Preserve current view while we are animationg out (even though pathname changed)
return {
activeChildren: prevChildren,
pointerEvents: false,
...prevPropsState
}
// If future view is a modal, preserve current view, so it is visible behind it
} else if (data.overlay) {
return {
activeChildren: prevChildren,
...prevPropsState
}
}
}
// If previous view was a modal (only normal view can follow after modal) reset our view persistance
// and use children as opposed to activeChildren
return {
activeChildren: null,
...prevPropsState
}
}
// Persist prevProps in state
return {
...prevPropsState
}
}
// This just handles animation based on cases above
componentDidUpdate(prevProps: Props) {
const { pathname, data, authenticated } = this.props
const { authAnimation } = this.state
if (authenticated && authAnimation) this.animate(1)
else if (pathname !== prevProps.pathname) {
if (!prevProps.data || !prevProps.data.overlay) {
if (!data || !data.overlay) this.animate(0)
}
}
}
animate = (value: 0 | 1) => {
let delay = value === 1 ? 60 : 0
const { authAnimation } = this.state
if (authAnimation) delay = 2000
Animated.timing(this.state.animation, {
toValue: value,
duration: durationNormal,
delay,
easing: value === 0 ? easeInQuad : easeOutQuad,
useNativeDriver: true
}).start(() => this.animationLogic(value))
}
animationLogic = (value: 0 | 1) => {
if (value === 0) this.setState({ activeChildren: null }, () => this.animate(1))
else this.setState({ pointerEvents: true, authAnimation: false })
}
render() {
const { animation, pointerEvents, activeChildren } = this.state
const { children } = this.props
return (
<Animated.View
pointerEvents={pointerEvents ? 'auto' : 'none'}
style={{
opacity: animation.interpolate({ inputRange: [0, 1], outputRange: [0, 1] }),
transform: [
{
scale: animation.interpolate({ inputRange: [0, 1], outputRange: [0.94, 1] })
}
]
}}
>
{activeChildren || children}
</Animated.View>
)
}
}
export default RouteFadeAnimationUsage example and explanation
This component is used to wrap several routes and on pathname change preserve previous view, animate it out, replace it with new view and animate it in. Idea itself comes from react-router's documentation https://reacttraining.com/react-router/native/guides/animation/page-transitions but they use componentWillMount there.
basic implementation can look like this:
<RouterFadeAnimation
pathname={routerProps.pathname}
data={routerProps.data}
authenticated={authProps.auth}>
{routerProps.pathname === "/home" && <HomePage />}
{routerProps.pathname === "/about" && <AboutPage />}
</RouterFadeAnimation>Outside of this, there is similar component called <RouteModalAnimation /> that overlays component above, it similarly animates views in when routerProps.data has overlay: true set, you will see our original component checks for this and preserves its view so it appears behind the modal, as it would otherwise dissapear due to route change.