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
544 stars 81 forks source link

Question: Global Elmish #280

Closed Rekeyea closed 3 years ago

Rekeyea commented 3 years ago

Hi! First of all Great Work building this library! It's amazing! I wanted to ask how this played with Elmish. I know there's the useElmish hook but as I understand It only works at the Component level. But what If I wanted a global state to share data? I tried building my own hook on global state and the definition ended up like this:

let runReact state dispatch =
    let stateContext = getAndSetStateContext state dispatch
    ReactDOM.render(
        React.contextProvider(stateContext, (state, dispatch), React.fragment [ Layout.MasterPage() ]),
        document.getElementById("app")
    )

Program.mkProgram init update runReact
|> Program.run

Then I'd use a hook to get the "global" state and dispatch. However, when updating the state, the whole app refreshes (I guess because the ReactDOM.render is called again. So my question is how should all this be done? am I missing something?

Regards

Shmew commented 3 years ago

React is a tree so any usage of useElmish can be pulled up any number of levels until you hit the root component of your application. The issue here is due to the nature of the hook it's going to cause the root component to re-render when the model changes. It should actually be pretty easy to create an extension to the context API as part of the useElmish library.

This would end up looking something like this:

let elmishContext = React.createElmishContext(init, update)

let myChildComp = React.functionComponent(fun () ->
    let model,dispatch = React.useContext(elmishContext)

    ...
)

let app = React.functionComponent (fun () ->
    React.elmishProvider elmishContext [
        myChildComp()
    ]
)

I need to check if you can use hooks in some parts, but if not I have an idea on how that may be able to be worked around.

Thoughts @Zaid-Ajaj? I can send a PR if you like it.

MaxWilson commented 3 years ago

Is there a reason you can't just let the state be managed by Elmish as usual, as per https://zaid-ajaj.github.io/Feliz/#/Feliz/UseWithElmish?

let runReact state dispatch =
    React.fragment [ Layout.MasterPage() ]

Program.mkProgram init update runReact
|> Program.run

Then you let your update function compute the new state in response to messages, per usual. No need for useElmish in this scenario.

Shmew commented 3 years ago

Yeah you can do that, and for the most part it's fine. However, it will not work in the future without significant changes (React concurrent mode). I've also run into some weird behavior when mixing the two, but that's pretty rare.

This is obviously more based on use-case/preference, but after moving off the monolithic style of Elmish, I've never missed it.

Rekeyea commented 3 years ago

Is there a reason you can't just let the state be managed by Elmish as usual, as per https://zaid-ajaj.github.io/Feliz/#/Feliz/UseWithElmish?

let runReact state dispatch =
    React.fragment [ Layout.MasterPage() ]

Program.mkProgram init update runReact
|> Program.run

Then you let your update function compute the new state in response to messages, per usual. No need for useElmish in this scenario.

But then how would I call the dispatch function in child components?

Rekeyea commented 3 years ago

React is a tree so any usage of useElmish can be pulled up any number of levels until you hit the root component of your application. The issue here is due to the nature of the hook it's going to cause the root component to re-render when the model changes. It should actually be pretty easy to create an extension to the context API as part of the useElmish library.

This would end up looking something like this:

let elmishContext = React.createElmishContext(init, update)

let myChildComp = React.functionComponent(fun () ->
    let model,dispatch = React.useContext(elmishContext)

    ...
)

let app = React.functionComponent (fun () ->
    React.elmishProvider elmishContext [
        myChildComp()
    ]
)

I need to check if you can use hooks in some parts, but if not I have an idea on how that may be able to be worked around.

Thoughts @Zaid-Ajaj? I can send a PR if you like it.

Yeah that's more or less what I tried to do. It's probably an implementation problem on my side and not knowing how to do the createElmishContext part.

Zaid-Ajaj commented 3 years ago

But what If I wanted a global state to share data?

Hi @Rekeyea, if I understand correctly, you simply want to Feliz as part of an Elmish application following Elmish patterns? Then you can just use it in parts of the view with or without React components and it will work because it is compatible with the existing Elmish implementation without using React context features.

Feliz.UseElmish hook is made to simply the composition patterns of Elmish such that parent components do not need to manage state of their child components and instead expose a standalone React component that holds its internal state with an Elmish dispatch loop and can be used inside other React components. Basically, reducing the chattiness of Elmish composition.

But then how would I call the dispatch function in child components?

@Rekeyea You can simply pass the dispatch function as an argument for the child components. This is how classic "full" Elmish style is done. You can learn all about it in Chapter 4 - Composing Larger Applications of The Elmish Book.

Rekeyea commented 3 years ago

But what If I wanted a global state to share data?

Hi @Rekeyea, if I understand correctly, you simply want to Feliz as part of an Elmish application following Elmish patterns? Then you can just use it in parts of the view with or without React components and it will work because it is compatible with the existing Elmish implementation without using React context features.

Feliz.UseElmish hook is made to simply the composition patterns of Elmish such that parent components do not need to manage state of their child components and instead expose a standalone React component that holds its internal state with an Elmish dispatch loop and can be used inside other React components. Basically, reducing the chattiness of Elmish composition.

But then how would I call the dispatch function in child components?

@Rekeyea You can simply pass the dispatch function as an argument for the child components. This is how classic "full" Elmish style is done. You can learn all about it in Chapter 4 - Composing Larger Applications of The Elmish Book.

@Zaid-Ajaj I can pass the state and dispatch all over the children, that I know. However, is this a good thing to do? Redux for example uses this pattern of using a hook in the components that need the global state and dispatch. I read the book and think it's amazing but couldn't find a solution to passing both dispatch and state all over the place. Maybe it's just how it should be done and I'm overthinking it.

Zaid-Ajaj commented 3 years ago

However, is this a good thing to do?

It depends. Using React context to pass the dispatch and state into each child components is simply an easier way of passing the them explicitly down as parameters/props so it is really the same the thing IMO. However, when using the Elmish way to pass dispatch and state, you are really passing a derived version of them:

I tend to think that using a single type to model the entire application might not be the best idea because you start adding fields to state and more union cases to the possible message that are only relevant to small parts of the application but I could be wrong depending on what type of application you are building. The benefit of strictly following the classic Elmish way is that each component/program only knows about its own state type and its own message type without knowing what the rest of the application should be doing.

Rekeyea commented 3 years ago

Yeah I agree that each component should only see the state it cares for, and If passing the state explicitly is how it should be done then I'll do it that way. Thanks everyone for the discussion :-D

Shmew commented 3 years ago

Passing state/dispatch down through props isn't right or wrong. It's just one of the ways to handle this problem. In fact, I'd argue that unless you're hiding the actual state away in a component and then using a callback to send down to the other children it's strictly worse than using context as you're going to re-render anything that passes properties through rather than just the points where the context is consumed (without any other changes).