I run into two challenges:
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]);
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>;
};
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>
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])
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>
}
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]);
};