elmish / react

Elmish React extensions for writing SPA and Native apps
https://elmish.github.io/react
Other
104 stars 21 forks source link

How to use components that have instance methods #25

Closed thitemple closed 6 years ago

thitemple commented 6 years ago

Hi, I'd like to know if there's a recommended way to use components that have instance methods.

What we usually see in regular JS React component is something like this:

class MyComponent extends Component {
  render () {
    return (<SomeComponent ref={c => this.someComponentInstance = c} />)
  }
}

Using elmish should I handle the ref call, get the instance of the component and dispatched to be stored in the model? That's the best that I came up with and was wondering what is the approach normally.

Thanks

MangelMaxime commented 6 years ago

Storing the instance of you component in the elmish model can make your application slow and I am not sure it will be correct over time.

I mean your application model is immutable so every time you update your model, then you make a copy of it and then update the needed fields.

Perhaps, you should use a stateful model as a wrapper or one of the custom components provided by Fable.React source.

Also, I don't think passing your component parent state/instance to the children is the right approach. In my application, when I encounter similar case, I prefer to create a "contract props" (using a records for example). And then I build/update this props and pass it to the child.

Another, thing I can mention is if you need to do something similar but for your Elmish components don't forget your view is just a function so you can pass arguments to it.

let view contractProps model dispatch = 
  // ......
thitemple commented 6 years ago

I'm sorry, I'm not sure I'm following. I understand that having it in the model is not the best idea.

Am I suppose to use the reactiveCom function to create my component? How's that changing anything?

I came up with this code while testing, how could I keep a reference to one of react's components?

type MyCompProps = {
    GreetingValue : string
}

type MyCompState = {
    Greeting : string
}

let init (p : MyCompProps) =
    { Greeting = p.GreetingValue }

type ChildMsg =
    | AddPonctuation

let update msg model =
    match msg with
    | AddPonctuation ->
        { model with Greeting = sprintf "%s!" model.Greeting }

let myComp =
    reactiveCom
        init
        update
        (fun model dispatch ->
            view [] <|
                List.append [ text [] model.state.Greeting ] (Array.toList model.children))
        "something"
        { GreetingValue = "Hello here" }
        [
            button [ ButtonProperties.Title "Do something"; ButtonProperties.OnPress (fun () -> ()) ] []
        ]
alfonsogarciacaro commented 6 years ago

Why do you need to do this? The only time I needed to use ref is when using React together with a non-React library like jQuery. In other cases, in general it's usually better to handle all state-related logic in the update function and keep the view function as pure as possible (like @MangelMaxime says). Though sometimes you may want to enable some state in your React components to avoid excessive pollution of Elmish state. In those cases you can just create a custom React component or use the reactiveCom helper that simulates a mini-Elmish app.

thitemple commented 6 years ago

I'm all for keeping it pure, the think is I'm trying to use this component and it has some instance methods that I need to access such as:

https://github.com/mapbox/react-native-mapbox-gl/blob/master/docs/MapView.md#methods

There is a lot of functionality from this component that is only accessible via its instance methods.

If there's a better way to do that, I will gladly implement it, but I'm out of ideas. Using reactiveCom like I tried in the above example would help me how in this case? I would still need to keep a reference to the component somewhere. Would it be better to similate that mini-elmish app by storing the ref in the model provided to reactiveCom ?

alfonsogarciacaro commented 6 years ago

Ah, ok, sorry, I didn't understand your needs exactly. Well, having to use instance methods is not a pattern that fits very nicely with Elmish but if the component authors have decided to do it like this it can't be helped. Off the top of my head your only solutions are:

  1. Store the reference in a mutable variable somewhere. For a bit of safety, I would use Option.ofObj to turn the reference into an option (IIRC, React will call ref with null value when unmounting the component): Ref (fun ref -> myCache <- Option.ofObj(ref :?> ExpectedType))

  2. If you don't want to diverge from Elmish much, just treat Ref as an event, so when you get the reference you send it to the update function with a message, then you can access it from the Elmish state: Ref (fun ref -> ref :?> ExpectedType |> Option.ofObj |> Msg.UpdateReference |> dispatch).

thitemple commented 6 years ago

Thanks for the help so far.

So option 2 is what I was doing, I was just keeping the instance passed to ref in the model because the ref prop is only called once and I need to be able to some of the methods later on, and not just when the component gets created.

Isn't that what @MangelMaxime was saying is bad for performance?

alfonsogarciacaro commented 6 years ago

I think he meant in case you save a copy of the component every time view is called. But here it should be fine because ref is only called when the component is mounted the first time as far as I know.

In any case, here's a good guide with the gotchas to avoid unnecessary rerenders when using React + Fable + Elmish: https://blog.vbfox.net/2018/02/06/fable-react-1-react-in-fable-land.html

MangelMaxime commented 6 years ago

Alfonso, is right the performance problem is if you store the state in your model.

At work, I am using tech n°1 for something similar. I am using React.Leaflet and because not all the API is mapped via props we have access to the leaflet instance from the Ref property. So from here, I store the instance reference in a private mutable variable.

I didn't though about case n°2. But it can probably works too. I guess, you will have to experiment a bit with both and see what's is better for your usage.

Example from my code:

// Variable to store the instance
let private mutable mapRef : Leaflet.Map = null

// Retrieve the map instance
RL.MapProps.Ref (fun x ->
    if not (isNull x) then
        Sub.mapRef <- unbox<Leaflet.Map> x?leafletElement)

// Secure every call to the instance methods
    | ChangeZoomMin value ->
        try
            Sub.mapRef.setMinZoom(float value) |> ignore
        with
            | _ ->  ()

        { model with ZoomMin = value }
        |> checkIfDataChanged originalData
        |> bounceFormValidation Cmd.none
        |> noExternalMessage

It's better of course if you use Map option type. After, in general in my model, I am a boolean to detect if the map is loaded or no. And from this boolean I can match over it in order to prevent unexpected behavior to occur.

thitemple commented 6 years ago

Thanks this was really helpful and, if there's no performance downside on using what was proposed as option number 2 I think I prefer it that way because the application will be consistent throughout with all data coming from the model, although I have to say, option one is simpler as a solution.

A second quick question now, the main use for a ReactiveComponent would be to create a component for the application that needs state and can be re-used, is that it?

I'm thinking Elm right and in Elm, those sort of "components" have their state in the application Model and their messages have to dispatched from the Application's update to the component's update function. So this ReactiveComponent is to do something similar and keep all the state contained in the component?

Thanks

alfonsogarciacaro commented 6 years ago

The reactiveCom is a wrapper around a stateful React component emulating a mini-Elmish app. It encapsulates state (though it still can send messages to the Application state) so you can use it when you want to keep certain view details away from the App state (e.g. whether a dropdown is open or not).

Of course this also means changes in the encapsulated state won't go through the update function, debugger, app state backups, etc. So you need to weigh pros and cons when using it.

thitemple commented 6 years ago

Thanks guys, this was very helpful. BTW, I love the whole work you've been doing with Fable/Elmish, I'm using it only for a ReactNative app but I plan on doing a web app soon as well.

I do think though the documentation could be improved and I want to offer help on that front if that's what you want, just let me know if you thought about this. For instance, do you want the docs to be on Fable's site? Or do you want those separated, in the https://elmish.github.io/ site.

Anyway, just trying to help here...

alfonsogarciacaro commented 6 years ago

You're totally right @thitemple in that we're missing more docs, and it's very kind of you to offer to help, thank you! I think right now there're 3 main Fable-related sites:

I'm trying to keep Fable docs short so people read them (though I'm not very successful at it). Elmish site must be focused on the architecture and not too much on Fable (we have now Elmish for Xamarin), and SAFE is where everything comes together but it's a bit centered on web apps (though they also have a RN sample in the repo).

I'm sure any of these sites will appreciate a PR very much. But given that you're doing a React Native app and we don't have any Fable RN tutorial (besides @forki video at NDC Oslo), it'd be nice if you could write a guide to set up a Fable-Elmish RN app step by step (that would be very helpful for me too). If you have a blog, you can put it there and we can add links to it from any of those sites and awesome-fable. Or we can host it directly in Fable's blog as you prefer :)

thitemple commented 6 years ago

Alright, I do have a blog and I can definitely work on something for RN. And I'll try to propose a PR for the Elmish documentation