Docs / rescript-react / useEffect Hook
Edit

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

ReScriptJS Output
// 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:

ReScriptJS Output
// 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:

ReScriptJS Output
// 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.