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