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

Exposing React APIs: hooks and components #48

Closed Zaid-Ajaj closed 5 years ago

Zaid-Ajaj commented 5 years ago

Explore different idea's of easily exposing React's functionality through Feliz instead of falling back to Fable.React since these functions are at the core of React/Elmish apps and should be used in order to be able to profile and optimize for unnecessary diffs in the application tree but also an easy way to componentisation and internalizing local state as opposed to using Elmish states everywhere

1 - A dedicated React module/static type that contains functions to create hooks and components (I am in favor of this one!)

open Feliz

let counter = React.element "Counter" <| fun (props: {| count: int |} ->
  let (count, setCount) = React.useState props.count
  Html.div [
    Html.h1 count
    Html.button [ 
      prop.onClick (fun _ -> setCount(count + 1))
      prop.text "Increment"
    ]
  ]

let render state dispatch = 
  Html.div [
    counter {| count = state.Count |}
    counter {| count = state.Count * 2 |}
  ]

Here React.useState is a hook and React.element is used to create a function component, similar (and might be an alias to) FunctionComponent.Of

The use of element instead of component because of how overloaded the term "component" is but the same be said for element as well. Any suggestions on this one?

2 - Introduce a Feliz.React namespace where these functions are globally available

open Feliz
open Feliz.React

let counter = element "Counter" <| fun (props: {| count: int |} ->
  let (count, setCount) = useState props.count
  Html.div [
    Html.h1 count
    Html.button [ 
      prop.onClick (fun _ -> setCount(count + 1))
      prop.text "Increment"
    ]
  ]

let render state dispatch = 
  Html.div [
    counter {| count = state.Count |}
    counter {| count = state.Count * 2 |}
  ]

Make Reacts function available by only opening Feliz:

open Feliz

let counter = element "Counter" <| fun (props: {| count: int |} ->
  let (count, setCount) = useState props.count
  Html.div [
    Html.h1 count
    Html.button [ 
      prop.onClick (fun _ -> setCount(count + 1))
      prop.text "Increment"
    ]
  ]

Let me hear your thoughts please! @cmeeren @zanaptak @vbfox @MangelMaxime @alfonsogarciacaro

cmeeren commented 5 years ago

I'm leaning towards 1, but wouldn't a dedicated static type (not module) be better, to allow overloads like the rest of the Feliz API?

I've had to use this in Feliz.MaterialUI, see e.g. the Styles type with several overloads for Styles.makeStyles.

cmeeren commented 5 years ago

Also, do you plan on still depending on Fable.React or do you plan on creating custom wrappers? I'm asking because Feliz.MaterialUI currently depends on Fable.React stuff (e.g. IRefValue) transitively through Feliz, and if you plan on implementing your own React stuff in Feliz, then Feliz.MaterialUI should be updated to use that instead. And since it's a breaking change and Feliz.MaterialUI is not released yet, it's nice to know.

Zaid-Ajaj commented 5 years ago

but wouldn't a dedicated static type (not module) be better, to allow overloads like the rest of the Feliz API?

Indeed that's what I had in mind but I said module because I meant in the way you would access a function by fully qualifying the name React

do you plan on still depending on Fable.React or do you plan on creating custom wrappers?

Right now, I am thinking the only thing I want from Fable.React is ReactElement for compatibility with other applications and all existing third-party components, otherwise all existing and future third-party libraries would have to be (re)written with Feliz as a dependency if they were to work together which is a no-go if I want people to adopt this library one step at a time. As for React functions like hooks and components, I think I will write Feliz wrappers on top of them, then the implementation would be either a binding from scratch or fallback to Fable.React's if it makes sense

MangelMaxime commented 5 years ago

I am in favour of option 1 but I would prefer to use component because this is what you are creating.

For me, an element is really just the native DOM elements like div, etc. Also, using React terms would it easier for people to understand what's going on and searching documentation.

For the same reason, I would prefer to use Hooks.useState instead of React.useState the main problem being if people have both Fable.React and Feliz then Hooks.useState will depend on the open order.

Zaid-Ajaj commented 5 years ago

For me, an element is really just the native DOM elements like div, etc. Also, using React terms would it easier for people to understand what's going on and searching documentation.

I wanted to use "element" because I want users to think of as "just another element you can use in your render functions" there is no distinction between "native element" vs "component" there are only elements. Some "native elements" actually behave more like components such as input that has internal state but still it is just an element that can be used anywhere.

When you use React.element you are just creating an element with potentially some internalized state/behavior. Also it is a short word which encourages people to use it.

I like the word component too but it could mean A LOT of things and people might think it something that you are not supposed to do often but you actually should! Second con of the word is that it is preserved as a "future" F# keyword

For the same reason, I would prefer to use Hooks.useState instead of React.useState the main problem being if people have both Fable.React and Feliz then Hooks.useState will depend on the open order.

In javascript you do import { useState } from 'react', that is why I want to put in React without introducing many modules to look for things. Also it is highly unlikely that someone will open Feliz and Fable.React in the same module because they will quickly get name resolution conflicts when using style for some reason

zanaptak commented 5 years ago

Option 1 👍

What about React.functionComponent to get around the reserved word issue?

This library is already React-focused, i.e. using prop terminology, and React docs make a distinction between element and component with separate sections. I would find element more confusing personally.

Overloaded meaning of component isn't an issue with qualified use -- React.functionComponent would be unambiguous.

Zaid-Ajaj commented 5 years ago

@zanaptak I like React.functionComponent, I guess you and @MangelMaxime are right, they are function components at the end of the day and the docs say so, I guess we can also have xml docs in the function that refers to the React docs

Zaid-Ajaj commented 5 years ago

This is looking really good:

Using React.functionComponent with React.useState:

open Feliz

let counter =
    React.functionComponent(fun () ->
        let (count, setCount) = React.useState(0)
        Html.div [
            Html.h1 count
            Html.button [
                prop.text "Increment"
                prop.onClick (fun _ -> setCount(count + 1))
            ]
        ]
    )

Replace MVU with built-in React.useReducer

module Reducers =
    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 counter = React.functionComponent("Counter", fun () ->
        let (state, dispatch) = React.useReducer(update, initialState)
        Html.div [
            Html.h3 state.Count
            Html.button [ prop.onClick (fun _ -> dispatch Increment); prop.text "Increment" ]
            Html.button [ prop.onClick (fun _ -> dispatch Decrement); prop.text "Decrement" ]
        ]
    )

Use React.useEffect:

let ticker =
    React.functionComponent("Ticker", fun (props: {| start: int |}) ->
        let (tick, setTick) = React.useState props.start
        React.useEffect(fun () ->
            let interval = setInterval (fun () ->
                printfn "Tick"
                setTick(tick + 1)) 1000
            // creates a disposable value
            React.createDisposable(fun () -> clearInterval(interval))
        ,prop.start) // re-evaluate hook when `props.start` changes

        Html.h1 tick
    )

// later
Html.fragment [ 
  ticker {| start = 0 |}
  ticker {| start = 5 |}
]
cmeeren commented 5 years ago

Looks great! Will there be an overload of functionComponent accepting some memo stuff, like Fable.React's FunctionComponent.Of? (I don't know if it's part of the native React API or not.)