Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

static getDerivedStateFromProps that requires previous props and state callback?

I have just updated to react native 0.54.0 that also includes alpha 1 of react 16.3, naturally I get a lot of warnings regarding depreciation of componentWillMount and componentWillReceiveProps.

I have an animated route component that was relying on componentWillReceiveProps at its core it receives new path, compares it to previous one, if they are sets old children, animates them out, sets new children and animates them in.

Here is code in question:

componentWillReceiveProps(nextProps: Props) {
    if (nextProps.pathname !== this.props.pathname) {
      this.setState({ previousChildren: this.props.children, pointerEvents: false }, () =>
        this.animate(0)
      );
    }
  }

Now for the questions I have regarding porting this to static getDerivedStateFromProps

1) I no longer have access to this.props, hence no access to previous pathname, I guess I can store these props in state, is this correct approach now? Seems like repetition of data.

2) As I have no access to this I can't call my animation function, that relies on a state. How can I bypass this?

3) I need to first set state and then call the animation, as getDerivedStateFromProps sets state by returning the values, I can't do much afterwards, so is there a way to set state and after thats done execute a callback?

4) pathname bit is only used in componentWillreceiveProps right now, if I move it to state and never use this.state inside getDerivedStateFromProps (because I can't) this.state.pathname errors as being defined but never used. Is the best approach here to make it static as well?

My first instinct was to change it to componentDidUpdate, but we are not supposed to use setState inside of it correct? (this does work in my scenario though).

  componentDidUpdate(prevProps: Props) {
    if (this.props.pathname !== prevProps.pathname) {
      this.setState({ previousChildren: prevProps.children, pointerEvents: false }, () =>
        this.animate(0)
      );
    }
  }

NOTE: As far as I heard I think there is a feature coming in "suspense" that will allow us to keep views mounted on component unmount event? I wasn't able to find any reference to this, but it sounds like something I could potentially utilise for this animation

For anyone interested, this is snippet for a full component in question

// @flow
import React, { Component, type Node } from "react";
import { Animated } from "react-native";

type Props = {
  pathname: string,
  children: Node
};

type State = {
  animation: Animated.Value,
  previousChildren: Node,
  pointerEvents: boolean
};

class OnboardingRouteAnomation extends Component<Props, State> {
  state = {
    animation: new Animated.Value(1),
    previousChildren: null,
    pointerEvents: true
  };

  componentWillReceiveProps(nextProps: Props) {
    if (nextProps.pathname !== this.props.pathname) {
      this.setState({ previousChildren: this.props.children, pointerEvents: false }, () =>
        this.animate(0)
      );
    }
  }

  animate = (value: 0 | 1) => {
    Animated.timing(this.state.animation, {
      toValue: value,
      duration: 150
    }).start(() => this.animationLogic(value));
  };

  animationLogic = (value: 0 | 1) => {
    if (value === 0) {
      this.setState({ previousChildren: null, pointerEvents: true }, () => this.animate(1));
    }
  };

  render() {
    const { animation, previousChildren, pointerEvents } = this.state;
    const { children } = this.props;
    return (
      <Animated.View
        pointerEvents={pointerEvents ? "auto" : "none"}
        style={{
          alignItems: "center",
          opacity: animation.interpolate({ inputRange: [0, 1], outputRange: [0, 1] }),
          transform: [
            {
              scale: animation.interpolate({ inputRange: [0, 1], outputRange: [0.94, 1] })
            }
          ]
        }}
      >
        {previousChildren || children}
      </Animated.View>
    );
  }
}

export default OnboardingRouteAnomation;
like image 365
Ilja Avatar asked Mar 07 '18 16:03

Ilja


1 Answers

With react version 16.3, using componentWillReceiveProps is fine, but will cause a deprecation warning to be shown in the console. It will be removed in version 17. FWIW Dan Abramov has warned against using alpha functionality in production - but this was back in March of 2018.

Let's walk through your questions:

1) I no longer have access to this.props, hence no access to previous pathname, I guess I can store these props in state. Is this the correct approach now? Seems like repetition of data.

Yes.

If you want to update / re-render your component, you should combine props and state. In your case, you want to update the component / trigger animation when pathname is changed with a life cycle method.

Seems like repetition of data.

check this out: React Team post on State and Lifecycle

2) As I have no access to this I can't call my animation function, which relies on state. How can I bypass this?

Use componentDidUpdate

componentDidUpdate

This function will be called after render is finished in each of the re-render cycles. This means that you can be sure that the component and all its sub-components have properly rendered itself.

This means you can call animate after setState in componentDidUpdate

3) I need to first set state and then call the animation, as getDerivedStateFromProps sets state by returning the values, I can't do much afterwards, so is there a way to set state and after thats done execute a callback?

Refer to point #2 (Use componentDidUpdate)

4) pathname bit is only used in componentWillreceiveProps right now, if I move it to state and never use this.state inside getDerivedStateFromProps (because I can't) this.state.pathname errors as being defined but never used. Is the best approach here to make it static as well?

No, It will used by componentDidUpdate

Here's how I would approach this:

// @flow
import React, { Component, type Node } from "react";

type Props = {
    pathname: string,
    children: Node
};

type State = {
    previousChildren: Node,
    pointerEvents: boolean
};

class OnboardingRouteAnomation extends Component<Props, State> {
    state = {
        previousChildren: null,
        pointerEvents: true,
        pathname: ''
    };

    static getDerivedStateFromProps(nextProps, prevState) {
        if (nextProps.pathname !== prevState.pathname) {
            return {
                previousChildren: nextProps.children,
                pointerEvents: false,
                pathname: nextProps.pathname
            };
        }

        return null;
    }

    componentDidUpdate(prevProps, prevState) {
        if (prevState.pathname !== this.state.pathname){
            console.log("prevState.pathname", prevState.pathname);
            console.log("this.props.pathname", this.props.pathname);
            this.animate(0);
        }
    }

    componentDidMount(){
        this.setState({ pathname: this.props.pathname});
    }

    animate = (value: 0 | 1) => {
        console.log("this animate called", this);
        animationLogic(value);
    };

    animationLogic = (value: 0 | 1) => {
        if (value === 0) {
            this.setState({ previousChildren: null, pointerEvents: true }, () => this.animate(1));
        }
    };

    render() {
        const { animation, previousChildren, pointerEvents } = this.state;
        const { children } = this.props;
        return (
            <div>
                {this.props.children}
            </div>
        );
    }
}

export default OnboardingRouteAnomation;

I believe this is how the react devs intended this should be handled. You should call animate after updating via componentDidUpdate because it's a side effect.

I'd use a more descriptive name for indicating the path has changed. Something like isPathUpdated. You could then have animate check isPathUpdated as a toggle.

like image 60
hendrathings Avatar answered Oct 12 '22 13:10

hendrathings