Skip to content

Commit

Permalink
feat(Feliz): refactor React API to improve the transpilation output
Browse files Browse the repository at this point in the history
- Move `Feliz.DateParsing`, `Feliz.Helpers`, `Feliz.ReactInterop` modules to separate files.
- Convert `Feliz.Internal` internal type into a module and move it to a separate file. Make the module public to allow its definitions being called from the public `React` type's inline members.
- Adjust `useEffect` method definition in `Feliz.ReactApi.IReactApi` interface.
- Change `ReactInterop` binding definitions to the attribute-based to ensure the correct import statements generated by Fable (otherwise, the path to "ReactInterop.js" will be relative to the "Feliz" package root and not to the root of the referencing site).
- Make `Feliz.Helpers.optDispose` function not inline - to reduce the amount of Feliz transpiled code.
- Make all the members of `Feliz.React` type inline, and for those that contain some custom logic (beyond the trivial bindings) move the logic into the separate functions of the `Feliz.Internal` module. This way, the `React` type won't be present in the transpiled code.
- Switch `Feliz.ReactInterop`  module access modifier from `internal` to `public` to allow its definitions being called from the public `React` type's inline members.
  • Loading branch information
ArtemyB committed Dec 24, 2023
1 parent 4ef46b8 commit b0ac12a
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 b0ac12a

Please sign in to comment.