Zaid-Ajaj / Feliz

A fresh retake of the React API in Fable and a collection of high-quality components to build React applications in F#, optimized for happiness
https://zaid-ajaj.github.io/Feliz/
MIT License
534 stars 78 forks source link

Hot reload resets `[<ReactComponents>]` state #526

Closed Freymaurer closed 1 year ago

Freymaurer commented 1 year ago

Hello! I am using the SAFE stack with hot reload and the main model is not resetted when i apply changes to my code. Recently i started using [<ReactComponents>] from this library for all the styling stuff so my main model stays clean and i noticed, that whenever i save any changes made to my code it resets the state of all [<ReactComponents>] to their initial state determined in React.useState. Is this intended? Can this be circumvented?

I tried this with a fresh SAFE stack (v4.1.1) and a minimal example of:

[<ReactComponent>]
let minimalTest() =
    let counter, setCounter = React.useState(2)
    Html.div [
        Html.div counter
        Html.button [
            prop.onClick (fun _ ->
                setCounter (counter+1)
            )
            prop.text "increase"
        ]
    ]

/// `model` and `dispatch` from the main model both not used.
let view (model: Model) (dispatch: Msg -> unit) =
    minimalTest()

Thanks for your time in advance!

MangelMaxime commented 1 year ago

In order for a JavaScript module to support hot reload it needs to only have React components made public.

In your code, the view function is public and is not a react component.

Quoting myself from another issue:

For React refresh to work you need to only export ReactComponent from the module.

So mark init, update, or any other helpers function/values as private.

For vite.js, their is also the need to add this line

// Workaround to have React-refresh working
// I need to open an issue on react-refresh to see if they can improve the detection
emitJsStatement () "import React from \"react\""

Without this line vite.js doesn't seems to apply React Refresh

Example of a module:

using the bad example of counter 🙈

module Antidote.Components.Counter

// Stupid counter component
// This component is used to test integration with React and ViteJS
// In the future, this component will be removed from the project
// but I keep it in case I need to test/fix more stuff from Feliz

open Feliz
open Feliz.Bulma
open Elmish
open Fable.Core
open Feliz.UseElmish
open Fable.Core.JsInterop

// Workaround to have React-refresh working
// I need to open an issue on react-refresh to see if they can improve the detection
emitJsStatement () "import React from \"react\""

type private Classes =

    [<Emit("$0[\"counter-value\"]")>]
    abstract counterValue : string

let private classes : Classes = import "default" "./Counter.module.scss"

type private Model =
    {
        Value : int
    }

type private Msg =
    | Increment
    | Decrement

let private init () =
    {
        Value = 0
    }
    , Cmd.none

let private update (msg : Msg) (model : Model) =
    match msg with
    | Increment ->
        { model with
            Value = model.Value + 1
        }
        , Cmd.none

    | Decrement ->
        { model with
            Value = model.Value - 1
        }
        , Cmd.none

[<ReactComponent>]
let Counter () =
    let model, dispatch = React.useElmish(init, update, [||])

    Html.div [
        Html.div [
            prop.className classes.counterValue
            prop.text model.Value
        ]

        Bulma.button.button [
            prop.onClick (fun _ ->
                dispatch (Increment)
            )

            prop.text "Increment"
        ]

        Bulma.button.button [
            prop.onClick (fun _ ->
                dispatch (Decrement)
            )

            prop.text "Decrement"
        ]
    ]
Freymaurer commented 1 year ago

Sorry took me some time to try this... and it works! Thanks a lot.