Skip to content

Commit

Permalink
Merge pull request #591 from ArtemyB/react-hook-bindings-optimization
Browse files Browse the repository at this point in the history
Refactor React hooks and APIs F# API to improve the transpilation output
  • Loading branch information
Zaid-Ajaj authored Dec 25, 2023
2 parents 4ef46b8 + b0ac12a commit e2da7db
Show file tree
Hide file tree
Showing 8 changed files with 374 additions and 320 deletions.
52 changes: 52 additions & 0 deletions Feliz/DateParsing.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
module [<RequireQualifiedAccess>] Feliz.DateParsing

open System

let (|Between|_|) (x: int) (y: int) (input: int) =
if input >= x && input <= y
then Some()
else None
let isLeapYear (year: int) = DateTime.IsLeapYear(year)

let (|Int|_|) (input: string) =
try (Some (int input))
with _ -> None
/// <summary>Parses a specific yyyy-MM-dd or yyyy-MM-ddTHH:mm date format that comes out of an input element with type="date"</summary>
let parse (input: string) : Option<DateTime> =
try
if String.IsNullOrWhiteSpace input then None
else
let parts = input.Split('-')
let year, month, day, hour, minute =
match parts with
| [| Int year; Int month |] -> year, month, 1, 0, 0
| [| Int year; Int month; Int day |] -> year, month, day, 0, 0
| [| Int year; Int month; day |] ->
if day.Contains("T") then
match day.Split('T') with
| [| Int parsedDay; time |] ->
match time.Split ':' with
| [| Int hour; Int minute |] ->
match hour, minute with
| Between 0 59, Between 0 59 -> year, month, parsedDay, hour, minute
| _ ->
-1, 1, 1, 0, 0
| _ ->
-1, 1, 1, 0, 0
| _ ->
-1, 1, 1, 0, 0
else
-1, 1, 1, 0, 0
| _ ->
-1, 1, 1, 0, 0
if year <= 0 then
None
else
match month, day with
| 2, Between 1 29 when isLeapYear year -> Some (DateTime(year, month, day, hour, minute, 0))
| 2, Between 1 28 when not (isLeapYear year) -> Some (DateTime(year, month, day, hour, minute, 0))
| (1 | 3 | 5 | 7 | 8 | 10 | 12), Between 1 31 -> Some (DateTime(year, month, day, hour, minute, 0))
| (4 | 6 | 9 | 11), Between 1 30 -> Some (DateTime(year, month, day, hour, minute, 0))
| _ -> None
with
| _ -> None
16 changes: 13 additions & 3 deletions Feliz/Feliz.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,18 @@
<Description>A fresh retake of the React API in Fable, optimized for happiness</Description>
<PackageTags>fsharp;fable;react;html</PackageTags>
<Authors>Zaid Ajaj</Authors>
<Version>2.7.0</Version>
<Version>2.8.0-beta.0</Version>
<TargetFramework>netstandard2.0</TargetFramework>
<PackageReleaseNotes>Implement overflow anchor</PackageReleaseNotes>
<PackageReleaseNotes>Optimize React hooks and APIs Feliz API.</PackageReleaseNotes>
</PropertyGroup>

<ItemGroup>
<Content Include="*.fsproj; *.fs; *.js;" Exclude="**\*.fs.js" PackagePath="fable\" />
<Compile Include="Types.fs" />
<Compile Include="Helpers.fs" />
<Compile Include="Key.fs" />
<Compile Include="StyleTypes.fs" />
<Compile Include="ReactTypes.fs" />
<Compile Include="DateParsing.fs" />
<Compile Include="Interop.fs" />
<Compile Include="Colors.fs" />
<Compile Include="Fonts.fs" />
Expand All @@ -29,12 +31,20 @@
<Compile Include="Styles.fs" />
<Compile Include="Svg.fs" />
<Compile Include="Html.fs" />
<Compile Include="ReactInterop.fs" />
<Compile Include="Internal.fs" />
<Compile Include="React.fs" />
<Compile Include="ReactDOM.fs" />
</ItemGroup>

<ItemGroup>
<Content Include="*.fsproj; *.fs; *.js;" Exclude="**\*.fs.js" PackagePath="fable\" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Feliz.CompilerPlugins\Feliz.CompilerPlugins.fsproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Update="FSharp.Core" Version="4.7.2" />
<PackageReference Include="Fable.ReactDom.Types" Version="18.2.0" />
Expand Down
11 changes: 11 additions & 0 deletions Feliz/Helpers.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Feliz

open System
open System.ComponentModel
open Fable.Core

[<EditorBrowsable(EditorBrowsableState.Never); Erase>]
[<RequireQualifiedAccess>]
module Helpers =
let optDispose (disposeOption: #IDisposable option) =
{ new IDisposable with member _.Dispose () = disposeOption |> Option.iter (fun d -> d.Dispose()) }
176 changes: 176 additions & 0 deletions Feliz/Internal.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
namespace Feliz

open System
open Fable.Core
open Fable.Core.JsInterop


module Internal =

let propsWithKey (withKey: ('props -> string) option) props =
match withKey with
| Some f ->
props?key <- f props
props
| None -> props

let functionComponent
(renderElement: 'props -> ReactElement)
(name: string option)
(withKey: ('props -> string) option)
: 'props -> Fable.React.ReactElement =
name |> Option.iter (fun name -> renderElement?displayName <- name)
#if FABLE_COMPILER_3
Browser.Dom.console.warn("Feliz: using React.functionComponent in Fable 3 is obsolete, please consider using the [<ReactComponent>] attribute instead which makes Feliz output better Javascript code that is compatible with react-refresh")
#endif
fun props ->
let props = props |> propsWithKey withKey
Interop.reactApi.createElement(renderElement, props)
let memo
(renderElement: 'props -> ReactElement)
(name: string option)
(areEqual: ('props -> 'props -> bool) option)
(withKey: ('props -> string) option)
: 'props -> Fable.React.ReactElement =
let memoElementType = Interop.reactApi.memo(renderElement, (defaultArg areEqual (unbox null)))
name |> Option.iter (fun name -> renderElement?displayName <- name)
fun props ->
let props = props |> propsWithKey withKey
Interop.reactApi.createElement(memoElementType, props)

let inline useMemo (createFunction: unit -> 'a) (dependencies: (obj array) option) =
Interop.reactApi.useMemo createFunction (defaultArg dependencies [||])

let createDisposable (dispose: unit -> unit) =
{ new IDisposable with member _.Dispose() = dispose() }

[<Hook>]
let useDisposable (dispose: unit -> unit) =
useMemo (fun () -> createDisposable dispose) (Some [| dispose |])

let inline useEffectDisposableOptWithDeps (effect: unit -> #IDisposable option) (dependencies: obj []) =
ReactInterop.useEffectWithDeps (effect >> Helpers.optDispose) dependencies

let inline useEffectDisposableOpt (effect: unit -> #IDisposable option) =
ReactInterop.useEffect effect

let inline useEffectWithDeps (effect: unit -> unit) (dependencies: obj []) =
Interop.reactApi.useEffect(effect, dependencies)

let inline useEffect (effect: unit -> unit) =
Interop.reactApi.useEffect effect


[<Hook>]
let useEffectOnce(effect: unit -> unit) =
let calledOnce = Interop.reactApi.useRefInternal false

useEffectWithDeps (fun () ->
if not calledOnce.current then
calledOnce.current <- true
effect()
) [||]

[<Hook>]
let useEffectDisposableOnce (effect: unit -> #IDisposable) =
let destroyFunc = Interop.reactApi.useRefInternal None
let calledOnce = Interop.reactApi.useRefInternal false
let renderAfterCalled = Interop.reactApi.useRefInternal false

if calledOnce.current then
renderAfterCalled.current <- true

useEffectDisposableOptWithDeps (fun () ->
if calledOnce.current
then None
else
calledOnce.current <- true
destroyFunc.current <- effect() |> Some

if renderAfterCalled.current
then destroyFunc.current
else None
) [||]

[<Hook>]
let useEffectDisposableOptOnce (effect: unit -> #IDisposable option) =
let destroyFunc = Interop.reactApi.useRefInternal None
let calledOnce = Interop.reactApi.useRefInternal false
let renderAfterCalled = Interop.reactApi.useRefInternal false

if calledOnce.current then
renderAfterCalled.current <- true

useEffectDisposableOptWithDeps (fun () ->
if calledOnce.current
then None
else
calledOnce.current <- true
destroyFunc.current <- effect()

if renderAfterCalled.current
then destroyFunc.current
else None
) [||]


let createContext<'a> (name: string option) (defaultValue: 'a option) =
let contextObject = Interop.reactApi.createContext (defaultArg defaultValue Fable.Core.JS.undefined<'a>)
name |> Option.iter (fun name -> contextObject?displayName <- name)
contextObject

let inline useRef<'t>(initialValue: 't) = Interop.reactApi.useRefInternal(initialValue)

let inline useCallback(callbackFunction: 'a -> 'b) (dependencies: (obj array) option) =
Interop.reactApi.useCallback callbackFunction (defaultArg dependencies [||])

let inline useLayoutEffect(effect: unit -> unit) =
ReactInterop.useLayoutEffect
(fun _ ->
effect()
createDisposable ignore)

[<Hook>]
let useCallbackRef(callback: ('a -> 'b)) =
let lastRenderCallbackRef = useRef(callback)

let callbackRef =
useCallback (fun (arg: 'a) ->
lastRenderCallbackRef.current(arg)
) (Some [||])

useLayoutEffect(fun () ->
// render is commited - it's safe to update the callback
lastRenderCallbackRef.current <- callback
)

callbackRef

let forwardRef(render: ('props * IRefValue<'t> -> ReactElement)) : ('props * IRefValue<'t> -> ReactElement) =
let forwardRefType = Interop.reactApi.forwardRef(Func<'props,IRefValue<'t>,ReactElement> (fun props ref -> render(props,ref)))
fun (props, ref) ->
let propsObj = props |> JsInterop.toPlainJsObj
propsObj?ref <- ref
Interop.reactApi.createElement(forwardRefType, propsObj)

let forwardRefWithName (name: string) (render: ('props * IRefValue<'t> -> ReactElement)) : ('props * IRefValue<'t> -> ReactElement) =
let forwardRefType = Interop.reactApi.forwardRef(Func<'props,IRefValue<'t>,ReactElement> (fun props ref -> render(props,ref)))
render?displayName <- name
fun (props, ref) ->
let propsObj = props |> JsInterop.toPlainJsObj
propsObj?ref <- ref
Interop.reactApi.createElement(forwardRefType, propsObj)

[<Hook>]
let useCancellationToken () =
let cts = useRef(new System.Threading.CancellationTokenSource())
let token = useRef(cts.current.Token)

useEffectDisposableOnce(fun () ->
createDisposable(fun () ->
cts.current.Cancel()
cts.current.Dispose()
)
)

token
98 changes: 22 additions & 76 deletions Feliz/Interop.fs
Original file line number Diff line number Diff line change
@@ -1,83 +1,29 @@
namespace Feliz
module [<RequireQualifiedAccess>] Feliz.Interop

open Fable.Core
open Fable.Core.JsInterop
open Fable.React
open Feliz.ReactApi

[<RequireQualifiedAccess>]
module DateParsing =
open System
let (|Between|_|) (x: int) (y: int) (input: int) =
if input >= x && input <= y
then Some()
else None
let isLeapYear (year: int) = DateTime.IsLeapYear(year)
let reactApi : IReactApi = importDefault "react"
#if FABLE_COMPILER_3 || FABLE_COMPILER_4
let inline reactElement (name: string) (props: 'a) : ReactElement = import "createElement" "react"
#else
let reactElement (name: string) (props: 'a) : ReactElement = import "createElement" "react"
#endif
let inline mkAttr (key: string) (value: obj) : IReactProperty = unbox (key, value)
[<Emit "undefined">]
let undefined : obj = jsNative
let inline mkStyle (key: string) (value: obj) : IStyleAttribute = unbox (key, value)
let inline svgAttribute (key: string) (value: obj) : ISvgAttribute = unbox (key, value)
let inline reactElementWithChild (name: string) (child: 'a) =
reactElement name (createObj [ "children" ==> ResizeArray [| child |] ])
let inline reactElementWithChildren (name: string) (children: #seq<ReactElement>) =
reactElement name (createObj [ "children" ==> reactApi.Children.toArray (Array.ofSeq children) ])
let inline createElement name (properties: IReactProperty list) : ReactElement =
reactElement name (createObj !!properties)
let inline createSvgElement name (properties: ISvgAttribute list) : ReactElement =
reactElement name (createObj !!properties)

let (|Int|_|) (input: string) =
try (Some (int input))
with _ -> None
/// <summary>Parses a specific yyyy-MM-dd or yyyy-MM-ddTHH:mm date format that comes out of an input element with type="date"</summary>
let parse (input: string) : Option<DateTime> =
try
if String.IsNullOrWhiteSpace input then None
else
let parts = input.Split('-')
let year, month, day, hour, minute =
match parts with
| [| Int year; Int month |] -> year, month, 1, 0, 0
| [| Int year; Int month; Int day |] -> year, month, day, 0, 0
| [| Int year; Int month; day |] ->
if day.Contains("T") then
match day.Split('T') with
| [| Int parsedDay; time |] ->
match time.Split ':' with
| [| Int hour; Int minute |] ->
match hour, minute with
| Between 0 59, Between 0 59 -> year, month, parsedDay, hour, minute
| _ ->
-1, 1, 1, 0, 0
| _ ->
-1, 1, 1, 0, 0
| _ ->
-1, 1, 1, 0, 0
else
-1, 1, 1, 0, 0
| _ ->
-1, 1, 1, 0, 0
if year <= 0 then
None
else
match month, day with
| 2, Between 1 29 when isLeapYear year -> Some (DateTime(year, month, day, hour, minute, 0))
| 2, Between 1 28 when not (isLeapYear year) -> Some (DateTime(year, month, day, hour, minute, 0))
| (1 | 3 | 5 | 7 | 8 | 10 | 12), Between 1 31 -> Some (DateTime(year, month, day, hour, minute, 0))
| (4 | 6 | 9 | 11), Between 1 30 -> Some (DateTime(year, month, day, hour, minute, 0))
| _ -> None
with
| _ -> None

[<RequireQualifiedAccess>]
module Interop =
let reactApi : IReactApi = importDefault "react"
#if FABLE_COMPILER_3 || FABLE_COMPILER_4
let inline reactElement (name: string) (props: 'a) : ReactElement = import "createElement" "react"
#else
let reactElement (name: string) (props: 'a) : ReactElement = import "createElement" "react"
#endif
let inline mkAttr (key: string) (value: obj) : IReactProperty = unbox (key, value)
[<Emit "undefined">]
let undefined : obj = jsNative
let inline mkStyle (key: string) (value: obj) : IStyleAttribute = unbox (key, value)
let inline svgAttribute (key: string) (value: obj) : ISvgAttribute = unbox (key, value)
let inline reactElementWithChild (name: string) (child: 'a) =
reactElement name (createObj [ "children" ==> ResizeArray [| child |] ])
let inline reactElementWithChildren (name: string) (children: #seq<ReactElement>) =
reactElement name (createObj [ "children" ==> reactApi.Children.toArray (Array.ofSeq children) ])
let inline createElement name (properties: IReactProperty list) : ReactElement =
reactElement name (createObj !!properties)
let inline createSvgElement name (properties: ISvgAttribute list) : ReactElement =
reactElement name (createObj !!properties)

[<Emit "typeof $0 === 'number'">]
let isTypeofNumber (x: obj) : bool = jsNative
[<Emit "typeof $0 === 'number'">]
let isTypeofNumber (x: obj) : bool = jsNative
Loading

0 comments on commit e2da7db

Please sign in to comment.