From b0ac12aa232a37d6ff672af166620cf1d4a7ca49 Mon Sep 17 00:00:00 2001 From: ArtemyB Date: Sun, 24 Dec 2023 16:08:10 +0300 Subject: [PATCH] feat(Feliz): refactor React API to improve the transpilation output - 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. --- Feliz/DateParsing.fs | 52 +++++++ Feliz/Feliz.fsproj | 16 ++- Feliz/Helpers.fs | 11 ++ Feliz/Internal.fs | 176 +++++++++++++++++++++++ Feliz/Interop.fs | 98 +++---------- Feliz/React.fs | 316 ++++++++++-------------------------------- Feliz/ReactInterop.fs | 20 +++ Feliz/ReactTypes.fs | 5 +- 8 files changed, 374 insertions(+), 320 deletions(-) create mode 100644 Feliz/DateParsing.fs create mode 100644 Feliz/Helpers.fs create mode 100644 Feliz/Internal.fs create mode 100644 Feliz/ReactInterop.fs diff --git a/Feliz/DateParsing.fs b/Feliz/DateParsing.fs new file mode 100644 index 00000000..2c1bb310 --- /dev/null +++ b/Feliz/DateParsing.fs @@ -0,0 +1,52 @@ +module [] 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 +/// Parses a specific yyyy-MM-dd or yyyy-MM-ddTHH:mm date format that comes out of an input element with type="date" +let parse (input: string) : Option = + 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 diff --git a/Feliz/Feliz.fsproj b/Feliz/Feliz.fsproj index 90acb0f5..02ecbcdb 100644 --- a/Feliz/Feliz.fsproj +++ b/Feliz/Feliz.fsproj @@ -4,16 +4,18 @@ A fresh retake of the React API in Fable, optimized for happiness fsharp;fable;react;html Zaid Ajaj - 2.7.0 + 2.8.0-beta.0 netstandard2.0 - Implement overflow anchor + Optimize React hooks and APIs Feliz API. + - + + @@ -29,12 +31,20 @@ + + + + + + + + diff --git a/Feliz/Helpers.fs b/Feliz/Helpers.fs new file mode 100644 index 00000000..905cddf7 --- /dev/null +++ b/Feliz/Helpers.fs @@ -0,0 +1,11 @@ +namespace Feliz + +open System +open System.ComponentModel +open Fable.Core + +[] +[] +module Helpers = + let optDispose (disposeOption: #IDisposable option) = + { new IDisposable with member _.Dispose () = disposeOption |> Option.iter (fun d -> d.Dispose()) } diff --git a/Feliz/Internal.fs b/Feliz/Internal.fs new file mode 100644 index 00000000..3194d280 --- /dev/null +++ b/Feliz/Internal.fs @@ -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 [] 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() } + + [] + 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 + + + [] + let useEffectOnce(effect: unit -> unit) = + let calledOnce = Interop.reactApi.useRefInternal false + + useEffectWithDeps (fun () -> + if not calledOnce.current then + calledOnce.current <- true + effect() + ) [||] + + [] + 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 + ) [||] + + [] + 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) + + [] + 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) + + [] + 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 diff --git a/Feliz/Interop.fs b/Feliz/Interop.fs index af2d87a6..c2207ade 100644 --- a/Feliz/Interop.fs +++ b/Feliz/Interop.fs @@ -1,83 +1,29 @@ -namespace Feliz +module [] Feliz.Interop open Fable.Core open Fable.Core.JsInterop open Fable.React open Feliz.ReactApi -[] -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) +[] +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 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 - /// Parses a specific yyyy-MM-dd or yyyy-MM-ddTHH:mm date format that comes out of an input element with type="date" - let parse (input: string) : Option = - 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 - -[] -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) - [] - 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 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 isTypeofNumber (x: obj) : bool = jsNative +[] +let isTypeofNumber (x: obj) : bool = jsNative diff --git a/Feliz/React.fs b/Feliz/React.fs index 5c27edcd..25fed74d 100644 --- a/Feliz/React.fs +++ b/Feliz/React.fs @@ -1,65 +1,18 @@ namespace Feliz open System -open System.ComponentModel open Fable.Core open Fable.Core.JsInterop open Browser.Types -module internal ReactInterop = - let useDebugValueWithFormatter<'t>(value: 't, formatter: 't -> string) : unit = import "useDebugValue" "./ReactInterop.js" - let useEffect (effect: obj) : unit = import "useEffect" "./ReactInterop.js" - let useEffectWithDeps (effect: obj) (deps: obj) : unit = import "useEffectWithDeps" "./ReactInterop.js" - let useLayoutEffect (effect: obj) : unit = import "useLayoutEffect" "./ReactInterop.js" - let useLayoutEffectWithDeps (effect: obj) (deps: obj) : unit = import "useLayoutEffectWithDeps" "./ReactInterop.js" - -[] -[] -module Helpers = - let inline optDispose (disposeOption: #IDisposable option) = - { new IDisposable with member _.Dispose () = disposeOption |> Option.iter (fun d -> d.Dispose()) } - -type internal Internal() = - static let propsWithKey (withKey: ('props -> string) option) props = - match withKey with - | Some f -> - props?key <- f props - props - | None -> props - static member - functionComponent - ( - renderElement: 'props -> ReactElement, - ?name: string, - ?withKey: 'props -> string - ) - : '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 [] 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) - static member - memo - ( - renderElement: 'props -> ReactElement, - ?name: string, - ?areEqual: 'props -> 'props -> bool, - ?withKey: 'props -> string - ) - : '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) - -type React = + +type [] React = /// Creates a disposable instance by providing the implementation of the dispose member. - static member createDisposable(dispose: unit -> unit) = - { new IDisposable with member _.Dispose() = dispose() } + static member inline createDisposable(dispose: unit -> unit) = + Internal.createDisposable dispose + + static member inline useDisposable (dispose: unit -> unit) = + Internal.useDisposable dispose /// The `React.fragment` component lets you return multiple elements in your `render()` method without creating an additional DOM element. static member inline fragment xs = @@ -81,36 +34,30 @@ type React = static member inline imported() = Html.none /// The `useState` hook that creates a state variable for React function components from an initialization function. - [] - static member useState<'t>(initializer: unit -> 't) = Interop.reactApi.useState 't,'t>(initializer) + static member inline useState<'t>(initializer: unit -> 't) = Interop.reactApi.useState 't,'t>(initializer) /// Accepts a reducer and returns the current state paired with a dispatch. - [] - static member useReducer(update, initialState) = Interop.reactApi.useReducer update initialState + static member inline useReducer(update, initialState) = Interop.reactApi.useReducer update initialState /// The `useEffect` hook that creates a disposable effect for React function components. /// This effect has no dependencies which means the effect is re-executed on every re-render. /// To make the effect run once (for example you subscribe once to web sockets) then provide an empty array /// for the dependencies: `React.useEffect(disposableEffect, [| |])`. - [] - static member useEffect(effect: unit -> #IDisposable) : unit = ReactInterop.useEffect(effect) + static member inline useEffect(effect: unit -> #IDisposable) : unit = ReactInterop.useEffect(effect) /// The `useEffect` hook that creates a disposable effect for React function components. /// This effect has no dependencies which means the effect is re-executed on every re-render. /// To make the effect run once (for example you subscribe once to web sockets) then provide an empty array /// for the dependencies: `React.useEffect(disposableEffect, [| |])`. - [] static member inline useEffect(effect: unit -> #IDisposable option) = React.useEffect(effect >> Helpers.optDispose) /// The `useEffect` hook that creates a disposable effect for React function components. /// This effect takes an array of *dependencies*. /// Whenever any of these dependencies change, the effect is re-executed. To execute the effect only once, /// you have to explicitly provide an empty array for the dependencies: `React.useEffect(effect, [| |])`. - [] - static member useEffect(effect: unit -> #IDisposable, dependencies: obj []) : unit = ReactInterop.useEffectWithDeps effect dependencies + static member inline useEffect(effect: unit -> #IDisposable, dependencies: obj []) : unit = ReactInterop.useEffectWithDeps effect dependencies /// The `useEffect` hook that creates a disposable effect for React function components. /// This effect takes an array of *dependencies*. /// Whenever any of these dependencies change, the effect is re-executed. To execute the effect only once, /// you have to explicitly provide an empty array for the dependencies: `React.useEffect(effect, [| |])`. - [] static member inline useEffect(effect: unit -> #IDisposable option, dependencies: obj []) = React.useEffect(effect >> Helpers.optDispose, dependencies) /// The `useLayoutEffect` hook that creates a disposable effect for React function components. @@ -118,152 +65,90 @@ type React = /// To make the effect run once (for example you subscribe once to web sockets) then provide an empty array /// for the dependencies: `React.useLayoutEffect(disposableEffect, [| |])`. /// The signature is identical to useEffect, but it fires synchronously after all DOM mutations. Use this to read layout from the DOM and synchronously re-render. Updates scheduled inside useLayoutEffect will be flushed synchronously, before the browser has a chance to paint. - [] - static member useLayoutEffect(effect: unit -> #IDisposable) : unit = ReactInterop.useLayoutEffect(effect) + static member inline useLayoutEffect(effect: unit -> #IDisposable) : unit = ReactInterop.useLayoutEffect(effect) + /// The `useLayoutEffect` hook that creates a disposable effect for React function components. /// This effect has no dependencies which means the effect is re-executed on every re-render. /// To make the effect run once (for example you subscribe once to web sockets) then provide an empty array /// for the dependencies: `React.useLayoutEffect(disposableEffect, [| |])`. /// The signature is identical to useEffect, but it fires synchronously after all DOM mutations. Use this to read layout from the DOM and synchronously re-render. Updates scheduled inside useLayoutEffect will be flushed synchronously, before the browser has a chance to paint. - [] static member inline useLayoutEffect(effect: unit -> #IDisposable option) = React.useLayoutEffect(effect >> Helpers.optDispose) + /// The `useLayoutEffect` hook that creates a disposable effect for React function components. /// This effect takes an array of *dependencies*. /// Whenever any of these dependencies change, the effect is re-executed. To execute the effect only once, /// you have to explicitly provide an empty array for the dependencies: `React.useLayoutEffect(effect, [| |])`. /// The signature is identical to useEffect, but it fires synchronously after all DOM mutations. Use this to read layout from the DOM and synchronously re-render. Updates scheduled inside useLayoutEffect will be flushed synchronously, before the browser has a chance to paint. - [] - static member useLayoutEffect(effect: unit -> #IDisposable, dependencies: obj []) : unit = ReactInterop.useLayoutEffectWithDeps effect dependencies + static member inline useLayoutEffect(effect: unit -> #IDisposable, dependencies: obj []) : unit = ReactInterop.useLayoutEffectWithDeps effect dependencies + /// The `useLayoutEffect` hook that creates a disposable effect for React function components. /// This effect takes an array of *dependencies*. /// Whenever any of these dependencies change, the effect is re-executed. To execute the effect only once, /// you have to explicitly provide an empty array for the dependencies: `React.useLayoutEffect(effect, [| |])`. /// The signature is identical to useEffect, but it fires synchronously after all DOM mutations. Use this to read layout from the DOM and synchronously re-render. Updates scheduled inside useLayoutEffect will be flushed synchronously, before the browser has a chance to paint. - [] static member inline useLayoutEffect(effect: unit -> #IDisposable option, dependencies: obj []) = React.useLayoutEffect(effect >> Helpers.optDispose, dependencies) /// The signature is identical to useEffect, but it fires synchronously after all DOM mutations. Use this to read layout from the DOM and synchronously re-render. Updates scheduled inside useLayoutEffect will be flushed synchronously, before the browser has a chance to paint. /// This effect is executed on every (re)render - [] - static member useLayoutEffect(effect: unit -> unit) = + static member inline useLayoutEffect(effect: unit -> unit) = ReactInterop.useLayoutEffect (fun _ -> effect() React.createDisposable(ignore)) /// The signature is identical to useEffect, but it fires synchronously after all DOM mutations. Use this to read layout from the DOM and synchronously re-render. Updates scheduled inside useLayoutEffect will be flushed synchronously, before the browser has a chance to paint. - [] - static member useLayoutEffect(effect: unit -> unit, dependencies: obj []) = + static member inline useLayoutEffect(effect: unit -> unit, dependencies: obj []) = ReactInterop.useLayoutEffectWithDeps (fun _ -> effect() React.createDisposable(ignore)) dependencies - [] static member inline useLayoutEffectOnce(effect: unit -> unit) = React.useLayoutEffect(effect, [| |]) - [] static member inline useLayoutEffectOnce(effect: unit -> #IDisposable) = React.useLayoutEffect(effect, [| |]) - [] static member inline useLayoutEffectOnce(effect: unit -> #IDisposable option) = React.useLayoutEffect(effect, [| |]) /// React hook to define and use an effect only once when a function component renders for the first time. /// This is an alias for `React.useEffect(effect, [| |])` which explicitly provides an empty array for the dependencies of the effect which means the effect will only run once. - [] - static member useEffectOnce(effect: unit -> unit) = - let calledOnce = Interop.reactApi.useRefInternal false - - React.useEffect (fun () -> - if calledOnce.current - then () - else - calledOnce.current <- true - effect() - , [||]) + static member inline useEffectOnce(effect: unit -> unit) = + Internal.useEffectOnce effect /// React hook to define and use a disposable effect only once when a function component renders for the first time. /// This is an alias for `React.useEffect(effect, [| |])` which explicitly provides an empty array for the dependencies of the effect which means the effect will only run once. - [] - static member useEffectOnce(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 - - React.useEffect (fun () -> - if calledOnce.current - then None - else - calledOnce.current <- true - destroyFunc.current <- effect() |> Some - - if renderAfterCalled.current - then destroyFunc.current - else None - , [||]) + static member inline useEffectOnce(effect: unit -> #IDisposable) = + Internal.useEffectDisposableOnce effect /// React hook to define and use a disposable effect only once when a function component renders for the first time. /// This is an alias for `React.useEffect(effect, [| |])` which explicitly provide an empty array for the dependencies of the effect which means the effect will only run once. - [] - static member useEffectOnce(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 - - React.useEffect (fun () -> - if calledOnce.current - then None - else - calledOnce.current <- true - destroyFunc.current <- effect() - - if renderAfterCalled.current - then destroyFunc.current - else None - , [||]) + static member inline useEffectOnce(effect: unit -> #IDisposable option) = + Internal.useEffectDisposableOptOnce effect /// The `useEffect` hook that creates an effect for React function components. /// This effect is executed *every time* the function component re-renders. /// /// To make the effect run only once, write: `React.useEffect(effect, [| |])` which explicitly states /// that this effect has no dependencies and should only run once on initial render. - [] - static member useEffect(effect: unit -> unit) : unit = - ReactInterop.useEffect - (fun _ -> - effect() - React.createDisposable(ignore)) + static member inline useEffect(effect: unit -> unit) : unit = + Internal.useEffect effect /// The `useEffect` hook that creates an effect for React function components. This effect takes an array of *dependencies*. /// Whenever any of these dependencies change, the effect is re-executed. To execute the effect only once, /// you have to explicitly provide an empty array for the dependencies: `React.useEffect(effect, [| |])`. - [] - static member useEffect(effect: unit -> unit, dependencies: obj []) : unit = - ReactInterop.useEffectWithDeps - (fun _ -> - effect() - React.createDisposable(ignore)) - dependencies + static member inline useEffect (effect: unit -> unit, dependencies: obj []) : unit = + Internal.useEffectWithDeps effect dependencies /// Can be used to display a label for custom hooks in React DevTools. - [] - static member useDebugValue(value: string) = + static member inline useDebugValue(value: string) = ReactInterop.useDebugValueWithFormatter(value, id) /// Can be used to display a label for custom hooks in React DevTools. - [] - static member useDebugValue(value: 't, formatter: 't -> string) = + static member inline useDebugValue(value: 't, formatter: 't -> string) = ReactInterop.useDebugValueWithFormatter(value, formatter) /// @@ -273,31 +158,26 @@ type React = /// A callback function to be memoized. /// An array of dependencies upon which the callback function depends. /// If not provided, defaults to empty array, representing dependencies that never change. - [] - static member useCallback(callbackFunction: 'a -> 'b, ?dependencies: obj array) = + static member inline useCallback(callbackFunction: 'a -> 'b, ?dependencies: obj array) = Interop.reactApi.useCallback callbackFunction (defaultArg dependencies [||]) /// Returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component. /// /// Essentially, useRef is like a container that can hold a mutable value in its .current property. - [] - static member useRef<'t>(initialValue: 't) = Interop.reactApi.useRefInternal(initialValue) + static member inline useRef<'t>(initialValue: 't) = Interop.reactApi.useRefInternal(initialValue) /// A specialized version of React.useRef() that creates a reference to an input element. /// /// Useful for controlling the internal properties and methods of that element, for example to enable focus(). - [] - static member useInputRef() : IRefValue = React.useRef(None) + static member inline useInputRef() : IRefValue = React.useRef(None) /// A specialized version of React.useRef() that creates a reference to a button element. - [] - static member useButtonRef() : Fable.React.IRefValue = React.useRef(None) + static member inline useButtonRef() : Fable.React.IRefValue = React.useRef(None) /// A specialized version of React.useRef() that creates a reference to a generic HTML element. /// /// Useful for controlling the internal properties and methods of that element, for integration with third-party libraries that require a Html element. - [] - static member useElementRef() : IRefValue = React.useRef(None) + static member inline useElementRef() : IRefValue = React.useRef(None) /// /// The `useMemo` hook. Returns a memoized value. Pass a "create" function and an array of dependencies. @@ -306,8 +186,7 @@ type React = /// A create function returning a value to be memoized. /// An array of dependencies upon which the create function depends. /// If not provided, defaults to empty array, representing dependencies that never change. - [] - static member useMemo(createFunction: unit -> 'a, ?dependencies: obj array) = + static member inline useMemo(createFunction: unit -> 'a, ?dependencies: obj array) = Interop.reactApi.useMemo createFunction (defaultArg dependencies [||]) // @@ -320,8 +199,8 @@ type React = /// /// A render function that returns an element. /// A function to derive a component key from the props. - static member functionComponent(render: 'props -> ReactElement, ?withKey: 'props -> string) = - Internal.functionComponent(render, ?withKey=withKey) + static member inline functionComponent(render: 'props -> ReactElement, ?withKey: 'props -> string) = + Internal.functionComponent render None withKey /// /// Creates a React function component from a function that accepts a "props" object and renders a result. @@ -331,8 +210,8 @@ type React = /// A render function that returns an element. /// A function to derive a component key from the props. [] attribute to automatically convert them to React components">] - static member functionComponent(name: string, render: 'props -> ReactElement, ?withKey: 'props -> string) = - Internal.functionComponent(render, name, ?withKey=withKey) + static member inline functionComponent(name: string, render: 'props -> ReactElement, ?withKey: 'props -> string) = + Internal.functionComponent render (Some name) withKey /// /// Creates a React function component from a function that accepts a "props" object and renders a result. @@ -341,8 +220,8 @@ type React = /// A render function that returns a list of elements. /// A function to derive a component key from the props. [] attribute to automatically convert them to React components">] - static member functionComponent(render: 'props -> #seq, ?withKey: 'props -> string) = - Internal.functionComponent(render >> React.fragment, ?withKey=withKey) + static member inline functionComponent(render: 'props -> #seq, ?withKey: 'props -> string) = + Internal.functionComponent (render >> React.fragment) None withKey /// /// Creates a React function component from a function that accepts a "props" object and renders a result. @@ -352,8 +231,8 @@ type React = /// The component name to display in the React dev tools. /// A function to derive a component key from the props. [] attribute to automatically convert them to React components">] - static member functionComponent(name: string, render: 'props -> #seq, ?withKey: 'props -> string) = - Internal.functionComponent(render >> React.fragment, name, ?withKey=withKey) + static member inline functionComponent(name: string, render: 'props -> #seq, ?withKey: 'props -> string) = + Internal.functionComponent (render >> React.fragment) (Some name) withKey // // React.memo @@ -367,8 +246,8 @@ type React = /// A render function or a React.functionComponent. /// A custom comparison function to use instead of React's default shallow compare. /// A function to derive a component key from the props. - static member memo(render: 'props -> ReactElement, ?withKey: 'props -> string, ?areEqual: 'props -> 'props -> bool) = - Internal.memo(render, ?areEqual=areEqual, ?withKey=withKey) + static member inline memo(render: 'props -> ReactElement, ?withKey: 'props -> string, ?areEqual: 'props -> 'props -> bool) = + Internal.memo render None areEqual withKey /// /// `React.memo` memoizes the result of a function component. Given the same props, React will skip rendering the component, and reuse the last rendered result. @@ -379,8 +258,8 @@ type React = /// A render function or a React.functionComponent. /// A custom comparison function to use instead of React's default shallow compare. /// A function to derive a component key from the props. - static member memo(name: string, render: 'props -> ReactElement, ?withKey: 'props -> string, ?areEqual: 'props -> 'props -> bool) = - Internal.memo(render, name, ?areEqual=areEqual, ?withKey=withKey) + static member inline memo(name: string, render: 'props -> ReactElement, ?withKey: 'props -> string, ?areEqual: 'props -> 'props -> bool) = + Internal.memo render (Some name) areEqual withKey /// /// `React.memo` memoizes the result of a function component. Given the same props, React will skip rendering the component, and reuse the last rendered result. @@ -390,8 +269,8 @@ type React = /// A render function that returns a list of elements. /// A function to derive a component key from the props. /// A custom comparison function to use instead of React's default shallow compare. - static member memo(render: 'props -> #seq, ?withKey: 'props -> string, ?areEqual: 'props -> 'props -> bool) = - Internal.memo(render >> React.fragment, ?areEqual=areEqual, ?withKey=withKey) + static member inline memo(render: 'props -> #seq, ?withKey: 'props -> string, ?areEqual: 'props -> 'props -> bool) = + Internal.memo (render >> React.fragment) None areEqual withKey /// /// `React.memo` memoizes the result of a function component. Given the same props, React will skip rendering the component, and reuse the last rendered result. @@ -402,8 +281,8 @@ type React = /// A render function that returns a list of elements. /// A function to derive a component key from the props. /// A custom comparison function to use instead of React's default shallow compare. - static member memo(name: string, render: 'props -> #seq, ?withKey: 'props -> string, ?areEqual: 'props -> 'props -> bool) = - Internal.memo(render >> React.fragment, name, ?areEqual=areEqual, ?withKey=withKey) + static member inline memo(name: string, render: 'props -> #seq, ?withKey: 'props -> string, ?areEqual: 'props -> 'props -> bool) = + Internal.memo (render >> React.fragment) (Some name) areEqual withKey // // React.useContext @@ -415,10 +294,8 @@ type React = /// /// The component name to display in the React dev tools. /// A default value that is only used when a component does not have a matching Provider above it in the tree. - static member createContext<'a>(?name: string, ?defaultValue: 'a) = - let contextObject = Interop.reactApi.createContext (defaultArg defaultValue Fable.Core.JS.undefined<'a>) - name |> Option.iter (fun name -> contextObject?displayName <- name) - contextObject + static member inline createContext<'a>(?name: string, ?defaultValue: 'a) = + Internal.createContext<'a> name defaultValue /// /// A Provider component that allows consuming components to subscribe to context changes. @@ -426,7 +303,7 @@ type React = /// A context object returned from a previous React.createContext call. /// The context value to be provided to descendant components. /// A child element. - static member contextProvider(contextObject: Fable.React.IContext<'a>, contextValue: 'a, child: ReactElement) : ReactElement = + static member inline contextProvider(contextObject: Fable.React.IContext<'a>, contextValue: 'a, child: ReactElement) : ReactElement = Interop.reactApi.createElement(contextObject?Provider, createObj ["value" ==> contextValue], [child]) /// /// A Provider component that allows consuming components to subscribe to context changes. @@ -434,7 +311,7 @@ type React = /// A context object returned from a previous React.createContext call. /// The context value to be provided to descendant components. /// A sequence of child elements. - static member contextProvider(contextObject: Fable.React.IContext<'a>, contextValue: 'a, children: #seq) : ReactElement = + static member inline contextProvider(contextObject: Fable.React.IContext<'a>, contextValue: 'a, children: #seq) : ReactElement = Interop.reactApi.createElement(contextObject?Provider, createObj ["value" ==> contextValue], children) /// @@ -442,14 +319,14 @@ type React = /// /// A context object returned from a previous React.createContext call. /// A render function that returns an element. - static member contextConsumer(contextObject: Fable.React.IContext<'a>, render: 'a -> ReactElement) : ReactElement = + static member inline contextConsumer(contextObject: Fable.React.IContext<'a>, render: 'a -> ReactElement) : ReactElement = Interop.reactApi.createElement(contextObject?Consumer, null, [!!render]) /// /// A Consumer component that subscribes to context changes. /// /// A context object returned from a previous React.createContext call. /// A render function that returns a sequence of elements. - static member contextConsumer(contextObject: Fable.React.IContext<'a>, render: 'a -> #seq) : ReactElement = + static member inline contextConsumer(contextObject: Fable.React.IContext<'a>, render: 'a -> #seq) : ReactElement = Interop.reactApi.createElement(contextObject?Consumer, null, [!!(render >> React.fragment)]) /// @@ -457,8 +334,7 @@ type React = /// The current context value is determined by the value prop of the nearest Provider component above the calling component in the tree. /// /// A context object returned from a previous React.createContext call. - [] - static member useContext(contextObject: Fable.React.IContext<'a>) = Interop.reactApi.useContext contextObject + static member inline useContext(contextObject: Fable.React.IContext<'a>) = Interop.reactApi.useContext contextObject /// /// Creates a callback that keeps the same reference during the entire lifecycle of the component while having access to @@ -470,21 +346,8 @@ type React = /// dependency declarations and never causes a re-render. /// /// The function call. - [] - static member useCallbackRef(callback: ('a -> 'b)) = - let lastRenderCallbackRef = React.useRef(callback) - - let callbackRef = - React.useCallback((fun (arg: 'a) -> - lastRenderCallbackRef.current(arg) - ), [||]) - - React.useLayoutEffect(fun () -> - // render is commited - it's safe to update the callback - lastRenderCallbackRef.current <- callback - ) - - callbackRef + static member inline useCallbackRef(callback: ('a -> 'b)) = + Internal.useCallbackRef callback /// /// Just like React.useState except that the updater function uses the previous state of the state variable as input and allows you to compute the next value using it. @@ -492,31 +355,22 @@ type React = /// /// Use this instead of React.useState when your state variable is a list, an array, a dictionary, a map or other complex structures. /// - static member useStateWithUpdater (initial: 't) : ('t * (('t -> 't) -> unit)) = import "useState" "react" + static member inline useStateWithUpdater (initial: 't) : ('t * (('t -> 't) -> unit)) = import "useState" "react" /// /// Forwards a given ref, allowing you to pass it further down to a child. /// /// A render function that returns an element. - static member 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) + static member inline forwardRef(render: ('props * IRefValue<'t> -> ReactElement)) : ('props * IRefValue<'t> -> ReactElement) = + Internal.forwardRef render /// /// Forwards a given ref, allowing you to pass it further down to a child. /// /// The component name to display in the React dev tools. /// A render function that returns an element. - static member forwardRef(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) + static member inline forwardRef(name: string, render: ('props * IRefValue<'t> -> ReactElement)) : ('props * IRefValue<'t> -> ReactElement) = + Internal.forwardRefWithName name render /// /// Highlights potential problems in an application by enabling additional checks @@ -527,7 +381,7 @@ type React = /// /// The elements that will be rendered with additional /// checks and warnings. - static member strictMode(children: ReactElement list) = + static member inline strictMode(children: ReactElement list) = Interop.reactApi.createElement(Interop.reactApi.StrictMode, None, children) /// @@ -541,7 +395,7 @@ type React = /// Where you would then pass in `asyncComponent`. /// /// The props to be passed to the component. - static member lazy'<'t,'props>(dynamicImport: JS.Promise<'t>, props: 'props) = + static member inline lazy'<'t,'props>(dynamicImport: JS.Promise<'t>, props: 'props) = Interop.reactApi.createElement(Interop.reactApi.lazy'(fun () -> dynamicImport),props) /// /// Lets you define a component that is loaded dynamically. Which helps with code @@ -555,7 +409,7 @@ type React = /// Where you would then pass in `fun () -> asyncComponent`. /// /// The props to be passed to the component. - static member lazy'<'t,'props>(dynamicImport: unit -> JS.Promise<'t>, props: 'props) = + static member inline lazy'<'t,'props>(dynamicImport: unit -> JS.Promise<'t>, props: 'props) = Interop.reactApi.createElement(Interop.reactApi.lazy'(dynamicImport),props) /// @@ -565,7 +419,7 @@ type React = /// Currently this is only usable with `React.lazy'`. /// /// The elements that will be rendered within the suspense block. - static member suspense(children: ReactElement list) = + static member inline suspense(children: ReactElement list) = Interop.reactApi.createElement(Interop.reactApi.Suspense, {| fallback = Html.none |} |> JsInterop.toPlainJsObj, children) /// /// Lets you specify a loading indicator whenever a child element is not yet ready @@ -575,7 +429,7 @@ type React = /// /// The elements that will be rendered within the suspense block. /// The element that will be rendered while the children are loading. - static member suspense(children: ReactElement list, fallback: ReactElement) = + static member inline suspense(children: ReactElement list, fallback: ReactElement) = Interop.reactApi.createElement(Interop.reactApi.Suspense, {| fallback = fallback |} |> JsInterop.toPlainJsObj, children) /// @@ -584,8 +438,7 @@ type React = /// /// The ref you want to override. /// A function that returns a new ref with changed behavior. - [] - static member useImperativeHandle(ref: IRefValue<'t>, createHandle: unit -> 't) = + static member inline useImperativeHandle(ref: IRefValue<'t>, createHandle: unit -> 't) = Interop.reactApi.useImperativeHandleNoDeps ref createHandle /// @@ -597,28 +450,15 @@ type React = /// The ref you want to override. /// A function that returns a new ref with changed behavior. /// An array of dependencies upon which the imperative handle function depends. - [] - static member useImperativeHandle(ref: IRefValue<'t>, createHandle: unit -> 't, dependencies: obj []) = + static member inline useImperativeHandle(ref: IRefValue<'t>, createHandle: unit -> 't, dependencies: obj []) = Interop.reactApi.useImperativeHandle ref createHandle dependencies /// /// Creates a CancellationToken that is cancelled when a component is unmounted. /// - [] - static member inline useCancellationToken () = - let cts = React.useRef(new System.Threading.CancellationTokenSource()) - let token = React.useRef(cts.current.Token) - - React.useEffectOnce(fun () -> - React.createDisposable(fun () -> - cts.current.Cancel() - cts.current.Dispose() - ) - ) - - token + static member inline useCancellationToken () = Internal.useCancellationToken () -[] +[] module ReactOverloadMagic = type React with /// Creates a disposable instance by merging multiple IDisposables. @@ -648,9 +488,7 @@ module ReactOverloadMagic = ) /// The `useState` hook that creates a state variable for React function components. - [] - static member useState<'t>(initial: 't) = Interop.reactApi.useState<'t,'t>(initial) + static member inline useState<'t>(initial: 't) = Interop.reactApi.useState<'t,'t>(initial) - [] - static member useStateWithUpdater<'t>(initializer: unit -> 't): ('t * (('t -> 't) -> unit)) = import "useState" "react" + static member inline useStateWithUpdater<'t>(initializer: unit -> 't): ('t * (('t -> 't) -> unit)) = import "useState" "react" diff --git a/Feliz/ReactInterop.fs b/Feliz/ReactInterop.fs new file mode 100644 index 00000000..30861c82 --- /dev/null +++ b/Feliz/ReactInterop.fs @@ -0,0 +1,20 @@ +namespace Feliz + +open Fable.Core + +module ReactInterop = + [] + let useDebugValueWithFormatter<'t>(value: 't, formatter: 't -> string) : unit = jsNative + + [] + let useEffect (effect: obj) : unit = jsNative + + [] + let useEffectWithDeps (effect: obj) (deps: obj) : unit = jsNative + + [] + let useLayoutEffect (effect: obj) : unit = jsNative + + [] + let useLayoutEffectWithDeps (effect: obj) (deps: obj) : unit = jsNative + diff --git a/Feliz/ReactTypes.fs b/Feliz/ReactTypes.fs index 733d1e42..ac349e40 100644 --- a/Feliz/ReactTypes.fs +++ b/Feliz/ReactTypes.fs @@ -22,9 +22,10 @@ type IReactApi = abstract Suspense: obj abstract useCallback: callbackFunction: ('a -> 'b) -> dependencies: obj array -> ('a -> 'b) abstract useContext: ctx: IContext<'a> -> 'a - abstract useEffect: obj * 't array -> unit + abstract useEffect: obj * dependencies: obj array -> unit abstract useEffect: obj -> unit - abstract useEffect: (unit -> unit) -> unit + abstract useEffect: effect: (unit -> unit) -> unit + abstract useEffect: effect: (unit -> unit) * dependencies: obj [] -> unit abstract useImperativeHandle<'t> : ref: Fable.React.IRefValue<'t> -> createHandle: (unit -> 't) -> dependencies: obj array -> unit [] abstract useImperativeHandleNoDeps<'t> : ref: Fable.React.IRefValue<'t> -> createHandle: (unit -> 't) -> unit