Skip to main content
Version: 2.9.0

Subscriptions with Effects

In the previous section, we saw how React.useEffect can be used to issue a side-effect and control when and how that effect is re-executed. However, there are cases where you want to have some cleanup code after the component unmounts or the props has changed. For example when we subscribe to some event as the component mounts, we want to unsubscribe to that event when the component unmounts.

To use React.useEffect with a cleanup phase, we call one these function signatures:

  • React.useEffect : (unit -> IDisposable) -> unit
  • React.useEffect : (unit -> IDisposable) * obj array -> unit

Where the first parameter of type unit -> IDisposable is the effect that returns IDisposable to signal that this effect has some cleanup code which runs after the component unmounts:

Show code
module Example.EffectfulTimer

open System
open Feliz
open Fable.Core.JS

[<ReactComponent(true)>]
let EffectfulTimer() =
let (paused, setPaused) = React.useState(false)
// using useStateWithUpdater instead of useState
// to avoid stale closures in React.useEffect
let (value, setValue) = React.useStateWithUpdater(0)

let subscribeToTimer() =
// start the timer
let subscriptionId = setInterval (fun _ -> if not paused then setValue (fun prev -> prev + 1)) 1000
// return IDisposable with cleanup code that stops the timer
{ new IDisposable with member this.Dispose() = clearInterval(subscriptionId) }

React.useEffect(subscribeToTimer, [| box paused |])

Html.div [
Html.h1 value

Html.button [
prop.style [
if paused then
style.backgroundColor "yellow"
else
style.backgroundColor "green"
]

prop.onClick (fun _ -> setPaused(not paused))
prop.text (if paused then "Resume" else "Pause")
]
]

Here we are subscribing with the value subscribeToTimer of type unit -> IDisposable:

let subscribeToTimer() =
// start the timer
let subscriptionId = setInterval (fun _ -> if not paused then setValue (fun prev -> prev + 1)) 1000
// return IDisposable with cleanup code that stops the timer
{ new IDisposable with member this.Dispose() = clearTimeout(subscriptionId) }

We return an IDisposable using an object expression but if you like functions more, you can use React.createDisposable which does exactly the same:

let subscribeToTimer() =
// start the ticking
let subscriptionId = setTimeout (fun _ -> if not paused then setValue (fun prev -> prev + 1)) 1000
// return IDisposable with cleanup code that stops the timer
React.createDisposable(fun _ -> clearTimeout subscriptionId)

useCancellationToken Hook

A common scenario is executing a promise or async function based on some input or user event.

When this component is unmounted, if you have a pending operation in-flight this can cause errors. The way to fix this is to pass a CancellationToken to your async call so it is cancelled when that token is disposed.

To make this easier you can use React.useCancellationToken() which will create a React IRefValue that you can pass to your calls (or children if you have a use case of not wanting to cancel an operation, but adjust logic if the component was unmounted).

Here is an example of how you could use this:

Show code
module Example.UseCancellationTokenHook

open Feliz
open Fable.Core

[<Erase; Mangle(false)>]
type Main =

[<ReactComponent>]
static member private UseToken (failedCallback: unit -> unit) =
let token = React.useCancellationToken ()

React.useEffect (fun () ->
async {
do! Async.Sleep 4000
failedCallback ()
}
|> fun a -> Async.StartImmediate(a, token.current))

Html.none

[<ReactComponent(true)>]
static member Main () =
let renderChild, setRenderChild = React.useState true
let resultText, setResultText = React.useState "Pending..."

let setFailed =
React.useCallback (fun () -> setResultText "You didn't cancel me in time!")

let isDisabled = resultText = "Disposed"

Html.div [
if renderChild then
Main.UseToken(setFailed)
Html.div resultText
Html.button [
prop.disabled isDisabled
prop.text "Dispose"
prop.onClick (fun _ ->
async {
setResultText "Disposed"
setRenderChild false
}
|> Async.StartImmediate)
]

Html.button [
prop.disabled (renderChild && resultText = "Pending...")
prop.text "Reset"
prop.onClick (fun _ ->
async {
setResultText "Pending..."
setRenderChild true
}
|> Async.StartImmediate)
]
]