Deng  Zhebin
Deng Zhebin

Reputation: 1262

componentWillReceiveProps, componentDidUpdate for React Hook

I run into two challenges:

see below code sample:

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: props.count > 100 ? 100 : props.count,
    }

  }

  /*What is the equivalent implementation when React Hook is used here componentWillReceiveProps*/
  componentWillReceiveProps(nextProps) {
    if (nextProps.count !== this.props.count) {
      this.setState({
        count: nextProps.count > 100 ? 100 : nextProps.count
      });
    }
  }

  render() {
    return ( <
      div > {
        this.state.count
      } <
      /div>
    );
  }
}

export default App;

see below code sample:

class App extends Component {


  /*What is the equivalent implementation when functional component with React Hook is used here */
  componentDidUpdate(prevProps, prevState) {
    if (prevProps.groupName !== this.props.groupName) {
      console.log('Let'
        's say, I do want to do some task here only when groupName differs');
    } else if (prevProps.companyName !== this.props.companyName) {
      console.log('Let'
        's say, I do want to do some different task here only when companyName differs');
    }

  }


  render() {
    /*for simplicity, render code is ignored*/
    return null;
  }
}

export default App;

Upvotes: 66

Views: 100878

Answers (10)

Jo Momma
Jo Momma

Reputation: 1307

I'm using React Native. I added this to my screen component where I needed a componentWillReceiveProps functionality:

function MyComponent({ props }) {
   const propsRef = useRef(props)

   const componentWillReceiveProps = () => {
      const propsHaveChanged = !isEqual(propsRef.current, props)
      propsRef.current = props

      if (propsHaveChanged) {
         // Do stuff after props has changed
      }
   }

   componentWillReceiveProps();

   return (
      <View>
         <Text>My Component</Text>
      </View>
   )

 }

Here isEqual is a lodash function that does a deep comparison. Then just call the function.

componentWillReceiveProps()

Notice that I called componentWillReceiveProps at the top level inside the functional component. So, as a result this function will be called before the component renders. The ref that I use will -preserve it's data across renders. So, when componentWillReceiveProps runs on every render it will look at the props that are being passed in and compare that to the props from the previous render which are stored in the ref. Then you check if they are different so you can perform whatever actions you need based on the props changing. Next you set the ref to the current props to check against on the next render. Hope that helps. 🍻

Upvotes: 0

amirhe
amirhe

Reputation: 2341

In a case, you want to replicate a log system such as why did you render, and you need the nextProps here is helping hook.

  • aligned with old lifeCycle function rename stuffs to props and nextProps
  • nextProps means current props
  • props mean previous props
const useComponentWillReceiveProps(nextProps, callback) {
  const props = useRef(nextProps) 

  useEffect(() => {
    callback(nextProps, props.current)
    props.current = nextProps
  })
}

Usage

const diffLogger = (nextProps, props, { name = "Component" } = {}) => {
  Object.keys(nextProps)
    .filter((key) => nextProps[key] !== props[key])
    .forEach((key) => {
      console.log(
        `%c${name}:%c${key}`,
        "color:#ff5722; font-size:1rem; font-weight:bold;",
        "color:#ffc107; font-size:1.2rem; font-weight:bold;",
        {
          from: props[key],
          to: nextProps[key],
        }
      )
    })
}
const Button = (props) => {
 useComponentWillReceiveProps(props, diffLogger, {name: 'Button'})
 return <button onClick={props.onClick}>Button</button>
}

Now If your button re-render e.g. by onClick new reference you will get something like this: enter image description here

Upvotes: 1

Winchester
Winchester

Reputation: 490

setCount will trigger a re-render. Using useEffect with [count] as the dependencies array will ensure that the hook will only calls setCount when count's value changes.

This is how you go about replacing whatever componentWillReceiveProps logic you might have otherwise written in the old class-based React style. I find the "Each Render Has Its Own Props and State" principle useful: if you want to trigger a re-render only when specific props change, you can have several useEffect hooks.

useEffect(() => {
  count > 100 ? setCount(100) : setCount(count)
}, [count]) 
 
useEffect(() => {
  console.log('groupName has changed');
  // do something with groupName
}, [groupName])
    
useEffect(() => {
  console.log('companyName has changed');
  // do something with companyName
}, [companyName]) 

Upvotes: 7

Hasan Zahran
Hasan Zahran

Reputation: 1442

simply by using useEffect like this.

useEffect( () => {
    props.actions.fetchSinglePost(props.match.params.id); //> I'm dispatching an action here.
}, [props.comments]) //> and here to watch comments and call the action in case there is any change.

Upvotes: 2

Jan-Philipp Marks
Jan-Philipp Marks

Reputation: 1539

If you use the useMemo hook on top of your component and have it dependent on all your props, it runs before everything everytime props change. useEffect is triggered after the updated render and since dependent on all props it triggers after a rerender depending on all props.

const Component = (...props) => {
   // useState, useReducer if have
   useMemo(() => {
     // componentWillReceiveProps
   },[...props]);
   // ...other logic and stuff
   useEffect(() => {
     // componentDidUpdate
   }, [...props]);
};

Upvotes: 11

fgonzalez
fgonzalez

Reputation: 3877

The react hook equivalent to the old componentWillReceive props can be done using the useEffect hook, just specifying the prop that we want to listen for changes in the dependency array.

I.e:

export default (props) => {

    useEffect( () => {
        console.log('counter updated');
    }, [props.counter])

    return <div>Hi {props.counter}</div>
}

For componentDidUpdate just by omitting the dependency array, the useEffect function will be called after every re-render.

I.e:

export default (props) => {

    useEffect( () => {
        console.log('counter updated');
    })

    return <div>Hi {props.counter}</div>
}

Upvotes: 47

ford04
ford04

Reputation: 74500

1.) What is the equivalent implementation with React Hook, If I do need derived state?

Derived state for Hooks = set state conditionally and directly in the render phase:

constComp = (props) => {
  const [derivedState, setDerivedState] = useState(42);
  if (someCondition) {
    setDerivedState(...);
  }
  // ...
}

This updates state without an additional commit phase as opposed to useEffect. Above pattern is supported by React Strict Mode (no warnings):

const App = () => {
  const [row, setRow] = React.useState(1);

  React.useEffect(() => {
    setTimeout(() => {
      setRow(2);
    }, 3000);
  }, []);

  return (
    <React.StrictMode>
      <Comp row={row} />
    </React.StrictMode>
  );
}

const Comp = ({ row }) => {
  const [isScrollingDown, setIsScrollingDown] = React.useState(false);
  const [prevRow, setPrevRow] = React.useState(null);

  console.log("render, prevRow:", prevRow)

  if (row !== prevRow) {
    console.log("derive state");
    // Row changed since last render. Update isScrollingDown.
    setIsScrollingDown(prevRow !== null && row > prevRow);
    setPrevRow(row);
  }

  return `Scrolling down: ${isScrollingDown}`;
};

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.development.js" integrity="sha256-4gJGEx/zXAxofkLPGXiU2IJHqSOmYV33Ru0zw0TeJ30=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.development.min.js" integrity="sha256-9xBa2Hcuh2S3iew36CzJawq7T9iviOAcAVz3bw8h3Lo=" crossorigin="anonymous"></script>
<div id="root"></div>

Note 1: componentWillReceiveProps has been deprecated for quite some time. getDerivedStateFromProps is the class components' successor in terms of derived state.

Note 2: Check preferred solutions before you resort to derived state.


2.) What if I want to do respective tasks based on multiple respective props changes?

You can either leave useEffect deps out completely or preferably add another prop dep:

React.useEffect(() => {
  return () => { };
}, [parentProp, secondProp]);

Upvotes: 0

Kevin F.
Kevin F.

Reputation: 57

In your scenario, you don't need to use or re-implement getDerivedStateFromProps at all. You just need to create a new variable to get the new form of data. Using state in this scenario will just cause another re-rendering which is not good performance wise.

import React from 'react';

const App = ({ count }) => {
  const derivedCount = count > 100 ? 100 : count;

  return (
    <div>Counter: {derivedCount}</div>
  );
}

App.propTypes = {
  count: PropTypes.number.isRequired
}

Demo here: https://codesandbox.io/embed/qzn8y9y24j?fontsize=14

You can read more on different ways to solve these kind of scenarios without using getDerivedStateFromProps here: https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html

If you really need to use a separate state, you can use something like this

import React, { useState } from 'react';

const App = ({ count }) => {
  const [derivedCounter, setDerivedCounter] = useState(
    count > 100 ? 100 : count
  );

  useEffect(() => {
    setDerivedCounter(count > 100 ? 100 : count);
  }, [count]); // this line will tell react only trigger if count was changed

  return <div>Counter: {derivedCounter}</div>;
};

Upvotes: 2

Ryan Cogswell
Ryan Cogswell

Reputation: 81006

I realize your "derived state" example is intentionally simple, but because there are so few legitimate cases of derived state, it is difficult to make a recommendation on the replacement except on a case-by-case basis since it depends on the reason you are using derived state. In the particular example you provided, there was no reason to use derived state in the class case and so there continues to be no reason in the hook case (the value can just be derived locally without putting it in state). If the derived value is expensive, you can use useMemo as Tholle presents. If these don't fit the more realistic case(s) you have in mind, you would need to present a more specific case that truly requires derived state.

As far as your componentDidUpdate example, if what you want to do for the different props is independent, then you can use separate effects for each (i.e. multiple useEffect calls). If you want to do exactly what is in your example (i.e. only do something for a companyName change if groupName didn't also change as indicated by your else if), then you can use refs for more sophisticated conditions. You should not mutate the ref during rendering (there is always the possibility of the render being discarded/redone once concurrent mode is supported), so the example uses the last effect to make updates to the refs. In my example, I use a ref to avoid doing effect work on the initial render (see Tholle's answer in this related question) and to detect whether or not groupName changed when deciding whether or not to do work based on a companyName change.

const { useState, useEffect, useRef } = React;

const DerivedStateFromProps = ({ count }) => {
  const derivedCount = count > 100 ? 100 : count;

  return (
    <div>
      Derived from {count}: {derivedCount}{" "}
    </div>
  );
};
const ComponentDidUpdate = ({ groupName, companyName }) => {
  const initialRender = useRef(true);
  const lastGroupName = useRef(groupName);
  useEffect(
    () => {
      if (!initialRender.current) {
        console.log("Do something when groupName changes", groupName);
      }
    },
    [groupName]
  );
  useEffect(
    () => {
      if (!initialRender.current) {
        console.log("Do something when companyName changes", companyName);
      }
    },
    [companyName]
  );
  useEffect(
    () => {
      if (!initialRender.current && groupName === lastGroupName.current)
        console.log(
          "Do something when companyName changes only if groupName didn't also change",
          companyName
        );
    },
    [companyName]
  );
  useEffect(
    () => {
      // This effect is last so that these refs can be read accurately in all the other effects.
      initialRender.current = false;
      lastGroupName.current = groupName;
    },
    [groupName]
  );

  return null;
};
function App() {
  const [count, setCount] = useState(98);
  const [groupName, setGroupName] = useState("initial groupName");
  const [companyName, setCompanyName] = useState("initial companyName");
  return (
    <div>
      <div>
        <DerivedStateFromProps count={count} />
        <button onClick={() => setCount(prevCount => prevCount + 1)}>
          Increment Count
        </button>
      </div>
      <div>
        <ComponentDidUpdate groupName={groupName} companyName={companyName} />
        groupName:{" "}
        <input
          type="text"
          value={groupName}
          onChange={event => setGroupName(event.target.value)}
        />
        <br />
        companyName:{" "}
        <input
          type="text"
          value={companyName}
          onChange={event => setCompanyName(event.target.value)}
        />
        <br />
        change both{" "}
        <input
          type="text"
          onChange={event => {
            const suffix = event.target.value;
            setGroupName(prev => prev + suffix);
            setCompanyName(prev => prev + suffix);
          }}
        />
      </div>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
<div id="root"></div>
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

Edit Derived state and componentDidUpdate

Upvotes: 0

Tholle
Tholle

Reputation: 112787

You can use the useMemo hook to store a calculation and put props.count in the array given as second argument to recalculate the value when it changes.

const { useState, useEffect, useMemo } = React;

function App() {
  const [count, setCount] = useState(50);

  useEffect(() => {
    setTimeout(() => {
      setCount(150);
    }, 2000);
  }, []);

  return <DisplayCount count={count} />;
}

function DisplayCount(props) {
  const count = useMemo(() => props.count > 100 ? 100 : props.count, [props.count]);

  return <div> {count} </div>;
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>

<div id="root"></div>

The easiest way to do separate effects when separate props change is to create multiple useEffect hooks that are only run when one of the separate props change.

const { useState, useEffect } = React;

function App() {
  const [groupName, setGroupName] = useState('foo');
  const [companyName, setCompanyName] = useState('foo');

  useEffect(() => {
    setTimeout(() => {
      setGroupName('bar');
    }, 1000);
    setTimeout(() => {
      setCompanyName('bar');
    }, 2000);
  }, []);

  return <DisplayGroupCompany groupName={groupName} companyName={companyName} />;
}

function DisplayGroupCompany(props) {
  useEffect(() => {
    console.log("Let's say, I do want to do some task here only when groupName differs");
  }, [props.groupName])
  useEffect(() => {
    console.log("Let's say,I do want to do some different task here only when companyName differs");
  }, [props.companyName])

  return <div> {props.groupName} - {props.companyName} </div>;
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>

<div id="root"></div>

Upvotes: 9

Related Questions