useEffect
The Effect Hook lets you perform side effects in function components.
What are Effects?
Common examples for (side) effects are data fetching, setting up a subscription, and manually changing the DOM in React components.
There are two common kinds of side effects in React components: those that don’t require cleanup, and those that do. We'll look into the distinction later on in our examples, but first let's see how the interface looks like.
Basic Usage
// Runs after every completed render
React.useEffect(() => {
// Run effects
None // or Some(() => {})
})
// Runs only once right after mounting the component
React.useEffect0(() => {
// Run effects
None // or Some(() => {})
})
// Runs everytime `prop1` has changed
React.useEffect1(() => {
// Run effects based on prop1
None
}, [prop1])
// Runs everytime `prop1` or `prop2` has changed
React.useEffect2(() => {
// Run effects based on prop1 / prop2
None
}, (prop1, prop2))
React.useEffect3(() => {
None
}, (prop1, prop2, prop3));
// useEffect4...7 with according dependency
// tuple just like useEffect3
React.useEffect
receives a function that contains imperative, possibly effectful code, and returns a value option<unit => unit>
as a potential cleanup function.
A useEffect
call may receive an additional array of dependencies (see React.useEffect1
/ React.useEffect2...7
). The effect function will run whenever one of the provided dependencies has changed. More details on why this is useful down below.
Note: You probably wonder why React.useEffect1
receives an array
, and useEffect2
etc require a tuple
(e.g. (prop1, prop2)
) for the dependency list. That's because a tuple can receive multiple values of different types, whereas an array
only accepts values of identical types. It's possible to replicate useEffect2
by doing React.useEffect1(fn, [1, 2])
, on other hand the type checker wouldn't allow React.useEffect1(fn, [1, "two"])
.
React.useEffect
will run its function after every completed render, while React.useEffect0
will only run the effect on the first render (when the component has mounted).
Examples
Effects without Cleanup
Sometimes, we want to run some additional code after React has updated the DOM. Network requests, manual DOM mutations, and logging are common examples of effects that don’t require a cleanup. We say that because we can run them and immediately forget about them.
As an example, let's write a counter component that updates document.title
on every render:
// Counter.res
module Document = {
type t;
@val external document: t = "document";
@set external setTitle: (t, string) => unit = "title"
}
@react.component
let make = () => {
let (count, setCount) = React.useState(_ => 0);
React.useEffect(() => {
open Document
document->setTitle(`You clicked ${Belt.Int.toString(count)} times!`)
None
}, );
let onClick = (_evt) => {
setCount(prev => prev + 1)
};
let msg = "You clicked" ++ Belt.Int.toString(count) ++ "times"
<div>
<p>{React.string(msg)}</p>
<button onClick> {React.string("Click me")} </button>
</div>
}
In case we want to make the effects dependent on count
, we can just use following useEffect
call instead:
RES React.useEffect1(() => {
open Document
document->setTitle(`You clicked ${Belt.Int.toString(count)} times!`)
None
}, [count]);
Now instead of running an effect on every render, it will only run when count
has a different value than in the render before.
Effects with Cleanup
Earlier, we looked at how to express side effects that don’t require any cleanup. However, some effects do. For example, we might want to set up a subscription to some external data source. In that case, it is important to clean up so that we don’t introduce a memory leak!
Let's look at an example that gracefully subscribes, and later on unsubscribes from some subscription API:
// FriendStatus.res
module ChatAPI = {
// Imaginary globally available ChatAPI for demo purposes
type status = { isOnline: bool };
@val external subscribeToFriendStatus: (string, status => unit) => unit = "subscribeToFriendStatus";
@val external unsubscribeFromFriendStatus: (string, status => unit) => unit = "unsubscribeFromFriendStatus";
}
type state = Offline | Loading | Online;
@react.component
let make = (~friendId: string) => {
let (state, setState) = React.useState(_ => Offline)
React.useEffect(() => {
let handleStatusChange = (status) => {
setState(_ => {
status.ChatAPI.isOnline ? Online : Offline
})
}
ChatAPI.subscribeToFriendStatus(friendId, handleStatusChange);
setState(_ => Loading);
let cleanup = () => {
ChatAPI.unsubscribeFromFriendStatus(friendId, handleStatusChange)
}
Some(cleanup)
})
let text = switch(state) {
| Offline => friendId ++ " is offline"
| Online => friendId ++ " is online"
| Loading => "loading..."
}
<div>
{React.string(text)}
</div>
}
Effect Dependencies
In some cases, cleaning up or applying the effect after every render might create a performance problem. Let's look at a concrete example to see what useEffect
does:
RES// from a previous example above
React.useEffect1(() => {
open Document
document->setTitle(`You clicked ${Belt.Int.toString(count)} times!`)
None;
}, [count]);
Here, we pass [count]
to useEffect1
as a dependency. What does this mean? If the count
is 5, and then our component re-renders with count still equal to 5, React will compare [5]
from the previous render and [5]
from the next render. Because all items within the array are the same (5 === 5), React would skip the effect. That’s our optimization.
When we render with count updated to 6, React will compare the items in the [5]
array from the previous render to items in the [6]
array from the next render. This time, React will re-apply the effect because 5 !== 6
. If there are multiple items in the array, React will re-run the effect even if just one of them is different.
This also works for effects that have a cleanup phase:
RES// from a previous example above
React.useEffect1(() => {
let handleStatusChange = (status) => {
setState(_ => {
status.ChatAPI.isOnline ? Online : Offline
})
}
ChatAPI.subscribeToFriendStatus(friendId, handleStatusChange);
setState(_ => Loading);
let cleanup = () => {
ChatAPI.unsubscribeFromFriendStatus(friendId, handleStatusChange)
}
Some(cleanup)
}, [friendId]) // Only re-subscribe if friendId changes
Important: If you use this optimization, make sure the array includes all values from the component scope (such as props and state) that change over time and that are used by the effect. Otherwise, your code will reference stale values from previous renders
If you want to run an effect and clean it up only once (on mount and unmount), use React.useEffect0
.
If you are interested in more performance optmization related topics, have a look at the ReactJS Performance Optimization Docs for more detailed infos.