So, you used to use recompose / class components in your React app to add lifecycle methods to your stateless functional components, before the Hooks API was launched.
The time has come to upgrade to React 16 and make use of the new way of managing state within your function components (note that they're now no longer stateless).
This guide will walk through implementing side effects when your component mounts (componentDidMount
), un-mounts (componentWillUnmount
) and re-renders (componentDidUpdate
). We will also touch on the React 16 approach to shouldComponentUpdate
.
Running side effects when your component mounts/updates/unmounts
If you need to run the same code in your componentDidMount
and componentDidUpdate
and componentWillUnmount
lifecycle methods then you can use useEffect
like below.
Tip: When working with effects, try to forget about the concept of mounting/unmounting and instead replace this with rendering.
The useEffect
hook tells React that your component needs to do something after a render. You can pass a single argument to useEffect
this is what we refer to as your effect
- essentially the function to run when the render happens. In the example below we pass callback function down called onUpdate
and use this to pass our counter value higher up on every render.
const Counter = ({ onUpdate }) => {
const [counter, setCounter] = useState(0);
useEffect(() => {
onUpdate(counter);
});
const incrementCounter = setCounter(counter + 1);
return (
<div>
<button onClick={incrementCounter}>Increment Counter</button>
{counter}
</div>
)
}
Conditional effects - componentDidUpdate
Sometimes you only want your side effects to run when certain props change, maybe to prevent infinite re-renders. In a standard lifecycle method in a recompose
or class based approach you would include a conditional if
statement within your componentDidUpdate
function.
compose(
lifecycle({
componentDidUpdate(prevProps) {
const { count, update } = this.props;
if (prevProps.count !== count) {
update(count)
}
}
})
)(Counter)
Using hooks, you can utilise the useEffect
hook we made above and make use of the second argument that you can pass. The second argument is essentially a whitelist of values that should be compared on each render and if the value is found to be different only then should your effect
run.
In the below example you can see that we pass an array with our counter
value in. We are telling React to only call our effect
on a render if the value of count
has changed.
const Counter = ({ onUpdate }) => {
const [counter, setCounter] = useState(0);
useEffect(() => {
onUpdate(counter);
}, [counter]);
const incrementCounter = setCounter(counter + 1);
return (
<div>
<button onClick={incrementCounter}>Increment Counter</button>
{counter}
</div>
)
}
componentDidMount / componentWillUpdate
It's common to have to perform an action that will need cleaning up when your component is unmounted. One example of such a scenario could be setting and clearing an interval/timer.
If you were implementing a counter than incremented every second, you could have set it up like in the example below. using componentDidMount
and componentDidUpdate
.
compose(
withState("counter", "setCounter", 0),
withState("intervalRef", "setIntervalRef"),
withHandlers({
incrementCounter: ({ counter, setCounter }) => () => {
setCounter(counter + 1);
}
}),
lifecycle({
componentDidMount() {
const { setIntervalRef, incrementCounter } = this.props;
const interval = setInterval(incrementCounter, 1000)
setIntervalRef(interval)
},
componentWillUnmount() {
clearInterval(this.props.intervalRef);
}
})
)(Counter)
You could achieve the same result with hooks by utilising our useEffect
with a couple of modifications.
- First of all we need to pass
[]
as the second argument. By not whitelisting props, theeffect
will only be called on mount. This is the syntax forcomponentDidMount
. - Secondly we need to return a function within our
effect
. Returning a function is the syntax for the equivalent to acomponentWillUnmount
. - Finally we need to make use of of the functional update form of setState.
You'll notice that we aren't referencing counter directly inside our
effect
. If we did, when our component mounts, the initial value of counter would get locked inside our effect. This would mean that every time the function we passed to oursetInterval
call runs it would always evaluate tosetCounter(0 + 1)
ascounter
would always be frozen at0
. Instead of passing in a new value to our state settersetCounter
you can also pass a function that gets called with the current state value and returns the new value.
const Counter = ({ onUpdate }) => {
const [counter, setCounter] = useState(0);
useEffect(() => {
const interval = setInterval(setCounter(count => count + 1), 1000);
// The function you return acts like your componentWillUnmount
return () => clearInterval(interval);
}, []);
return (
<div>
{counter}
</div>
)
}
Should component update
In recompose
the shouldUpdate
hoc would allow you to specify a custom function to determine whether your component should update or not.
compose(
shouldUpdate((props, nextProps) => props.id !== nextProps.id)
)(Person)
Instead of using shouldUpdate
you can wrap your component in React.memo()
.
Note: React.memo()
is not a hook.
By default your component will only update if props change. You can also pass a second argument to React.memo()
which is a custom equality check to determine if your component should update.
Watch out: We're checking for quality here rather than prop difference so we use ===
.
const Person = React.memo(
({ firstName, surname }) => {
return (
<div>
<span>{firstName}</span> <span>{surname}</span>
</div>
)
},
(prevProps, nextProps) => prevProps.id === nextProps.id
)