Skip to content

Instantly share code, notes, and snippets.

@zanaptak
Last active February 13, 2024 21:13
Show Gist options
  • Save zanaptak/20214830a59905f47a9c0bb0a9c22546 to your computer and use it in GitHub Desktop.
Save zanaptak/20214830a59905f47a9c0bb0a9c22546 to your computer and use it in GitHub Desktop.

React.memo, React.useCallback, React.useMemo

Normally, function components are rerendered every time the application renders. If you have large/complex components, this can affect application performance.

You can use React.memo, React.useCallback, and React.useMemo to memoize parts of your application. This means that React will cache and reuse their previous results instead of rerendering them.

Example:

We start with a useReducer-based Counter application, with the following additions:

  1. A renderCount helper function. This will let us see when components are rerendered by incrementing a value.
  2. A Button component.
  3. A Label component.

Here is the initial application:

open Feliz

type State = { Count : int }
type Msg = Increment | Decrement

let initialState = { Count = 0 }

let update (state: State) = function
    | Increment -> { state with Count = state.Count + 1 }
    | Decrement -> { state with Count = state.Count - 1 }

let renderCount =
    let mutable count = 0
    fun text -> count <- count + 1; sprintf "%s [render:%i]" text count

let Button =
    React.functionComponent(fun (props: {| text: string; action: unit->unit |}) ->
        Html.button [ prop.onClick (fun _ -> props.action()); prop.text (renderCount props.text) ])

let Label =
    React.functionComponent(fun (props: {| text: string |}) -> Html.div (renderCount props.text))

let Counter = React.functionComponent(fun () ->
    let (state, dispatch) = React.useReducer(update, initialState)
    Html.div [
        Html.div (renderCount (string state.Count))
        Button {| text = "Incr"; action = fun _ -> dispatch Increment |}
        Button {| text = "Decr"; action = fun _ -> dispatch Decrement |}
        Label {| text = "MyLabel" |}
    ]
)

open Browser.Dom
ReactDOM.render(Counter, document.getElementById "root")

Run it, click the buttons, and note that all [render:NNN] values are changing on each click, meaning that all components are rerendering on each click.

We can memoize the Label component so that it doesn't rerender every time. Change it to use React.memo:

let Label =
    React.memo(fun (props: {| text: string |}) -> Html.div (renderCount props.text))

The Label render count now no longer updates on clicks. React is no longer rerendering it on each click, it is using the cached result. React looks for changes in the props object to determine whether to rerender. Because we are using a hard-coded "MyLabel", React never has to rerender. In a real-world app, you would likely be using some value defined programmatically, and React would rerender when that value changes.

Now, try the same thing with Button, change it to React.memo. At first, it won't appear to work. The button render counts are still incrementing.

This is because of the action value in the props object that we are passing. It is an anonymous function that we are dynamically generating on the fly each time the Counter component renders. React doesn't know that it has the same behavior, it only sees a new function object reference each time and thus, because the props have changed, rerenders the Button.

The solution for this is useCallback. It creates a memoized (or stable) reference to a function, and reuses it on subsequent calls.

Update the Button actions to use useCallback:

Button {| text = "Incr"; action = React.useCallback(fun _ -> dispatch Increment) |}
Button {| text = "Decr"; action = React.useCallback(fun _ -> dispatch Decrement) |}

Now, the Buttons are successfully memoized just like the Label.

Similar to memo using changed props to determine whether to rerender, the useCallback function takes an optional dependency array that React will check for changes to determine whether to update the function reference. Since we are using hard-coded functions here, we've omitted this parameter and React permanently uses the cached reference. A real app might, for example, programmatically use different message types based on received props, and could use something like React.useCallback((fun _ -> dispatch props.msgType), [| props.msgType |]) to update the reference only when the type changes.

Here is our final memoized application:

open Feliz

type State = { Count : int }
type Msg = Increment | Decrement

let initialState = { Count = 0 }

let update (state: State) = function
    | Increment -> { state with Count = state.Count + 1 }
    | Decrement -> { state with Count = state.Count - 1 }

let renderCount =
    let mutable count = 0
    fun text -> count <- count + 1; sprintf "%s [render:%i]" text count

let Button =
    React.memo(fun (props: {| text: string; action: unit->unit |}) ->
        Html.button [ prop.onClick (fun _ -> props.action()); prop.text (renderCount props.text) ])

let Label =
    React.memo(fun (props: {| text: string |}) -> Html.div (renderCount props.text))

let Counter = React.functionComponent(fun () ->
    let (state, dispatch) = React.useReducer(update, initialState)
    Html.div [
        Html.div (renderCount (string state.Count))
        Button {| text = "Incr"; action = React.useCallback(fun _ -> dispatch Increment) |}
        Button {| text = "Decr"; action = React.useCallback(fun _ -> dispatch Decrement) |}
        Label {| text = "MyLabel" |}
    ]
)

open Browser.Dom
ReactDOM.render(Counter, document.getElementById "root")

Finally, useMemo is similar to the above concepts, but for memoizing an arbitrary function return value. For example:

let someValue = React.useMemo((fun _ -> someExpensiveCalculation props.inputValue), [| props.inputValue |])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment