useEffect how to compare previous and updated props

Viewed 25008

I'm converting class-based Heading component to functional component but that component is using 3 lifecycle hooks componentDidMount, componentWillUnmount and componentDidUpdate.

I replaced componentDidMount and componentWillUnmount but how to replace componentDidUpdate? It is comparing updated prop value with old prop value.

Codesandbox link.

App Component:

import React, { Component } from "react";
import { Heading } from "./components/Heading";
import { HeadingHook } from "./components/HeadingHook";

export class App extends Component {
  state = {
    flag: false,
    mountUnmount: true
  };

  toggleChangeFlag = () => {
    if (this.state.mountUnmount) {
      this.setState({
        flag: !this.state.flag
      });
    }
  };

  toggleMountUnmount = () => {
    this.setState({
      flag: false,
      mountUnmount: !this.state.mountUnmount
    });
  };

  render() {
    return (
      <>
        <div>
          <p>Controls :</p>
          <button onClick={this.toggleChangeFlag}>Change Flag</button>
          <button onClick={this.toggleMountUnmount}>Mount & Unmount</button>
        </div>
        {this.state.mountUnmount && <Heading flag={this.state.flag} />}
        {/* Comment above line to use useEffect Heading Component */}
        {/* {this.state.mountUnmount && <HeadingHook flag={this.state.flag} />} */}
      </>
    );
  }
}

Class-based Heading Component:

import React, {Component} from 'react';

export class Heading extends Component {

    handleProps() {
        if (this.props.flag) {
            alert(this.props.flag);
        } else {
             alert(this.props.flag);
        }
    };

    componentDidMount() {
        this.handleProps();
    }

    componentWillUnmount() {
        this.handleProps();
    }

    componentDidUpdate(prevProps) {
        if (this.props.flag !== prevProps.flag) {
            this.handleProps();
        }
    }

    render() {
        return <h1>Hello World!</h1>;
    }

}

useEffect Heading Component:

import React, { useEffect } from "react";

export const HeadingHook = (props) => {
  const handleProps = () => {
    if (props.flag) {
      alert(props.flag);
    } else {
      alert(props.flag);
    }
  };

  useEffect(() => {
    handleProps();
    return () => {
      handleProps();
    };
  });

  return <h1>Hello World!</h1>;
};
6 Answers

All you need to do is to pass a dependency array to useEffect to make sure when the effect runs. In the current implementation of yours, useEffect is not only called on initial render but on each render. To call it on an initial render only, you need to pass an empty array as the second argument. However if you also want the effect to run on a param change, you pass in that param in the dependency array.

Secondly, Functional components don't have this keyword and the props received by them are through the argument of the function.

const Heading = ({flag}) => {


    useEffect(() => {
        const handleProps = () => {
          if (flag) {
            alert(flag);
          } else {
            alert(flag);
          }
         };
        handleProps();
        return () => {
            handleProps();
        }

    }, [flag]);
    return <h1>Hello World!</h1>;
};

New hook to use instead of useEffect:

function useComponentDidMountOrUpdate(effect, deps) {
  const prev = React.useRef(deps)

  React.useEffect(
    () => {
      const unmountHandler = effect(prev.current)
      prev.current = deps
      return unmountHandler
    },
    deps
  )
}

Using the hook:

useComponentDidMountOrUpdate(
  (prevDeps) => {
    const prevDep1 = prevDeps[1]

    if (dep1 !== prevDep1) {
      // dep1 changed
    }

    return () => { /* unmount handler */ }
  },
  [dep0, dep1, dep2]
)

You can check for prop updates by using useEffect this way

import React, {useEffect} from 'react';

const Heading = (props) => {

  useEffect(() => {
    const handleProps = () => {
      if (props.flag) {
        alert(props.flag);
      } else {
        alert(props.flag);
      }
    };

    handleProps();
    return () => {
        handleProps();
    }
  }, [props.flag]);
  return <h1>Hello World!</h1>;
};

Adding the [props.flag] at the end of the useEffect allows for it to act as a dependency array where the useEffect hook will only fire when the prop.flag value in the component changes. Thus when you update or change this value from your parent component, the effect will be triggered much like the way componentDidUpdate does.

when new props being passed in , the cleanup will run before the effect being applied, the cleanup function will not depend on Flag so we can simply have two useEffect.

import React,{useEffect} from "react";

const handleProps = (props) => {
  if (props) {
    console.log(props);
  } else {
    console.log(props);
  }
}

export const Heading = ({ flag }) => {
    useEffect(() => {
      console.log("mount")
      handleProps(flag);
    }, [flag]);

    useEffect(() => {
      return () => {
        console.log("unmount")
        handleProps(flag)
      };
    }, []);

  return <h1>Hello World!</h1>;
};
const prevSearchText = useRef();

useEffect(() => {

        return (function(searchText) {

            return function() {
                prevSearchText.current = searchText;
            }
        })(props.searchText)

    }, [props.searchText])

Using a closure in useEffect cleanup, and providing right dependencies to useEffect, should help

You can pass a second argument to useEffect(). The second argument will be an array with the flag prop. Every time the flag prop changes the useEffect method will be triggered.

useEffect(() => {
  handleProps();
  
  return () => {
    handleProps();
  }
}, [props.flag]);
Related