Shmew / Fable.SignalR

A functional type-safe wrapper for SignalR and Fable.
https://shmew.github.io/Fable.SignalR/
MIT License
91 stars 17 forks source link

How to capture current React Component state inside SignalR hub onMessage handler? #19

Closed theimowski closed 3 years ago

theimowski commented 3 years ago

Apologies for a noob question but I can't get my head around following: In the sample code you're sending request to increment counter from client, the counter is incremented on server, and new counter is sent back to client in response and then set inside React Component state:

let render = React.functionComponent(fun () ->
    let count,setCount = React.useState 0

    let hub =
        React.useSignalR<Action,Response>(fun hub -> 
            hub.withUrl(Endpoints.Root)
                .withAutomaticReconnect()
                .configureLogging(LogLevel.Debug)
                .onMessage <|
                    function
                    | Response.NewCount i -> setCount i
                    | Response.RandomCharacter str -> setText str
        )

now say I'd like to capture current count state when handling the message, e.g. do increment on client side:

                    // ignore counter from server and try using current state - but count is always 0
                    | Response.NewCount _ -> setCount (count + 1)

But this approach doesn't work - count evaluates always to 0.

The approach works fine with bare state hook:

[<ReactComponent>]
let Counter () =
    let x, setX = React.useState 0

    Html.div [
        Html.h1 (string x)
        Html.button [
           // works as expected
           prop.onClick (fun _ -> setX (x + 1))
        ]
    ]

Is that by design? How can I work around that?

Shmew commented 3 years ago

This is due to the fact that the hub is a ref and thus only created once, so the value of count is always going to be 0.

This can be fixed like so:

let render = React.functionComponent(fun () ->
    let count,setCount = React.useState 0

    let countOne = React.useCallbackRef(fun () -> setCount (count + 1))

    let hub =
        React.useSignalR<Action,Response>(fun hub -> 
            hub.withUrl(Endpoints.Root)
                .withAutomaticReconnect()
                .configureLogging(LogLevel.Debug)
                .onMessage <|
                    function
                    | Response.NewCount _ -> countOne()
        )

I think in this situation it's probably better to just go ahead and wrap the entire function in the useCallbackRef hook:

let render = React.functionComponent(fun () ->
    let count,setCount = React.useState 0

    let handleMsg =
        React.useCallbackRef (fun msg -> 
            match msg with
            | Response.NewCount i -> setCount (count + 1)
        )

    let hub =
        React.useSignalR<Action,Response>(fun hub -> 
            hub.withUrl(Endpoints.Root)
                .withAutomaticReconnect()
                .configureLogging(LogLevel.Debug)
                .onMessage(handleMsg))

An alternative solution:

let render = React.functionComponent(fun () ->
    let count,setCount = React.useState 0

    let otherCount,setOtherCount = React.useState 0

    let hub =
        React.useSignalR<Action,Response>(fun hub -> 
            hub.withUrl(Endpoints.Root)
                .withAutomaticReconnect()
                .configureLogging(LogLevel.Debug)
                .onMessage <|
                    function
                    | Response.NewCount i -> setCount i
        )

    React.useEffect((fun () -> setOtherCount (count + 1)), [| count |])

I'm glad you brought this up, I should add this scenario to the documentation. Thanks!

theimowski commented 3 years ago

Thanks for explanation, I'll try that approach!

mastoj commented 3 years ago

Great example @Shmew , but how does the last one work?

Shmew commented 3 years ago

Whenever count is updated (via onMessage) the component will re-render, whenever the component re-renders and count has changed React will execute the function passed via useEffect setting otherCount. It's a bit convoluted, but demonstrates a way to fire additional side-effects after onMessage.