├── .docker └── Dockerfile ├── .gitignore ├── .gitpod.yml ├── .paket ├── Paket.Restore.targets └── paket.exe ├── Directory.Build.props ├── Fable.React.sln ├── LICENSE ├── README.md ├── SSRSample ├── .paket │ ├── Paket.Restore.targets │ └── paket.exe ├── benchmark │ ├── dotnet.fs │ ├── dotnet.fsproj │ ├── node.js │ └── paket.references ├── build.cmd ├── build.fsx ├── build.sh ├── paket.dependencies ├── paket.lock ├── src │ ├── Client │ │ ├── Bench.fs │ │ ├── Client.fs │ │ ├── Client.fsproj │ │ ├── index.css │ │ ├── index.html │ │ └── paket.references │ ├── Server │ │ ├── Server.fs │ │ ├── Server.fsproj │ │ └── paket.references │ └── Shared │ │ ├── Shared.fsproj │ │ ├── Types.fs │ │ ├── View.fs │ │ ├── jsComp.js │ │ └── paket.references └── webpack.config.js ├── Settings.FSharpLint ├── build.fsx ├── docs ├── react-error-boundaries.md ├── server-side-rendering.md └── using-third-party-react-components.md ├── fable_logo.png ├── global.json ├── package-lock.json ├── package.json └── src ├── Fable.React.Types ├── Fable.React.Extensions.fs ├── Fable.React.Hooks.fs ├── Fable.React.Types.fsproj ├── Fable.React.fs └── RELEASE_NOTES.md ├── Fable.React ├── Fable.React.FunctionComponent.fs ├── Fable.React.Helpers.fs ├── Fable.React.Isomorphic.fs ├── Fable.React.Props.fs ├── Fable.React.ReactiveComponents.fs ├── Fable.React.Standard.fs ├── Fable.React.fsproj ├── Fable.ReactServer.fs └── RELEASE_NOTES.md └── Fable.ReactDom.Types ├── Fable.ReactDom.Types.fsproj ├── Fable.ReactDom.fs └── RELEASE_NOTES.md /.docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gitpod/workspace-full:latest 2 | 3 | USER root 4 | 5 | RUN wget -q https://packages.microsoft.com/config/ubuntu/18.04/packages-microsoft-prod.deb && \ 6 | dpkg -i packages-microsoft-prod.deb && rm -rf packages-microsoft-prod.deb && \ 7 | add-apt-repository universe && \ 8 | apt-get update && apt-get -y -o APT::Install-Suggests="true" install dotnet-sdk-3.1 && \ 9 | apt -y clean; 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm/ 2 | .vscode/ 3 | .DS_Store 4 | .ionide/ 5 | 6 | ## Ignore Visual Studio temporary files, build results, and 7 | ## files generated by popular Visual Studio add-ons. 8 | 9 | # User-specific files 10 | *.suo 11 | *.user 12 | *.userosscache 13 | *.sln.docstates 14 | 15 | # User-specific files (MonoDevelop/Xamarin Studio) 16 | *.userprefs 17 | 18 | # Build results 19 | [Dd]ebug/ 20 | [Dd]ebugPublic/ 21 | [Rr]elease/ 22 | [Rr]eleases/ 23 | x64/ 24 | x86/ 25 | bld/ 26 | [Bb]in/ 27 | [Oo]bj/ 28 | [Ll]og/ 29 | 30 | # Visual Studio 2015 cache/options directory 31 | .vs/ 32 | # Uncomment if you have tasks that create the project's static files in wwwroot 33 | #wwwroot/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # DNX 49 | project.lock.json 50 | artifacts/ 51 | 52 | *_i.c 53 | *_p.c 54 | *_i.h 55 | *.ilk 56 | *.meta 57 | *.obj 58 | *.pch 59 | *.pdb 60 | *.pgc 61 | *.pgd 62 | *.rsp 63 | *.sbr 64 | *.tlb 65 | *.tli 66 | *.tlh 67 | *.tmp 68 | *.tmp_proj 69 | *.log 70 | *.vspscc 71 | *.vssscc 72 | .builds 73 | *.pidb 74 | *.svclog 75 | *.scc 76 | 77 | # Chutzpah Test files 78 | _Chutzpah* 79 | 80 | # Visual C++ cache files 81 | ipch/ 82 | *.aps 83 | *.ncb 84 | *.opendb 85 | *.opensdf 86 | *.sdf 87 | *.cachefile 88 | *.VC.db 89 | *.VC.VC.opendb 90 | 91 | # Visual Studio profiler 92 | *.psess 93 | *.vsp 94 | *.vspx 95 | *.sap 96 | 97 | # TFS 2012 Local Workspace 98 | $tf/ 99 | 100 | # Guidance Automation Toolkit 101 | *.gpState 102 | 103 | # ReSharper is a .NET coding add-in 104 | _ReSharper*/ 105 | *.[Rr]e[Ss]harper 106 | *.DotSettings.user 107 | 108 | # JustCode is a .NET coding add-in 109 | .JustCode 110 | 111 | # TeamCity is a build add-in 112 | _TeamCity* 113 | 114 | # DotCover is a Code Coverage Tool 115 | *.dotCover 116 | 117 | # NCrunch 118 | _NCrunch_* 119 | .*crunch*.local.xml 120 | nCrunchTemp_* 121 | 122 | # MightyMoose 123 | *.mm.* 124 | AutoTest.Net/ 125 | 126 | # Web workbench (sass) 127 | .sass-cache/ 128 | 129 | # Installshield output folder 130 | [Ee]xpress/ 131 | 132 | # DocProject is a documentation generator add-in 133 | DocProject/buildhelp/ 134 | DocProject/Help/*.HxT 135 | DocProject/Help/*.HxC 136 | DocProject/Help/*.hhc 137 | DocProject/Help/*.hhk 138 | DocProject/Help/*.hhp 139 | DocProject/Help/Html2 140 | DocProject/Help/html 141 | 142 | # Click-Once directory 143 | publish/ 144 | 145 | # Publish Web Output 146 | *.[Pp]ublish.xml 147 | *.azurePubxml 148 | # TODO: Comment the next line if you want to checkin your web deploy settings 149 | # but database connection strings (with potential passwords) will be unencrypted 150 | *.pubxml 151 | *.publishproj 152 | 153 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 154 | # checkin your Azure Web App publish settings, but sensitive information contained 155 | # in these scripts will be unencrypted 156 | PublishScripts/ 157 | 158 | # NuGet Packages 159 | *.nupkg 160 | # The packages folder can be ignored because of Package Restore 161 | **/packages/* 162 | # except build/, which is used as an MSBuild target. 163 | !**/packages/build/ 164 | # Uncomment if necessary however generally it will be regenerated when needed 165 | #!**/packages/repositories.config 166 | # NuGet v3's project.json files produces more ignoreable files 167 | *.nuget.props 168 | *.nuget.targets 169 | 170 | # Microsoft Azure Build Output 171 | csx/ 172 | *.build.csdef 173 | 174 | # Microsoft Azure Emulator 175 | ecf/ 176 | rcf/ 177 | 178 | # Windows Store app package directories and files 179 | AppPackages/ 180 | BundleArtifacts/ 181 | Package.StoreAssociation.xml 182 | _pkginfo.txt 183 | 184 | # Visual Studio cache files 185 | # files ending in .cache can be ignored 186 | *.[Cc]ache 187 | # but keep track of directories ending in .cache 188 | !*.[Cc]ache/ 189 | 190 | # Others 191 | ClientBin/ 192 | ~$* 193 | *~ 194 | *.dbmdl 195 | *.dbproj.schemaview 196 | *.pfx 197 | *.publishsettings 198 | node_modules/ 199 | orleans.codegen.cs 200 | 201 | # Since there are multiple workflows, uncomment next line to ignore bower_components 202 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 203 | #bower_components/ 204 | 205 | # RIA/Silverlight projects 206 | Generated_Code/ 207 | 208 | # Backup & report files from converting an old project file 209 | # to a newer Visual Studio version. Backup files are not needed, 210 | # because we have git ;-) 211 | _UpgradeReport_Files/ 212 | Backup*/ 213 | UpgradeLog*.XML 214 | UpgradeLog*.htm 215 | 216 | # SQL Server files 217 | *.mdf 218 | *.ldf 219 | 220 | # Business Intelligence projects 221 | *.rdl.data 222 | *.bim.layout 223 | *.bim_*.settings 224 | 225 | # Microsoft Fakes 226 | FakesAssemblies/ 227 | 228 | # GhostDoc plugin setting file 229 | *.GhostDoc.xml 230 | 231 | # Node.js Tools for Visual Studio 232 | .ntvs_analysis.dat 233 | 234 | # Visual Studio 6 build log 235 | *.plg 236 | 237 | # Visual Studio 6 workspace options file 238 | *.opt 239 | 240 | # Visual Studio LightSwitch build output 241 | **/*.HTMLClient/GeneratedArtifacts 242 | **/*.DesktopClient/GeneratedArtifacts 243 | **/*.DesktopClient/ModelManifest.xml 244 | **/*.Server/GeneratedArtifacts 245 | **/*.Server/ModelManifest.xml 246 | _Pvt_Extensions 247 | 248 | # Paket dependency manager 249 | .paket/paket.exe 250 | paket-files/ 251 | packages/ 252 | 253 | # FAKE - F# Make 254 | .fake/ 255 | 256 | # JetBrains Rider 257 | .idea/ 258 | *.sln.iml 259 | *.orig 260 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | image: 2 | file: .docker/Dockerfile 3 | 4 | ports: 5 | - port: 8080 6 | 7 | tasks: 8 | - init: npm install 9 | 10 | vscode: 11 | extensions: 12 | - christian-kohler.path-intellisense@1.4.2:QnOrf5fk6KiVaQs4cNEP+w== 13 | - wayou.vscode-todo-highlight@1.0.4:8IqxuxCVol2WnScJc5xVzg== 14 | - mrmlnc.vscode-scss@0.9.0:/wXbNRm+2kunH5HbQqfnXA== 15 | - PKief.material-icon-theme@3.9.2:xeHlNzPEF04yFqz/xKCD5w== 16 | - Ionide.Ionide-fsharp@4.5.0:0qxXuhq6eO066etkNQrKCQ== 17 | - zhuangtongfa.material-theme@3.2.2:jGTZwg0ChZg3eEKHC+UO+w== 18 | - mhutchie.git-graph@1.21.0:zoSeoEOMfrwN0BMBC8VvqQ== 19 | - eamodio.gitlens@10.2.0:GNDO73Cmp0fYDiLNxVkbsQ== 20 | -------------------------------------------------------------------------------- /.paket/Paket.Restore.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 8 | 9 | true 10 | $(MSBuildThisFileDirectory) 11 | $(MSBuildThisFileDirectory)..\ 12 | $(PaketRootPath)paket-files\paket.restore.cached 13 | $(PaketRootPath)paket.lock 14 | classic 15 | proj 16 | assembly 17 | native 18 | /Library/Frameworks/Mono.framework/Commands/mono 19 | mono 20 | 21 | 22 | $(PaketRootPath)paket.bootstrapper.exe 23 | $(PaketToolsPath)paket.bootstrapper.exe 24 | $([System.IO.Path]::GetDirectoryName("$(PaketBootStrapperExePath)"))\ 25 | 26 | 27 | 28 | 29 | $(PaketRootPath)paket.exe 30 | $(PaketToolsPath)paket.exe 31 | $(PaketToolsPath)paket.exe 32 | $(_PaketBootStrapperExeDir)paket.exe 33 | paket.exe 34 | 35 | 36 | $(PaketRootPath)paket 37 | $(PaketToolsPath)paket 38 | $(PaketToolsPath)paket 39 | 40 | 41 | $(PaketRootPath)paket.exe 42 | $(PaketToolsPath)paket.exe 43 | 44 | 45 | $(PaketBootStrapperExeDir)paket.exe 46 | 47 | 48 | paket 49 | 50 | 51 | <_PaketExeExtension>$([System.IO.Path]::GetExtension("$(PaketExePath)")) 52 | dotnet "$(PaketExePath)" 53 | $(MonoPath) --runtime=v4.0.30319 "$(PaketExePath)" 54 | "$(PaketExePath)" 55 | 56 | 57 | "$(PaketBootStrapperExePath)" 58 | $(MonoPath) --runtime=v4.0.30319 "$(PaketBootStrapperExePath)" 59 | 60 | 61 | 62 | 63 | true 64 | true 65 | 66 | 67 | True 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | true 79 | $(NoWarn);NU1603;NU1604;NU1605;NU1608 80 | 81 | 82 | 83 | 84 | /usr/bin/shasum "$(PaketRestoreCacheFile)" | /usr/bin/awk '{ print $1 }' 85 | /usr/bin/shasum "$(PaketLockFilePath)" | /usr/bin/awk '{ print $1 }' 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | $([System.IO.File]::ReadAllText('$(PaketRestoreCacheFile)')) 102 | $([System.IO.File]::ReadAllText('$(PaketLockFilePath)')) 103 | true 104 | false 105 | true 106 | 107 | 108 | 112 | 113 | true 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | $(MSBuildProjectDirectory)\obj\$(MSBuildProjectFile).paket.references.cached 130 | 131 | $(MSBuildProjectFullPath).paket.references 132 | 133 | $(MSBuildProjectDirectory)\$(MSBuildProjectName).paket.references 134 | 135 | $(MSBuildProjectDirectory)\paket.references 136 | 137 | false 138 | true 139 | true 140 | references-file-or-cache-not-found 141 | 142 | 143 | 144 | 145 | $([System.IO.File]::ReadAllText('$(PaketReferencesCachedFilePath)')) 146 | $([System.IO.File]::ReadAllText('$(PaketOriginalReferencesFilePath)')) 147 | references-file 148 | false 149 | 150 | 151 | 152 | 153 | false 154 | 155 | 156 | 157 | 158 | true 159 | target-framework '$(TargetFramework)' or '$(TargetFrameworks)' files @(PaketResolvedFilePaths) 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | false 170 | true 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',').Length) 182 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[0]) 183 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[1]) 184 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[4]) 185 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[5]) 186 | 187 | 188 | %(PaketReferencesFileLinesInfo.PackageVersion) 189 | All 190 | runtime 191 | runtime 192 | true 193 | true 194 | 195 | 196 | 197 | 198 | $(MSBuildProjectDirectory)/obj/$(MSBuildProjectFile).paket.clitools 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[0]) 208 | $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[1]) 209 | 210 | 211 | %(PaketCliToolFileLinesInfo.PackageVersion) 212 | 213 | 214 | 215 | 219 | 220 | 221 | 222 | 223 | 224 | false 225 | $(MSBuildVersion) 226 | 15.8.0 227 | 228 | 229 | 230 | 231 | 232 | <_NuspecFilesNewLocation Include="$(BaseIntermediateOutputPath)$(Configuration)\*.nuspec"/> 233 | 234 | 235 | 236 | 237 | 238 | $(MSBuildProjectDirectory)/$(MSBuildProjectFile) 239 | true 240 | false 241 | true 242 | false 243 | true 244 | false 245 | true 246 | $(BaseIntermediateOutputPath)$(Configuration) 247 | $(BaseIntermediateOutputPath) 248 | 249 | 250 | 251 | <_NuspecFiles Include="$(AdjustedNuspecOutputPath)\*.nuspec"/> 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 304 | 305 | 347 | 348 | 389 | 390 | 391 | 392 | -------------------------------------------------------------------------------- /.paket/paket.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fable-compiler/fable-react/7ca5a7f9645e578aefcf244244047137aba36b13/.paket/paket.exe -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | https://fable.io 5 | https://github.com/fable-compiler/fable-react 6 | LICENSE 7 | README.md 8 | fable_logo.png 9 | Fable contributors 10 | true 11 | 12 | 13 | true 14 | true 15 | true 16 | snupkg 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Fable.React.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30114.105 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{1E4287BE-D16A-4BE4-B83C-6517DFF40321}" 7 | EndProject 8 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Fable.React.Types", "src\Fable.React.Types\Fable.React.Types.fsproj", "{FB949797-DF35-49CC-B94F-1D50127448F4}" 9 | EndProject 10 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Fable.React", "src\Fable.React\Fable.React.fsproj", "{E53327BA-B69D-4720-ACD7-3C1CC413F0EA}" 11 | EndProject 12 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Fable.ReactDom.Types", "src\Fable.ReactDom.Types\Fable.ReactDom.Types.fsproj", "{D3AEACF9-C3DD-4F77-B61E-94E5D916F95F}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Debug|x64 = Debug|x64 18 | Debug|x86 = Debug|x86 19 | Release|Any CPU = Release|Any CPU 20 | Release|x64 = Release|x64 21 | Release|x86 = Release|x86 22 | EndGlobalSection 23 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 24 | {FB949797-DF35-49CC-B94F-1D50127448F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {FB949797-DF35-49CC-B94F-1D50127448F4}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {FB949797-DF35-49CC-B94F-1D50127448F4}.Debug|x64.ActiveCfg = Debug|Any CPU 27 | {FB949797-DF35-49CC-B94F-1D50127448F4}.Debug|x64.Build.0 = Debug|Any CPU 28 | {FB949797-DF35-49CC-B94F-1D50127448F4}.Debug|x86.ActiveCfg = Debug|Any CPU 29 | {FB949797-DF35-49CC-B94F-1D50127448F4}.Debug|x86.Build.0 = Debug|Any CPU 30 | {FB949797-DF35-49CC-B94F-1D50127448F4}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {FB949797-DF35-49CC-B94F-1D50127448F4}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {FB949797-DF35-49CC-B94F-1D50127448F4}.Release|x64.ActiveCfg = Release|Any CPU 33 | {FB949797-DF35-49CC-B94F-1D50127448F4}.Release|x64.Build.0 = Release|Any CPU 34 | {FB949797-DF35-49CC-B94F-1D50127448F4}.Release|x86.ActiveCfg = Release|Any CPU 35 | {FB949797-DF35-49CC-B94F-1D50127448F4}.Release|x86.Build.0 = Release|Any CPU 36 | {E53327BA-B69D-4720-ACD7-3C1CC413F0EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {E53327BA-B69D-4720-ACD7-3C1CC413F0EA}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {E53327BA-B69D-4720-ACD7-3C1CC413F0EA}.Debug|x64.ActiveCfg = Debug|Any CPU 39 | {E53327BA-B69D-4720-ACD7-3C1CC413F0EA}.Debug|x64.Build.0 = Debug|Any CPU 40 | {E53327BA-B69D-4720-ACD7-3C1CC413F0EA}.Debug|x86.ActiveCfg = Debug|Any CPU 41 | {E53327BA-B69D-4720-ACD7-3C1CC413F0EA}.Debug|x86.Build.0 = Debug|Any CPU 42 | {E53327BA-B69D-4720-ACD7-3C1CC413F0EA}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {E53327BA-B69D-4720-ACD7-3C1CC413F0EA}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {E53327BA-B69D-4720-ACD7-3C1CC413F0EA}.Release|x64.ActiveCfg = Release|Any CPU 45 | {E53327BA-B69D-4720-ACD7-3C1CC413F0EA}.Release|x64.Build.0 = Release|Any CPU 46 | {E53327BA-B69D-4720-ACD7-3C1CC413F0EA}.Release|x86.ActiveCfg = Release|Any CPU 47 | {E53327BA-B69D-4720-ACD7-3C1CC413F0EA}.Release|x86.Build.0 = Release|Any CPU 48 | {D3AEACF9-C3DD-4F77-B61E-94E5D916F95F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 49 | {D3AEACF9-C3DD-4F77-B61E-94E5D916F95F}.Debug|Any CPU.Build.0 = Debug|Any CPU 50 | {D3AEACF9-C3DD-4F77-B61E-94E5D916F95F}.Debug|x64.ActiveCfg = Debug|Any CPU 51 | {D3AEACF9-C3DD-4F77-B61E-94E5D916F95F}.Debug|x64.Build.0 = Debug|Any CPU 52 | {D3AEACF9-C3DD-4F77-B61E-94E5D916F95F}.Debug|x86.ActiveCfg = Debug|Any CPU 53 | {D3AEACF9-C3DD-4F77-B61E-94E5D916F95F}.Debug|x86.Build.0 = Debug|Any CPU 54 | {D3AEACF9-C3DD-4F77-B61E-94E5D916F95F}.Release|Any CPU.ActiveCfg = Release|Any CPU 55 | {D3AEACF9-C3DD-4F77-B61E-94E5D916F95F}.Release|Any CPU.Build.0 = Release|Any CPU 56 | {D3AEACF9-C3DD-4F77-B61E-94E5D916F95F}.Release|x64.ActiveCfg = Release|Any CPU 57 | {D3AEACF9-C3DD-4F77-B61E-94E5D916F95F}.Release|x64.Build.0 = Release|Any CPU 58 | {D3AEACF9-C3DD-4F77-B61E-94E5D916F95F}.Release|x86.ActiveCfg = Release|Any CPU 59 | {D3AEACF9-C3DD-4F77-B61E-94E5D916F95F}.Release|x86.Build.0 = Release|Any CPU 60 | EndGlobalSection 61 | GlobalSection(SolutionProperties) = preSolution 62 | HideSolutionNode = FALSE 63 | EndGlobalSection 64 | GlobalSection(NestedProjects) = preSolution 65 | {FB949797-DF35-49CC-B94F-1D50127448F4} = {1E4287BE-D16A-4BE4-B83C-6517DFF40321} 66 | {E53327BA-B69D-4720-ACD7-3C1CC413F0EA} = {1E4287BE-D16A-4BE4-B83C-6517DFF40321} 67 | {D3AEACF9-C3DD-4F77-B61E-94E5D916F95F} = {1E4287BE-D16A-4BE4-B83C-6517DFF40321} 68 | EndGlobalSection 69 | EndGlobal 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Fable contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fable.React 2 | 3 | [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/fable-compiler/fable-react) 4 | 5 | > **ATTENTION**: This package is less well maintained, for new Fable projects using React we recommend [Feliz](https://zaid-ajaj.github.io/Feliz/). 6 | 7 | `Fable.React.Types` package contains bindings for [React](https://reactjs.org/). 8 | 9 | `Fable.React` package contains helpers for writing for React projects using Fable. 10 | 11 | When updating a package, edit the RELEASE_NOTES.md of the corresponding project and run `dotnet fsi build.fsx publish` to publish a new version. 12 | 13 | ## Documents 14 | 15 | * [Server-Side Rendering tutorial](docs/server-side-rendering.md): A **Pure F#** solution for SSR, **No NodeJS Required!** 16 | * [Using third party React components](docs/using-third-party-react-components.md): How to create binding so that third party Javascript React components can be used like stock React components in Fable code. 17 | * [React error boundaries](docs/react-error-boundaries.md): Example on how to use react error boundaries in fable. 18 | * [Function components, hooks and code splitting in Fable.React 5](https://fable.io/blog/Announcing-Fable-React-5.html) 19 | 20 | ## Other packages 21 | 22 | This repository previously contained other packages like Fable.React.TransitionGroup, but they've been moved to their own repos. [Find here where](https://github.com/fable-compiler/fable-react/issues/145#issuecomment-478961364). 23 | -------------------------------------------------------------------------------- /SSRSample/.paket/Paket.Restore.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 8 | 9 | $(MSBuildVersion) 10 | 15.0.0 11 | false 12 | true 13 | 14 | true 15 | $(MSBuildThisFileDirectory) 16 | $(MSBuildThisFileDirectory)..\ 17 | $(PaketRootPath)paket-files\paket.restore.cached 18 | $(PaketRootPath)paket.lock 19 | classic 20 | proj 21 | assembly 22 | native 23 | /Library/Frameworks/Mono.framework/Commands/mono 24 | mono 25 | 26 | 27 | $(PaketRootPath)paket.bootstrapper.exe 28 | $(PaketToolsPath)paket.bootstrapper.exe 29 | $([System.IO.Path]::GetDirectoryName("$(PaketBootStrapperExePath)"))\ 30 | 31 | "$(PaketBootStrapperExePath)" 32 | $(MonoPath) --runtime=v4.0.30319 "$(PaketBootStrapperExePath)" 33 | 34 | 35 | 36 | 37 | true 38 | true 39 | 40 | 41 | True 42 | 43 | 44 | False 45 | 46 | $(BaseIntermediateOutputPath.TrimEnd('\').TrimEnd('\/')) 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | $(PaketRootPath)paket 56 | $(PaketToolsPath)paket 57 | 58 | 59 | 60 | 61 | 62 | $(PaketRootPath)paket.exe 63 | $(PaketToolsPath)paket.exe 64 | 65 | 66 | 67 | 68 | 69 | <_DotnetToolsJson Condition="Exists('$(PaketRootPath)/.config/dotnet-tools.json')">$([System.IO.File]::ReadAllText("$(PaketRootPath)/.config/dotnet-tools.json")) 70 | <_ConfigContainsPaket Condition=" '$(_DotnetToolsJson)' != ''">$(_DotnetToolsJson.Contains('"paket"')) 71 | <_ConfigContainsPaket Condition=" '$(_ConfigContainsPaket)' == ''">false 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | <_PaketCommand>dotnet paket 83 | 84 | 85 | 86 | 87 | 88 | $(PaketToolsPath)paket 89 | $(PaketBootStrapperExeDir)paket 90 | 91 | 92 | paket 93 | 94 | 95 | 96 | 97 | <_PaketExeExtension>$([System.IO.Path]::GetExtension("$(PaketExePath)")) 98 | <_PaketCommand Condition=" '$(_PaketCommand)' == '' AND '$(_PaketExeExtension)' == '.dll' ">dotnet "$(PaketExePath)" 99 | <_PaketCommand Condition=" '$(_PaketCommand)' == '' AND '$(OS)' != 'Windows_NT' AND '$(_PaketExeExtension)' == '.exe' ">$(MonoPath) --runtime=v4.0.30319 "$(PaketExePath)" 100 | <_PaketCommand Condition=" '$(_PaketCommand)' == '' ">"$(PaketExePath)" 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | true 122 | $(NoWarn);NU1603;NU1604;NU1605;NU1608 123 | false 124 | true 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | $([System.IO.File]::ReadAllText('$(PaketRestoreCacheFile)')) 134 | 135 | 136 | 137 | 138 | 139 | 141 | $([System.Text.RegularExpressions.Regex]::Split(`%(Identity)`, `": "`)[0].Replace(`"`, ``).Replace(` `, ``)) 142 | $([System.Text.RegularExpressions.Regex]::Split(`%(Identity)`, `": "`)[1].Replace(`"`, ``).Replace(` `, ``)) 143 | 144 | 145 | 146 | 147 | %(PaketRestoreCachedKeyValue.Value) 148 | %(PaketRestoreCachedKeyValue.Value) 149 | 150 | 151 | 152 | 153 | true 154 | false 155 | true 156 | 157 | 158 | 162 | 163 | true 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | $(PaketIntermediateOutputPath)\$(MSBuildProjectFile).paket.references.cached 183 | 184 | $(MSBuildProjectFullPath).paket.references 185 | 186 | $(MSBuildProjectDirectory)\$(MSBuildProjectName).paket.references 187 | 188 | $(MSBuildProjectDirectory)\paket.references 189 | 190 | false 191 | true 192 | true 193 | references-file-or-cache-not-found 194 | 195 | 196 | 197 | 198 | $([System.IO.File]::ReadAllText('$(PaketReferencesCachedFilePath)')) 199 | $([System.IO.File]::ReadAllText('$(PaketOriginalReferencesFilePath)')) 200 | references-file 201 | false 202 | 203 | 204 | 205 | 206 | false 207 | 208 | 209 | 210 | 211 | true 212 | target-framework '$(TargetFramework)' or '$(TargetFrameworks)' files @(PaketResolvedFilePaths) 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | false 224 | true 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',').Length) 236 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[0]) 237 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[1]) 238 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[4]) 239 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[5]) 240 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[6]) 241 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[7]) 242 | 243 | 244 | %(PaketReferencesFileLinesInfo.PackageVersion) 245 | All 246 | runtime 247 | $(ExcludeAssets);contentFiles 248 | $(ExcludeAssets);build;buildMultitargeting;buildTransitive 249 | true 250 | true 251 | 252 | 253 | 254 | 255 | $(PaketIntermediateOutputPath)/$(MSBuildProjectFile).paket.clitools 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[0]) 265 | $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[1]) 266 | 267 | 268 | %(PaketCliToolFileLinesInfo.PackageVersion) 269 | 270 | 271 | 272 | 276 | 277 | 278 | 279 | 280 | 281 | false 282 | 283 | 284 | 285 | 286 | 287 | <_NuspecFilesNewLocation Include="$(PaketIntermediateOutputPath)\$(Configuration)\*.nuspec"/> 288 | 289 | 290 | 291 | 292 | 293 | $(MSBuildProjectDirectory)/$(MSBuildProjectFile) 294 | true 295 | false 296 | true 297 | false 298 | true 299 | false 300 | true 301 | false 302 | true 303 | false 304 | true 305 | $(PaketIntermediateOutputPath)\$(Configuration) 306 | $(PaketIntermediateOutputPath) 307 | 308 | 309 | 310 | <_NuspecFiles Include="$(AdjustedNuspecOutputPath)\*.$(PackageVersion.Split(`+`)[0]).nuspec"/> 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 370 | 371 | 420 | 421 | 466 | 467 | 511 | 512 | 555 | 556 | 557 | 558 | -------------------------------------------------------------------------------- /SSRSample/.paket/paket.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fable-compiler/fable-react/7ca5a7f9645e578aefcf244244047137aba36b13/SSRSample/.paket/paket.exe -------------------------------------------------------------------------------- /SSRSample/benchmark/dotnet.fs: -------------------------------------------------------------------------------- 1 | 2 | open System 3 | open System.Threading 4 | open System.Diagnostics 5 | open Shared.Types 6 | open Fable.ReactServer 7 | open FSharp.Core 8 | 9 | let initState: Model = { 10 | counter = Some 42 11 | someString = "Some String" 12 | someFloat = 11.11 13 | someInt = 22 14 | } 15 | 16 | let coreCount = Environment.ProcessorCount 17 | let workerTimes = 20_000 18 | let totalTimes = workerTimes * coreCount 19 | let mutable totalms = 0L 20 | 21 | let reset () = 22 | totalms <- 0L 23 | 24 | let render times () = 25 | reset () 26 | let tid = Thread.CurrentThread.ManagedThreadId 27 | printfn "Thread %i started" tid 28 | let watch = Stopwatch() 29 | watch.Start() 30 | for i = 1 to times do 31 | renderToString(Shared.View.view initState ignore) |> ignore 32 | watch.Stop() 33 | totalms <- totalms + watch.ElapsedMilliseconds 34 | printfn "Thread %i render %d times used %dms" tid times watch.ElapsedMilliseconds 35 | int watch.ElapsedMilliseconds 36 | 37 | let singleTest () = 38 | let times = totalTimes 39 | let time = render times () 40 | printfn "[Single thread] %dms %.3freq/s" time ((float times) / (float time) * 1000.) 41 | 42 | let tasksTest () = 43 | Tasks.Parallel.For(0, coreCount, (fun _ -> render workerTimes () |> ignore)) |> ignore 44 | Process.GetCurrentProcess().WorkingSet64 45 | 46 | let log label memory = 47 | let totalms = totalms / (int64 coreCount) 48 | printfn "[%d %s] Total: %dms Memory footprint: %.3fMB Requests/sec: %.3f" coreCount label totalms ((float memory) / 1024. / 1024.) ((float totalTimes) / (float totalms) * 1000.) 49 | 50 | [] 51 | let main _ = 52 | reset () 53 | singleTest () 54 | reset () 55 | tasksTest() |> log "tasks" 56 | 0 57 | -------------------------------------------------------------------------------- /SSRSample/benchmark/dotnet.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp2.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /SSRSample/benchmark/node.js: -------------------------------------------------------------------------------- 1 | const cluster = require('cluster'); 2 | const http = require('http'); 3 | const View = require('../src/Client/bin/lib/View') 4 | const ReactDOMServer = require('react-dom/server') 5 | const os = require('os') 6 | const coreCount = os.cpus().length; 7 | 8 | const initState = { 9 | counter: 42, 10 | someString: "Some String", 11 | someFloat: 11.11, 12 | someInt: 22, 13 | } 14 | 15 | const noop = () => {} 16 | const mb = bytes => (bytes / 1024 / 1024).toFixed(3) 17 | const workerTimes = 20000 18 | const totalTimes = workerTimes * coreCount 19 | 20 | function render(len = workerTimes) { 21 | const start = Date.now() 22 | while (len--) { 23 | ReactDOMServer.renderToString(View.view(initState, noop)) 24 | } 25 | return Date.now() - start 26 | } 27 | 28 | function singleTest() { 29 | const times = totalTimes 30 | const time = render(times) 31 | console.log(`[Single process] ${time}ms ${(times / time * 1000).toFixed(3)}req/s`) 32 | } 33 | 34 | if (cluster.isMaster) { 35 | console.log(`Master ${process.pid} is running`); 36 | 37 | singleTest() 38 | 39 | // Fork workers. 40 | for (let i = 0; i < coreCount; i++) { 41 | const worker = cluster.fork(); 42 | } 43 | 44 | let totalms = 0 45 | let count = 0 46 | let memoryUsed = 0 47 | function messageHandler(msg) { 48 | if (msg.cmd === 'finished') { 49 | count++ 50 | totalms = totalms + msg.time 51 | memoryUsed += msg.memory 52 | if (count >= coreCount) { 53 | totalms = totalms / coreCount 54 | console.log(`[${coreCount} workers] Total: ${totalms}ms Memory footprint: ${mb(memoryUsed)}MB Requests/sec: ${(totalTimes / totalms * 1000).toFixed(3)}`) 55 | for (const id in cluster.workers) { 56 | cluster.workers[id].destroy() 57 | } 58 | process.exit(0) 59 | } 60 | } 61 | } 62 | 63 | for (const id in cluster.workers) { 64 | cluster.workers[id].on('message', messageHandler); 65 | } 66 | cluster.on('exit', (worker, code, signal) => { 67 | console.log(`worker ${worker.process.pid} died`); 68 | }); 69 | } else { 70 | console.log(`Worker ${process.pid}: started`); 71 | const time = render() 72 | console.log(`Worker ${process.pid}: render ${workerTimes} times used ${time}ms`) 73 | const mem = process.memoryUsage() 74 | process.send({ 75 | cmd: 'finished', 76 | time, 77 | memory: mem.heapTotal + mem.external 78 | }) 79 | } 80 | -------------------------------------------------------------------------------- /SSRSample/benchmark/paket.references: -------------------------------------------------------------------------------- 1 | group Server 2 | FSharp.Core 3 | 4 | group Client 5 | Fable.Core 6 | Fable.Browser.Dom 7 | -------------------------------------------------------------------------------- /SSRSample/build.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | cls 3 | 4 | IF EXIST "paket.lock" ( 5 | .paket\paket.exe restore 6 | ) ELSE ( 7 | .paket\paket.exe install 8 | ) 9 | 10 | if errorlevel 1 ( 11 | exit /b %errorlevel% 12 | ) 13 | 14 | packages\build\FAKE\tools\FAKE.exe build.fsx %* 15 | -------------------------------------------------------------------------------- /SSRSample/build.fsx: -------------------------------------------------------------------------------- 1 | #r @"packages/build/FAKE/tools/FakeLib.dll" 2 | 3 | open System 4 | 5 | open Fake 6 | 7 | let serverPath = "./src/Server" |> FullName 8 | let benchmarkPath = "./benchmark" |> FullName 9 | let clientPath = "./src/Client" |> FullName 10 | let deployDir = "./deploy" |> FullName 11 | let packageJsonDir = "../../" |> FullName 12 | 13 | let platformTool tool winTool = 14 | let tool = if isUnix then tool else winTool 15 | tool 16 | |> ProcessHelper.tryFindFileOnPath 17 | |> function Some t -> t | _ -> failwithf "%s not found" tool 18 | 19 | let nodeTool = platformTool "node" "node.exe" 20 | let npmTool = platformTool "npm" "npm.cmd" 21 | 22 | let mutable dotnetCli = "dotnet" 23 | 24 | let runWithEnv cmd args workingDir (env: (string * string) list) = 25 | let result = 26 | ExecProcess (fun info -> 27 | info.FileName <- cmd 28 | for (key, value) in env do 29 | info.Environment.Add(key, value) 30 | info.WorkingDirectory <- workingDir 31 | info.Arguments <- args) TimeSpan.MaxValue 32 | if result <> 0 then failwithf "'%s %s' failedwith" cmd args 33 | 34 | let run cmd args workingDir = 35 | runWithEnv cmd args workingDir [] 36 | 37 | Target "Clean" (fun _ -> 38 | CleanDirs [deployDir] 39 | ) 40 | 41 | Target "InstallClient" (fun _ -> 42 | printfn "Node version:" 43 | run nodeTool "--version" packageJsonDir 44 | printfn "Npm version:" 45 | run npmTool "--version" packageJsonDir 46 | run npmTool "install" packageJsonDir 47 | ) 48 | 49 | Target "Build" (fun () -> 50 | run dotnetCli "build" serverPath 51 | run npmTool "run ssrsample-build" packageJsonDir 52 | ) 53 | 54 | Target "BuildBench" (fun () -> 55 | run dotnetCli "build --configuration Release" serverPath 56 | run dotnetCli "build --configuration Release" benchmarkPath 57 | run npmTool "run ssrsample-build-lib" packageJsonDir 58 | ) 59 | 60 | Target "Bench" (fun () -> 61 | run dotnetCli "./bin/Release/netcoreapp2.0/dotnet.dll" benchmarkPath 62 | runWithEnv nodeTool "./node.js" benchmarkPath ["NODE_ENV", "production"] 63 | ) 64 | 65 | Target "Run" (fun () -> 66 | let server = async { 67 | run dotnetCli "watch run" serverPath 68 | } 69 | let client = async { 70 | run npmTool "run ssrsample-start" packageJsonDir 71 | } 72 | let browser = async { 73 | Threading.Thread.Sleep 10000 74 | Diagnostics.Process.Start "http://localhost:8085" |> ignore 75 | } 76 | 77 | [ server; client; browser] 78 | |> Async.Parallel 79 | |> Async.RunSynchronously 80 | |> ignore 81 | ) 82 | 83 | 84 | "Clean" 85 | ==> "InstallClient" 86 | ==> "Build" 87 | 88 | "InstallClient" 89 | ==> "Run" 90 | 91 | "InstallClient" 92 | ==> "BuildBench" 93 | ==> "Bench" 94 | 95 | RunTargetOrDefault "Run" 96 | -------------------------------------------------------------------------------- /SSRSample/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | 5 | cd "$(dirname "$0")" 6 | 7 | PAKET_EXE=.paket/paket.exe 8 | FAKE_EXE=packages/build/FAKE/tools/FAKE.exe 9 | 10 | FSIARGS="" 11 | FSIARGS2="" 12 | OS=${OS:-"unknown"} 13 | if [ "$OS" != "Windows_NT" ] 14 | then 15 | # Can't use FSIARGS="--fsiargs -d:MONO" in zsh, so split it up 16 | # (Can't use arrays since dash can't handle them) 17 | FSIARGS="--fsiargs" 18 | FSIARGS2="-d:MONO" 19 | fi 20 | 21 | run() { 22 | if [ "$OS" != "Windows_NT" ] 23 | then 24 | mono "$@" 25 | else 26 | "$@" 27 | fi 28 | } 29 | 30 | echo "Executing Paket..." 31 | 32 | FILE='paket.lock' 33 | if [ -f $FILE ]; then 34 | echo "paket.lock file found, restoring packages..." 35 | run $PAKET_EXE restore 36 | else 37 | echo "paket.lock was not found, installing packages..." 38 | run $PAKET_EXE install 39 | fi 40 | 41 | run $FAKE_EXE "$@" $FSIARGS $FSIARGS2 build.fsx 42 | 43 | -------------------------------------------------------------------------------- /SSRSample/paket.dependencies: -------------------------------------------------------------------------------- 1 | github elmish/react src/common.fs 2 | github elmish/react src/react.fs 3 | 4 | group Server 5 | source https://api.nuget.org/v3/index.json 6 | storage: none 7 | 8 | nuget FSharp.Core 9 | nuget Giraffe ~> 1 10 | nuget Microsoft.AspNetCore 11 | nuget Microsoft.AspNetCore.StaticFiles 12 | nuget Thoth.Json.Giraffe 13 | 14 | group Client 15 | source https://api.nuget.org/v3/index.json 16 | storage: none 17 | 18 | nuget Fable.Core 19 | nuget Fable.Elmish 20 | nuget Fable.Browser.Dom 21 | nuget Thoth.Json 22 | 23 | group Build 24 | source https://api.nuget.org/v3/index.json 25 | 26 | nuget FAKE ~> 4 27 | -------------------------------------------------------------------------------- /SSRSample/src/Client/Bench.fs: -------------------------------------------------------------------------------- 1 | module Client.Bench 2 | 3 | open Fable.Core 4 | open Fable.Core.JsInterop 5 | open Shared.Types 6 | open Shared.View 7 | open Fable.React 8 | open Browser 9 | 10 | let initState: Model = { 11 | counter = Some 42 12 | someString = "Some String" 13 | someFloat = 11.11 14 | someInt = 22 15 | } 16 | 17 | [] 18 | let isNode: bool = jsNative 19 | 20 | let jsRenderBench () = 21 | if not isNode then () else 22 | 23 | let renderToString: ReactElement -> string = importMember "react-dom/server" 24 | let mutable times = 10000 25 | let label = sprintf "render %d times in nodejs" times 26 | console.time(label) 27 | while times > 0 do 28 | renderToString (view initState ignore) |> ignore 29 | times <- times - 1 30 | console.timeEnd(label) 31 | 32 | jsRenderBench () 33 | -------------------------------------------------------------------------------- /SSRSample/src/Client/Client.fs: -------------------------------------------------------------------------------- 1 | module Client.Main 2 | 3 | open Elmish 4 | open Elmish.React 5 | 6 | open Fable.Core 7 | open Fable.Core.JsInterop 8 | open Thoth.Json 9 | open Shared.Types 10 | open Shared.View 11 | open Browser 12 | 13 | // let div = document.getElementById("elmish-app") 14 | // div.innerHTML <- "" 15 | // console.log("root", div) 16 | 17 | let init () = 18 | // Init model by server side state 19 | let model = 20 | match Decode.Auto.fromString window?__INIT_STATE__ with 21 | | Ok model -> model 22 | | Error er -> 23 | JS.console.error("Cannot decode init state", er, window?__INIT_STATE__) 24 | Model.Empty 25 | // let cmd = 26 | // Cmd.ofPromise 27 | // (fetchAs "/api/init") 28 | // [] 29 | // (Ok >> Init) 30 | // (Error >> Init) 31 | model, Cmd.none 32 | 33 | let update msg (model : Model) = 34 | let model' = 35 | match model.counter, msg with 36 | | Some x, Increment -> { model with counter=Some (x + 1) } 37 | | Some x, Decrement -> { model with counter=Some (x - 1) } 38 | | None, Init (Ok x) -> x 39 | | _ -> model 40 | model', Cmd.none 41 | 42 | 43 | Program.mkProgram init update view 44 | |> Program.withReactHydrate "elmish-app" 45 | |> Program.run 46 | -------------------------------------------------------------------------------- /SSRSample/src/Client/Client.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /SSRSample/src/Client/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 20px 40px; 3 | } 4 | .counter-app { 5 | margin: 20px 0; 6 | } 7 | .counter { 8 | margin: 10px auto 0; 9 | display: inline-block; 10 | } 11 | 12 | .counter .tag { 13 | margin: 0 15px; 14 | } 15 | 16 | .intro, 17 | .test-case { 18 | max-width: 600px; 19 | margin: 20px auto; 20 | } 21 | 22 | .test-case { 23 | border: 1px solid #ccc; 24 | padding: 10px; 25 | } 26 | 27 | .label { 28 | margin-right: 6px; 29 | } 30 | 31 | .checkbox, .input, .textarea { 32 | margin-right: 10px; 33 | margin-bottom: 10px; 34 | } 35 | 36 | 37 | .children-comp, 38 | .class-comp, 39 | .fn-comp, 40 | .js-comp { 41 | margin-bottom: 15px; 42 | padding: 10px; 43 | border: 1px solid #f93; 44 | } 45 | 46 | .children-comp > div { 47 | margin-bottom: 10px; 48 | border: 1px solid #0cb3f0; 49 | padding: 5px 10px; 50 | display: block; 51 | } 52 | 53 | [placeholder][contenteditable]:empty:before { 54 | content: attr(placeholder); 55 | color: #9b9b9b; 56 | } 57 | 58 | [placeholder][contenteditable]:empty:focus:before { 59 | content: ""; 60 | } 61 | -------------------------------------------------------------------------------- /SSRSample/src/Client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SAFE Template 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /SSRSample/src/Client/paket.references: -------------------------------------------------------------------------------- 1 | group Client 2 | Fable.Core 3 | Fable.Elmish 4 | Thoth.Json -------------------------------------------------------------------------------- /SSRSample/src/Server/Server.fs: -------------------------------------------------------------------------------- 1 | module Server.Main 2 | 3 | open System 4 | open System.IO 5 | open System.Threading.Tasks 6 | open Microsoft.AspNetCore 7 | open Microsoft.AspNetCore.Builder 8 | open Microsoft.AspNetCore.Hosting 9 | open Microsoft.Extensions.DependencyInjection 10 | 11 | open Giraffe 12 | open Giraffe.GiraffeViewEngine 13 | open Giraffe.Serialization.Json 14 | 15 | open Thoth.Json.Net 16 | open Thoth.Json.Giraffe 17 | 18 | open Shared.Types 19 | 20 | 21 | let clientPath = Path.Combine(__SOURCE_DIRECTORY__,"..","Client") |> Path.GetFullPath 22 | let port = 8085us 23 | let assetsBaseUrl = "http://localhost:8080" 24 | 25 | let initState: Model = { 26 | counter = Some 42 27 | someString = "Some String" 28 | someFloat = 11.11 29 | someInt = 22 30 | } 31 | let getInitCounter () : Task = task { return initState } 32 | 33 | let htmlTemplate = 34 | let clientHtml = Fable.ReactServer.renderToString(Shared.View.view initState ignore) 35 | 36 | let stateJson = // Serialize twice to output json as js string in html 37 | Encode.Auto.toString(0, initState) 38 | |> Encode.string |> Encode.toString 0 39 | html [] 40 | [ head [] 41 | [ link 42 | [ _rel "stylesheet" 43 | _href "https://cdnjs.cloudflare.com/ajax/libs/bulma/0.6.2/css/bulma.min.css" 44 | ] 45 | link 46 | [ _rel "stylesheet" 47 | _href (assetsBaseUrl + "/index.css") 48 | ] 49 | ] 50 | body [] 51 | [ div [_id "elmish-app"] [ rawText clientHtml ] 52 | script [] 53 | [ rawText (sprintf """ 54 | var __INIT_STATE__ = %s 55 | """ stateJson) ] 56 | script [ _src (assetsBaseUrl + "/public/bundle.js") ] [] 57 | ] 58 | ] 59 | 60 | 61 | let webApp : HttpHandler = 62 | choose [ 63 | 64 | route "/" >=> htmlView htmlTemplate 65 | route "/api/init" >=> 66 | fun next ctx -> 67 | task { 68 | let! counter = getInitCounter() 69 | return! Successful.OK counter next ctx 70 | } 71 | ] 72 | 73 | let configureApp (app : IApplicationBuilder) = 74 | app.UseStaticFiles() 75 | .UseGiraffe webApp 76 | 77 | 78 | let configureServices (services : IServiceCollection) = 79 | services.AddGiraffe() |> ignore 80 | services.AddSingleton() |> ignore 81 | 82 | [] 83 | let main argv = 84 | WebHost 85 | .CreateDefaultBuilder() 86 | .UseWebRoot(clientPath) 87 | .UseContentRoot(clientPath) 88 | .Configure(Action configureApp) 89 | .ConfigureServices(configureServices) 90 | .UseUrls("http://0.0.0.0:" + port.ToString() + "/") 91 | .Build() 92 | .Run() 93 | 0 94 | -------------------------------------------------------------------------------- /SSRSample/src/Server/Server.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | netcoreapp2.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /SSRSample/src/Server/paket.references: -------------------------------------------------------------------------------- 1 | group Server 2 | FSharp.Core 3 | Giraffe 4 | Microsoft.AspNetCore 5 | Microsoft.AspNetCore.StaticFiles 6 | Thoth.Json.Giraffe 7 | 8 | group Client 9 | Fable.Core 10 | -------------------------------------------------------------------------------- /SSRSample/src/Shared/Shared.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /SSRSample/src/Shared/Types.fs: -------------------------------------------------------------------------------- 1 | namespace Shared.Types 2 | 3 | type Counter = int 4 | 5 | type Model = 6 | { 7 | counter: Counter option 8 | someString: string 9 | someFloat: float 10 | someInt: int 11 | } 12 | static member Empty = 13 | { counter = None 14 | someString = "" 15 | someFloat = 0. 16 | someInt = 0 } 17 | 18 | type Msg = 19 | | Increment 20 | | Decrement 21 | | Init of Result -------------------------------------------------------------------------------- /SSRSample/src/Shared/View.fs: -------------------------------------------------------------------------------- 1 | 2 | module Shared.View 3 | 4 | 5 | open Fable.Core 6 | open Fable.Core.JsInterop 7 | open Fable.React 8 | open Fable.React.Props 9 | open Fable.React.Isomorphic 10 | open Shared.Types 11 | open Shared 12 | 13 | 14 | let show = function 15 | | Some x -> string x 16 | | None -> "Loading..." 17 | 18 | let safeComponents = 19 | let intersperse sep ls = 20 | List.foldBack (fun x -> function 21 | | [] -> [x] 22 | | xs -> x::sep::xs) ls [] 23 | 24 | let components = 25 | [ 26 | "Giraffe", "https://github.com/giraffe-fsharp/Giraffe" 27 | "Fable", "http://fable.io" 28 | "Elmish", "https://fable-elmish.github.io/" 29 | ] 30 | |> List.map (fun (desc,link) -> a [ Href link ] [ str desc ] ) 31 | |> intersperse (str ", ") 32 | |> span [ ] 33 | 34 | p [ ] 35 | [ strong [] [ str "SAFE Template" ] 36 | str " powered by: " 37 | components ] 38 | 39 | type JsCompProps = { 40 | text: string 41 | } 42 | 43 | let jsComp (props: JsCompProps) = 44 | ofImport "default" "./jsComp" props [] 45 | 46 | let jsCompServer (props: JsCompProps) = 47 | div [] [ str "loading" ] 48 | 49 | 50 | type MyProp = { 51 | text: string 52 | } 53 | type MyState = { 54 | text: string 55 | } 56 | 57 | type MyReactComp(initProps: MyProp) = 58 | inherit Component(initProps) with 59 | 60 | do base.setInitState({ text="my state" }) 61 | 62 | override x.render() = 63 | div [ ClassName "class-comp children-comp" ] 64 | [ div [] [ str (sprintf "prop: %s state: %s" x.props.text x.state.text) ] 65 | div [] [ ofArray x.children ] ] 66 | 67 | 68 | 69 | type FnCompProps = { 70 | text: string 71 | } 72 | 73 | let fnComp (props: FnCompProps) = 74 | div [ ClassName "fn-comp" ] 75 | [ span [] [ str (sprintf "prop: %s" props.text) ] ] 76 | 77 | type FnCompWithChildrenProps = { 78 | children: ReactElement array 79 | text: string 80 | } 81 | 82 | let fnCompWithChildren (props: FnCompWithChildrenProps) = 83 | div [ ClassName "fn-comp children-comp" ] 84 | [ div [] [ str (sprintf "prop: %s" props.text) ] 85 | div [] [ ofArray props.children ] ] 86 | 87 | let view (model: Model) (dispatch) = 88 | div [] 89 | [ h1 [ ClassName "title is-1 has-text-centered"] [ str "Server-Side Rendering Sample" ] 90 | div [ ClassName "intro" ] 91 | [ p [] [ str "The initial state is rendered in html from server." ] 92 | div [ ClassName "counter-app" ] 93 | [ p [] [ str "Press buttons to manipulate counter:" ] 94 | div [ ClassName "counter" ] 95 | [ button [ ClassName "button is-small"; OnClick (fun _ -> dispatch Decrement) ] [ str "-" ] 96 | span [ ClassName "tag is-info" ] [ str (show model.counter) ] 97 | button [ ClassName "button is-small"; OnClick (fun _ -> dispatch Increment) ] [ str "+" ] 98 | ] 99 | ] 100 | safeComponents 101 | ] 102 | div [ ClassName "test-case" ] [ 103 | span [ ClassName "label" ] [ str "Test str:" ] 104 | span [] [ str model.someString ] 105 | ] 106 | div [ ClassName "test-case" ] [ 107 | span [ ClassName "label" ] [ str "Test ofFloat:" ] 108 | span [] [ ofFloat model.someFloat ] 109 | ] 110 | div [ ClassName "test-case" ] [ 111 | span [ ClassName "label" ] [ str "Test ofInt:" ] 112 | span [] [ ofInt model.someInt ] 113 | ] 114 | div [ ClassName "test-case" ] [ 115 | span [ ClassName "label" ] [ str "Test html attr:" ] 116 | span [ Id "someId"; Data ("aa", "bb"); HTMLAttr.Custom ("cc", "dd") ] [ str "data-aa" ] 117 | ] 118 | div [ ClassName "test-case" ] [ 119 | span [ ClassName "label" ] [ str "Test CSS prop:" ] 120 | div [ Style [ Display DisplayOptions.Block 121 | CSSProp.Color "red" ] ] [ str "Custom CSSProp" ] 122 | ] 123 | div [ ClassName "test-case" ] [ 124 | span [ ClassName "label" ] [ str "Test checkbox:" ] 125 | input [ ClassName "checkbox"; Type "checkbox"; DefaultChecked true ] 126 | input [ ClassName "checkbox"; Type "checkbox"; DefaultChecked false ] 127 | input [ ClassName "checkbox"; Type "checkbox"; Checked true; OnChange ignore ] 128 | input [ ClassName "checkbox"; Type "checkbox"; Checked false; OnChange ignore ] 129 | ] 130 | div [ ClassName "test-case" ] [ 131 | span [ ClassName "label" ] [ str "Test value:" ] 132 | input [ ClassName "input"; Type "text"; DefaultValue "true" ] 133 | input [ ClassName "input"; Type "text"; DefaultValue "false" ] 134 | input [ ClassName "input"; Type "text"; Value "true"; OnChange ignore ] 135 | input [ ClassName "input"; Type "text"; Value "false"; OnChange ignore ] 136 | ] 137 | 138 | div [ ClassName "test-case" ] [ 139 | span [ ClassName "label" ] [ str "Test textarea:" ] 140 | textarea [ ClassName "textarea"; DefaultValue "true" ] [] 141 | textarea [ ClassName "textarea"; DefaultValue "false" ] [] 142 | textarea [ ClassName "textarea"; Value "true"; OnChange ignore] [] 143 | textarea [ ClassName "textarea"; Value "false"; OnChange ignore] [] 144 | ] 145 | 146 | div [ ClassName "test-case" ] [ 147 | span [ ClassName "label" ] [ str "Test React.Fragment:" ] 148 | fragment [] 149 | [ span [] [ str "child 1" ] 150 | span [] [ str "child 2" ] 151 | span [] [ str "child 3" ] 152 | span [] [ str "child 4" ] 153 | ] 154 | ] 155 | 156 | div [ ClassName "test-case" ] [ 157 | span [ ClassName "label" ] [ str "Test escape:" ] 158 | fragment [] 159 | [ span 160 | [ Data ("value", "
\"\'&
"); 161 | // Style [ Display "
\"\'&
"] 162 | ] 163 | [ str "
\"\'&
" ] 164 | ] 165 | ] 166 | 167 | div [ ClassName "test-case" ] [ 168 | span [ ClassName "label" ] [ str "Test js component:" ] 169 | isomorphicView jsComp jsCompServer { text="I'm rendered by a js Component!" } 170 | ] 171 | 172 | div [ ClassName "test-case" ] [ 173 | span [ ClassName "label" ] [ str "Test ofType:" ] 174 | ofType { text="my prop" } [ span [] [ str " I'm rendered by children!"] ] 175 | ] 176 | 177 | div [ ClassName "test-case" ] [ 178 | span [ ClassName "label" ] [ str "Test null:" ] 179 | null 180 | ] 181 | 182 | div [ ClassName "test-case" ] [ 183 | span [ ClassName "label" ] [ str "Test ofFunction:" ] 184 | ofFunction fnComp { text = "I'm rendered by Function Component!"} [] 185 | ofFunction fnCompWithChildren { text = " I'm rendered by Function Component! "; children=[||]} [ span [] [ str " I'm rendered by children!"] ] 186 | ] 187 | 188 | div [ ClassName "test-case" ] [ 189 | span [ ClassName "label" ] [ str "Test void elements:" ] 190 | hr [ Style [ BorderColor "green" ] ] 191 | br [] 192 | ] 193 | 194 | div [ ClassName "test-case" ] [ 195 | span [ ClassName "label" ] [ str "Test add slug to attributes:" ] 196 | div 197 | [ HTMLAttr.Custom ("contentEditable", "true") 198 | HTMLAttr.Custom ("placeholder", "I'm editable!") 199 | Style [ CSSProp.Custom ("-webkit-transform", "translateX(30px)"); CSSProp.Custom ("-webkit-transform-origin", "0 0") ] ] 200 | [ ] 201 | ] 202 | ] 203 | -------------------------------------------------------------------------------- /SSRSample/src/Shared/jsComp.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function JsComp({ text }) { 4 | return React.createElement('div', { className: 'js-comp' }, text) 5 | } 6 | -------------------------------------------------------------------------------- /SSRSample/src/Shared/paket.references: -------------------------------------------------------------------------------- 1 | group Server 2 | FSharp.Core 3 | 4 | group Client 5 | Fable.Core 6 | Fable.Browser.Dom 7 | -------------------------------------------------------------------------------- /SSRSample/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | const execSync = require("child_process").execSync; 4 | 5 | var CONFIG = { 6 | fsharpEntry: './src/Client/Client.fsproj', 7 | outputDir: './src/Client/public', 8 | assetsDir: './src/Client', 9 | devServerPort: 8080, 10 | devServerProxy: { 11 | '/api/*': { 12 | target: 'http://localhost:8085', 13 | } 14 | }, 15 | // Use babel-preset-env to generate JS compatible with most-used browsers. 16 | // More info at https://babeljs.io/docs/en/next/babel-preset-env.html 17 | babel: { 18 | presets: [ 19 | ['@babel/preset-env', { 20 | modules: false, 21 | useBuiltIns: 'usage', 22 | corejs: 3 23 | }] 24 | ], 25 | } 26 | } 27 | 28 | // If we're running the webpack-dev-server, assume we're in development mode 29 | var isProduction = !process.argv.find(v => v.indexOf('webpack-dev-server') !== -1); 30 | console.log('Bundling for ' + (isProduction ? 'production' : 'development') + '...'); 31 | 32 | 33 | var isGitPod = process.env.GITPOD_INSTANCE_ID !== undefined; 34 | 35 | function getDevServerUrl() { 36 | if (isGitPod) { 37 | const url = execSync(`gp url ${CONFIG.devServerPort}`); 38 | return url.toString().trim(); 39 | } else { 40 | return `http://localhost:${CONFIG.devServerPort}`; 41 | } 42 | } 43 | 44 | module.exports = { 45 | entry: resolve(CONFIG.fsharpEntry), 46 | output: { 47 | path: resolve(CONFIG.outputDir), 48 | filename: 'bundle.js' 49 | }, 50 | mode: isProduction ? 'production' : 'development', 51 | devtool: isProduction ? 'source-map' : 'eval-source-map', 52 | plugins: isProduction ? [] : [new webpack.HotModuleReplacementPlugin()], 53 | devServer: { 54 | public: getDevServerUrl(), 55 | publicPath: '/public', 56 | contentBase: resolve(CONFIG.assetsDir), 57 | host: '0.0.0.0', 58 | allowedHosts: ['localhost', '.gitpod.io'], 59 | port: CONFIG.devServerPort, 60 | proxy: CONFIG.devServerProxy, 61 | hot: true, 62 | inline: true 63 | }, 64 | module: { 65 | rules: [ 66 | { 67 | test: /\.fs(x|proj)?$/, 68 | use: { 69 | loader: 'fable-loader', 70 | options: { 71 | babel: CONFIG.babel 72 | } 73 | } 74 | }, 75 | ] 76 | } 77 | }; 78 | 79 | function resolve(filePath) { 80 | return path.isAbsolute(filePath) ? filePath : path.join(__dirname, filePath); 81 | } 82 | -------------------------------------------------------------------------------- /Settings.FSharpLint: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | False 6 | 7 | 8 | -------------------------------------------------------------------------------- /build.fsx: -------------------------------------------------------------------------------- 1 | #r "nuget: Fable.PublishUtils, 2.4.0" 2 | 3 | open PublishUtils 4 | 5 | let args = 6 | fsi.CommandLineArgs 7 | |> Array.skip 1 8 | |> List.ofArray 9 | 10 | match args with 11 | | IgnoreCase "publish"::_ -> 12 | pushFableNuget "src/Fable.React.Types/Fable.React.Types.fsproj" [] doNothing 13 | pushFableNuget "src/Fable.ReactDom.Types/Fable.ReactDom.Types.fsproj" [] doNothing 14 | pushFableNuget "src/Fable.React/Fable.React.fsproj" [] doNothing 15 | | _ -> () 16 | -------------------------------------------------------------------------------- /docs/react-error-boundaries.md: -------------------------------------------------------------------------------- 1 | # How to use react error boundaries in fable 2 | 3 | When react renders the view and encounters an error it will by default just crash the complete application. 4 | If your html page only contains a single react element your page will be essentially a white page from this point and the user needs to reload the website. 5 | 6 | The problem is that a simple `try-catch` in the view will not be enough to handle errors in the render-pipeline. 7 | In order to catch these errors you need to overwrite `componentDidCatch`, this process is documented [here](https://reactjs.org/docs/error-boundaries.html) 8 | 9 | While render-errors are rare when using fable it can happen, especially when embedding 3rd-party-components. 10 | Because of this we provide a general purpose ReactErrorBoundary component which can be added and used from your application (including elmish-architecture) 11 | 12 | 13 | ## ReactErrorBoundaries.fs 14 | 15 | Just copy the following code to your project: 16 | 17 | ```fsharp 18 | module ReactErrorBoundary 19 | 20 | open Fable.Core 21 | open Fable.React 22 | 23 | type [] InfoComponentObject = 24 | abstract componentStack: string with get 25 | 26 | type ErrorBoundaryProps = 27 | { Inner : React.ReactElement 28 | ErrorComponent : React.ReactElement 29 | OnError : exn * InfoComponentObject -> unit } 30 | 31 | type ErrorBoundaryState = 32 | { HasErrors : bool } 33 | 34 | // See https://github.com/MangelMaxime/Fulma/blob/master/docs/src/Widgets/Showcase.fs 35 | // See https://reactjs.org/docs/error-boundaries.html 36 | type ErrorBoundary(props) = 37 | inherit React.Component(props) 38 | do base.setInitState({ HasErrors = false }) 39 | 40 | override x.componentDidCatch(error, info) = 41 | let info = info :?> InfoComponentObject 42 | x.props.OnError(error, info) 43 | x.setState({ HasErrors = true }) 44 | 45 | override x.render() = 46 | if (x.state.HasErrors) then 47 | x.props.ErrorComponent 48 | else 49 | x.props.Inner 50 | 51 | let renderCatchSimple errorElement element = 52 | ofType { Inner = element; ErrorComponent = errorElement; OnError = fun _ -> () } [ ] 53 | 54 | let renderCatchFn onError errorElement element = 55 | ofType { Inner = element; ErrorComponent = errorElement; OnError = onError } [ ] 56 | ``` 57 | 58 | ## Usage 59 | 60 | Usage from an elmish application could look similar to this: 61 | 62 | Consider you have a `SubComponent` which might run into rendering issues and an `ErrorComponent` you want to show if a rendering issue occurs: 63 | 64 | ```fsharp 65 | let view model dispatch = 66 | SubComponent.view model dispatch 67 | |> ReactErrorBoundary.renderCatchFn 68 | (fun (error, info) -> 69 | //dispatch (MyMessage ...) 70 | logger.Error("SubComponent failed to render" + info.componentStack, error)) 71 | (ErrorComponent.view model dispatch) 72 | ``` 73 | 74 | If you don't need the `OnError` event: 75 | 76 | ```fsharp 77 | let view model dispatch = 78 | SubComponent.view model dispatch 79 | |> ReactErrorBoundary.renderCatchSimple 80 | (ErrorComponent.view model dispatch) 81 | ``` 82 | -------------------------------------------------------------------------------- /docs/server-side-rendering.md: -------------------------------------------------------------------------------- 1 | # Five steps to enable Server-Side Rendering in your [Elmish](https://github.com/fable-elmish/elmish) + [DotNet Core](https://github.com/dotnet/core) App! 2 | 3 | > [SSR Sample App](https://github.com/fable-compiler/fable-react/tree/master/SSRSample) based on [SAFE-Stack](https://github.com/SAFE-Stack/SAFE-BookStore) template is available! 4 | 5 | ## Introduction 6 | 7 | ### What is Server-Side Rendering (SSR) ? 8 | 9 | Commonly speaking SSR means the majority of your app's code can run on both the server and the client, it is also as known as "isomorphic app" or "universal app". In React, you can render your components to html on the server side (usually a nodejs server) by `ReactDOMServer.renderToString`, reuse the server-rendered html and bind events on the client side by `React.hydrate`. 10 | 11 | #### Pros 12 | 13 | * Better SEO, as the search engine crawlers will directly see the fully rendered page. 14 | * Faster time-to-content, especially on slow internet or slow devices. 15 | 16 | #### Cons 17 | 18 | * Development constraints, browser-specific code need add compile directives to ignore in the server. 19 | * More involved build setup and deployment requirements. 20 | * More server-side load. 21 | 22 | #### Conclusions 23 | 24 | While SSR looks pretty cool, it still adds more complexity to your app, and increases server-side load. But it could be really helpful in some cases like solving SEO issue in SPAs, improving time-to-content of mobile sites, etc. 25 | 26 | ### Server-Side Rendering in fable-react 27 | 28 | fable-react's SSR approach is a little different from those you see on the network, it is a **Pure F#** approach. It means you can render your elmish's view function directly on dotnet core, with all benefits of dotnet core runtime! 29 | 30 | There are lots of articles about comparing dotnet core and nodejs, I will only mention two main differences between F#/dotnet core and nodejs in SSR: 31 | 32 | * F# is a compiled language, which means it's generally considered faster then a dynamic language, like js. 33 | * Nodejs's single thread, event-driven, non-blocking I/O model works well in most web sites, but it is not good at CPU intensive tasks, including html rendering. Usually we need to run multi nodejs instances to take the advantage of multi-core systems. DotNet support non-blocking I/O (and `async/await` sugar), too. But the awesome part is that it also has pretty good support for multi-thread programming. 34 | 35 | In a simple test, rendering on dotnet core is about ~1.5x faster then nodejs (with ReactDOMServer.renderToString + NODE_ENV=production) in a single thread. You can find more detail in the bottom of this page. 36 | 37 | In a word, with this approach, you can not only get a better performance then nodejs, but also don't need the complexity of running and maintaining nodejs instances on your server! 38 | 39 | Here is a list of Fable.Helpers.React API that support server-side rendering: 40 | 41 | * HTML/CSS/SVG DSL function/unions, like `div`, `input`, `Style`, `Display`, `svg`, etc. 42 | * str/ofString/ofInt/ofFloat 43 | * ofOption/ofArray/ofList 44 | * fragment 45 | * ofType 46 | * ofFunction 47 | 48 | These don't support, but you can wrap it by `Fable.React.Isomorphic.isomorphicView` to skip or render a placeholder on the server: 49 | 50 | * ofImport 51 | 52 | ## Step 1: Reorganize your source files 53 | 54 | Separate all your elmish view and types to standalone files, like this: 55 | 56 | ```F# 57 | 58 | pages 59 | |-- Home 60 | |-- View.fs // contains view function. 61 | |-- Types.fs // contains msg and model type definitions, also should include init function. 62 | |-- State.fs // contains update function 63 | 64 | ``` 65 | 66 | View.fs and Types.fs will be shared between client and server. 67 | 68 | ## Step 2. Make sure shared files can be executed on the server side 69 | 70 | Some code that works in Fable might throw a runtime exception on dotnet core, we should be careful with unsafe type casting and add compiler directives to remove some code if necessary. 71 | 72 | Here are some hints about doing this: 73 | 74 | ### 1. Replace unsafe cast (unbox and `!!`) in your HTML attributes and CSS props with `HTMLAttr.Custom`, `SVGAttr.Custom` and `CSSProp.Custom` 75 | 76 | ```diff 77 | - div [ !!("class", "container") ] [] 78 | + div [ HTMLAttr.Custom ("class", "container") ] 79 | 80 | 81 | - div [ Style [ !!("class", "container") ] ] [] 82 | + div [ Style [ CSSProp.Custom("class", "container") ] ] [] 83 | 84 | - svg [ !!("width", 100) ] [] 85 | + svg [ SVGAttr.Custom("class", "container") ] [] 86 | ``` 87 | 88 | 89 | ### 2. Make sure your browser/js code won't be executed on the server side 90 | 91 | One big challenge of sharing code between client and server is that the server side has different API environment with client side. In this respect Fable + dotnet core's SSR is not much different than nodejs, except on dotnet core you should not only prevent browser's API call, but also js. 92 | 93 | Thanks for Fable Compiler's `FABLE_COMPILER` directive, we can easily distinguish it's running on client or server and execute different code in different environment: 94 | 95 | ```#F 96 | #if FABLE_COMPILER 97 | executeOnClient () 98 | #else 99 | executeOnServer () 100 | #endif 101 | ``` 102 | 103 | We also provide a help function in `Fable.Helpers.Isomorphic`, the definition is: 104 | 105 | ```F# 106 | let inline isomorphicExec clientFn serverFn input = 107 | #if FABLE_COMPILER 108 | clientFn input 109 | #else 110 | serverFn input 111 | #endif 112 | ``` 113 | 114 | Full example: 115 | 116 | ```diff 117 | open Fable.Core 118 | open Fable.Core.JS 119 | open Fable.React.Isomorphic 120 | open Browser 121 | 122 | // example code to add marquee effect to your document's title 123 | -window.setInterval( 124 | - fun () -> 125 | - document.title <- document.title.[1..len - 1] + document.title.[0..0], 126 | - 600 127 | -) 128 | 129 | 130 | +let inline clientFn () = 131 | + window.setInterval( 132 | + fun () -> 133 | + document.title <- document.title.[1..len - 1] + document.title.[0..0], 134 | + 600 135 | + ) 136 | +isomorphicExec clientFn ignore () 137 | ``` 138 | 139 | 140 | ### 3. Add a placeholder for components that cannot been rendered on the server side, like js native components. 141 | 142 | In `Fable.React.Isomorphic` we also implemented a help function (`isomorphicView`) to render a placeholder element for components that cannot be rendered on the server side, this function will also help [React.hydrate](https://reactjs.org/docs/react-dom.html#hydrate) to understand the differences between htmls rendered by client and server, so React won't treat it as a mistake and warn about it. 143 | 144 | ```diff 145 | open Fable.Core 146 | open Fable.Core.JS 147 | open Fable.React 148 | open Fable.React.Isomorphic 149 | open Browser 150 | 151 | type JsCompProps = { 152 | text: string 153 | } 154 | 155 | let jsComp (props: JsCompProps) = 156 | ofImport "default" "./jsComp" props [] 157 | 158 | -jsComp { text="I'm rendered by a js Component!" } 159 | 160 | +let jsCompServer (props: JsCompProps) = 161 | + div [] [ str "loading" ] 162 | + 163 | +isomorphicView jsComp jsCompServer { text="I'm rendered by a js Component!" } 164 | ``` 165 | 166 | ## Step 3. Create your initial state on the server side. 167 | 168 | On the server side, you can create routes like normal MVC app, just make sure the model passed to server-side rendering function is exactly match the model on the client side in current route. 169 | 170 | Here is an example: 171 | 172 | ```F# 173 | 174 | open Giraffe 175 | open Giraffe.GiraffeViewEngine 176 | open FableJson 177 | 178 | let initState: Model = { 179 | counter = Some 42 180 | someString = "Some String" 181 | someFloat = 11.11 182 | someInt = 22 183 | } 184 | 185 | let renderHtml () = 186 | // This would render the html by model create on the server side. 187 | // Note in an Elmish app, view function takes two parameters, 188 | // the first is model, and the second is dispatch, 189 | // which simple ignored here because React will bind event handlers for you on the client side. 190 | let htmlStr = Fable.Helpers.ReactServer.renderToString(Client.View.view initState ignore) 191 | 192 | // We also need to pass the model to Elmish and React by print a json string in html to let them know what's the model that used to rendering the html. 193 | // Note we call ofJson twice here, 194 | // because Elmish's model can contains some complicate type instead of pojo, 195 | // the first one will seriallize the state to json string, 196 | // and the second one will seriallize the json string to a legally js string, 197 | // so we can deseriallize it by Fable's ofJson and get the correct types. 198 | let stateJsonStr = toJson (toJson initState) 199 | 200 | html [] 201 | [ head [] [] 202 | body [] 203 | [ div [_id "elmish-app"] [ rawText htmlStr ] 204 | script [] 205 | [ rawText (sprintf """ 206 | var __INIT_STATE__ = %s 207 | """ stateJsonStr) ] // 208 | script [ _src (assetsBaseUrl + "/public/bundle.js") ] [] 209 | ] 210 | ] 211 | ``` 212 | 213 | ## Step 4. Update your elmish app's init function 214 | 215 | 1. Initialize your elmish app by state printed in the HTML. 216 | 2. Remove initial commands that fetch state which already included in the HTML. 217 | 218 | e.g. 219 | 220 | ```F# 221 | let init () = 222 | // Init model by server side state 223 | let model = ofJson !!window?__INIT_STATE__ 224 | // let cmd = 225 | // Cmd.ofPromise 226 | // (fetchAs "/api/init") 227 | // [] 228 | // (Ok >> Init) 229 | // (Error >> Init) 230 | model, Cmd.none 231 | ``` 232 | 233 | ## Step 5. Using React.hydrate to render your app 234 | 235 | ```diff 236 | Program.mkProgram init update view 237 | #if DEBUG 238 | |> Program.withConsoleTrace 239 | |> Program.withHMR 240 | #endif 241 | -|> Program.withReact "elmish-app" 242 | +|> Program.withReactHydrate "elmish-app" 243 | #if DEBUG 244 | |> Program.withDebugger 245 | #endif 246 | |> Program.run 247 | ``` 248 | 249 | Now enjoy! If you find bugs or just need some help, please create an issue and let us know, thanks! 250 | 251 | ## Try the sample app 252 | 253 | ```sh 254 | git clone https://github.com/fable-compiler/fable-react.git 255 | cd ./fable-react/SSRSample/ 256 | ./build.sh run # or ./build.cmd run on windows 257 | ``` 258 | 259 | ## Run simple benchmark test in sample app 260 | 261 | The SSRSample project also contains a simple benchmark test, you can try it in you computer by: 262 | 263 | ```sh 264 | 265 | cd ./SSRSample 266 | ./build.sh bench # or ./build.cmd bench on windows 267 | 268 | ``` 269 | 270 | Here is the benchmark result on a linux laptop (Intel Core i7-3630QM, 8 core), rendering on dotnet core is about ~1.5x faster then on nodejs in a single thread. To take the advantage of multi-core systems, we also tested with multi-thread on dotnet core and cluster mode in nodejs, the dotnet core version is still faster then nodejs version, but not much. I guess it's because multi-process takes more advantages from multi cores then multi-threaded. What's more, multi-threaded dotnet has less memory footprint. 271 | 272 | ```sh 273 | 274 | dotnet ./bin/Release/netcoreapp2.0/dotnet.dll 275 | Thread 1 started 276 | Thread 1 render 160000 times used 23062ms 277 | [Single thread] 23062ms 6937.820req/s 278 | Thread 1 started 279 | Thread 3 started 280 | Thread 4 started 281 | Thread 6 started 282 | Thread 5 started 283 | Thread 7 started 284 | Thread 10 started 285 | Thread 9 started 286 | Thread 3 render 20000 times used 9593ms 287 | Thread 5 render 20000 times used 9689ms 288 | Thread 10 render 20000 times used 9693ms 289 | Thread 9 render 20000 times used 9705ms 290 | Thread 4 render 20000 times used 9720ms 291 | Thread 1 render 20000 times used 9753ms 292 | Thread 7 render 20000 times used 9757ms 293 | Thread 6 render 20000 times used 9795ms 294 | [8 tasks] Total: 9713ms Memory footprint: 44.063MB Requests/sec: 16472.768 295 | 296 | /usr/local/bin/node ./node.js 297 | Master 10891 is running 298 | [Single process] 34322ms 4661.733req/s 299 | Worker 10911: started 300 | Worker 10916: started 301 | Worker 10928: started 302 | Worker 10942: started 303 | Worker 10930: started 304 | Worker 10935: started 305 | Worker 10922: started 306 | Worker 10951: started 307 | Worker 10911: render 20000 times used 11394ms 308 | Worker 10935: render 20000 times used 11353ms 309 | Worker 10928: render 20000 times used 11522ms 310 | Worker 10922: render 20000 times used 11492ms 311 | Worker 10916: render 20000 times used 11812ms 312 | Worker 10930: render 20000 times used 11913ms 313 | Worker 10951: render 20000 times used 11781ms 314 | Worker 10942: render 20000 times used 12236ms 315 | [8 workers] Total: 11687.875ms Memory footprint: 200.066MB Requests/sec: 13689.400 316 | 317 | ``` 318 | -------------------------------------------------------------------------------- /docs/using-third-party-react-components.md: -------------------------------------------------------------------------------- 1 | ## Using third party React components 2 | 3 | Using a third party (Javascript) React component is straightforward for most components. There are three ways of declaring a third party React component in F# - either by declaring a Discriminated Union where each case has one field; by declaring a record type for the props with the Pojo attribute; or by using an untyped list of `(string * obj)` tuples. All three ways are described below. 4 | 5 | Some components have a [Typescript](https://www.typescriptlang.org/) definition available, either because the component was authored in Typescript or someone created a type definition for the [Definitely Typed project](https://definitelytyped.org/). If this is the case then you can try the [ts2fable tool](https://github.com/fable-compiler/ts2fable) to convert this React component type definition from Typescript to a Fable type declaration - it might need some tweaking but for components with a big API surface this can be a real time saver. 6 | 7 | ## Table of contents 8 | 9 | 10 | 11 | - [Using third party React components](#using-third-party-react-components) 12 | - [Table of contents](#table-of-contents) 13 | - [Using a React component by declaring a Discriminated Union props type](#using-a-react-component-by-declaring-a-discriminated-union-props-type) 14 | - [1. Install the react component](#1-install-the-react-component) 15 | - [2. Define the props type](#2-define-the-props-type) 16 | - [3. Define the React component creation function](#3-define-the-react-component-creation-function) 17 | - [Member Import](#member-import) 18 | - [Default Import](#default-import) 19 | - [Fields of imported items](#fields-of-imported-items) 20 | - [Directly creating the element](#directly-creating-the-element) 21 | - [4. Use the creation function in your view code](#4-use-the-creation-function-in-your-view-code) 22 | - [5. Get component state back into your code](#5-get-component-state-back-into-your-code) 23 | - [Importing using a Pojo (plain old JS object) record](#importing-using-a-pojo-plain-old-js-object-record) 24 | - [Passing in props as tuples (without a type declaration of the props)](#passing-in-props-as-tuples-without-a-type-declaration-of-the-props) 25 | - [Edgecases](#edgecases) 26 | 27 | 28 | 29 | ## Using a React component by declaring a Discriminated Union props type 30 | 31 | The basic steps when working with a Discriminated Union are: 32 | 33 | ### 1. Install the react component 34 | 35 | Using yarn or npm, install the react component you want to use. 36 | 37 | For example to use the [rc-progress](https://github.com/react-component/progress) React component which we'll be using in this tutorial, run the following command inside your Fable project root folder: 38 | 39 | ```bash 40 | yarn add rc-progress 41 | ``` 42 | 43 | ### 2. Define the props type 44 | 45 | Reference the **documentation of the React component** to find out which props the component supports and declare them as an F# type (see below for the two supported mechanisms). You can define only a subset of supported props in F# if you don't need to cover the full props options that the React component supports. 46 | 47 | For example to expose the percent, strokeWidth and strokeColor props of the rc-progress components: 48 | 49 | ```fsharp 50 | type ProgressProps = 51 | | Percent of int 52 | | StrokeWidth of int 53 | | StrokeColor of string 54 | ``` 55 | 56 | If one of the props is treated as a string enum in Javascript (e.g. if there is a size prop with the supported values "small", "normal" and "big"), then the `[]` attribute can be very useful for defining helper types (see the [StringEnum docs](http://fable.io/docs/interacting.html#stringenum-attribute) for more info): 57 | 58 | ```fsharp 59 | [] 60 | type Size = 61 | | Small 62 | | Normal 63 | | Big 64 | 65 | type SomeComponentProps = 66 | | Size of Size 67 | | ... 68 | ``` 69 | 70 | ### 3. Define the React component creation function 71 | 72 | There are several different ways to declare exports in Javascript (default imports, member imports, namespace imports); depending on how the Javascript React component was declared, you have to choose the right import. Refer to the [Fable docs](http://fable.io/docs/interacting.html#importing-javascript-code) for more information on imports. 73 | 74 | Using the `ofImport` function you instruct Fable which component should be instantiated when the creation function is called. 75 | 76 | #### Member Import 77 | 78 | In the example of rc-progress, to declare a `progressLine` creation function that imports the `Line` component from the library `rc-progress`, you would declare it as follows. 79 | 80 | ```fsharp 81 | open Fable.Core 82 | open Fable.Core.JsInterop 83 | open Fable.React 84 | open Fable.React.Props 85 | 86 | let inline progressLine (props : ProgressProps list) (elems : ReactElement list) : ReactElement = 87 | ofImport "Line" "rc-progress" (keyValueList CaseRules.LowerFirst props) elems 88 | ``` 89 | 90 | The `keyValueList` function is used to convert the props of type `IProgressProps list` to a JavaScript object where the key is the lower case name of the discriminated union case identifier and the value is the field value of the discriminated union (e.g. if the list that is passed into the function is `[Percent 40; StrokeColor "red"]`, the Javascript object that will be passed to the `props` of the `Line` react component would look like this: `{ percent: 40, strokeColor: "red" }`) 91 | 92 | In the docs of the [rc-progress](https://github.com/react-component/progress) React component the import style used is a *member import* (e.g. `import { Line, Circle } from 'rc-progress';`), so we refer to the component member `Line` directly in the ofImport expression. 93 | 94 | #### Default Import 95 | 96 | If the export is declard as a default export, then you would use ``"default"`` as the member name. 97 | Taking [react-native-qrcode-scanner](https://github.com/moaazsidat/react-native-qrcode-scanner) as an example: 98 | 99 | To translate the example 100 | 101 | ```js 102 | import QRCodeScanner from 'react-native-qrcode-scanner'; 103 | ``` 104 | 105 | you would declare your function like 106 | 107 | ```fsharp 108 | let inline qr_code_scanner (props : QRCodeScannerProps list) : ReactElement = 109 | ofImport "default" "react-native-qrcode-scanner" (keyValueList CaseRules.LowerFirst props) [] 110 | ``` 111 | 112 | #### Fields of imported items 113 | 114 | Some React components must be instantiated as follows in JS: 115 | 116 | ```js 117 | import { Select } from 'react-select' 118 | let render = () => 119 | ``` 120 | 121 | In this case, you can also use `ofImport` to directly access the field of the imported item: 122 | 123 | ```fsharp 124 | // Import { Select } from "react-select" and then access the "Creatable" field 125 | ofImport "Select.Creatable" "react-select" myOptions [] 126 | 127 | // Also compatible with default imports 128 | ofImport "default.Creatable" "react-select" myOptions [] 129 | ``` 130 | 131 | #### Directly creating the element 132 | 133 | If you already have a reference to the imported component, then you can also use ``createElement``. 134 | 135 | The default import above could also be rewritten like this: 136 | 137 | ```fsharp 138 | let rnqs = importDefault "react-native-qrcode-scanner" 139 | createElement(rnqs, (keyValueList CaseRules.LowerFirst props), []) 140 | ``` 141 | 142 | > Please note it's also OK to duplicate `ofImport` with same member and path. In this case, Fable will automatically group the imports. 143 | 144 | ```fsharp 145 | let foo1 = ofImport "default" "react-foo" { height = 25 } [] 146 | let foo2 = ofImport "default" "react-foo" { height = 50 } [] 147 | ``` 148 | 149 | ### 4. Use the creation function in your view code 150 | 151 | The function you declared in step 2 now behaves just like any other React component function. 152 | 153 | To use the component in a [Fable-Elmish](https://fable-elmish.github.io/elmish/) view function: 154 | 155 | ```fsharp 156 | let view (model : Model) (dispatch : Msg -> unit) = 157 | div 158 | [] 159 | [ progressLine [ Percent model.currentProgress; StrokeColor "red" ] [] ] 160 | ``` 161 | 162 | ### 5. Get component state back into your code 163 | 164 | If you want to get from your component state back in F# code, you need to follow react documentation : 165 | https://reactjs.org/docs/lifting-state-up.html to propagate component state to upper components. 166 | 167 | Insert a function in your props to get state back. 168 | ```fsharp 169 | // Define function in props 170 | type ComponentProps = 171 | | GetState of (DateTime -> unit) 172 | | ... 173 | 174 | // sample function matching props signature 175 | let logDateTimeSelected (d : DateTime) = 176 | printfn "Date : %A" d 177 | 178 | // Component definition (here a calendar) 179 | let inline Calendar (props : ComponentProps list) : Fable.React.ReactElement = 180 | ofImport "default" "./CalendarComponent.tsx" (keyValueList CaseRules.LowerFirst props) [] 181 | 182 | // Component initialization with props 183 | div [] [ 184 | Calendar [ GetState logDateTimeSelected ] 185 | ] 186 | ``` 187 | 188 | On Javascript/TypeScript side, you need to use this props within your component 189 | ```js 190 | // Define component props 191 | export interface IComponentProps { 192 | // new props matching F# ComponentProps definition 193 | getState : ((date : Date) => void) 194 | ... 195 | } 196 | 197 | // within JS/TS event handler use your props function 198 | private _onSelectDate(date: Date): void { 199 | this.props.getState(date) 200 | ... 201 | } 202 | ``` 203 | 204 | ## Importing using a record 205 | 206 | This is similar to the approach above, but instead of declaring a DU you create a record. Using a record to express the props looks more like idiomatic F# code but it can be unwieldy if you have a lot of optional props. Since this is common with React components, using the DU approach above can often be more convenient. 207 | 208 | ```fsharp 209 | type ProgressProps = 210 | { percent : int 211 | strokeWidth : int 212 | strokeColor : string 213 | } 214 | 215 | let inline progressLine (props : ProgressProps) (elems : ReactElement list) : ReactElement = 216 | ofImport "Line" "rc-progress" props elems 217 | ``` 218 | 219 | ## Untyped props 220 | 221 | The third way of using a React component is to not give an F# type to the Props at all and simply pass a list of `(string * obj)` tuples to the `createObj` helper function which turns the list into a Javascript object and passes it as the props of the React component. This of course has the least level of type safety but it can be convenient for prototyping. The `==>` operator is defined in the [Fable.Core.JsInterop](https://fable.io/docs/communicate/js-from-fable.html#Plain-Old-JavaScript-Objects) module to make `(string * obj)` tuple creation easier to read. 222 | 223 | ```fsharp 224 | open Fable.Core.JsInterop 225 | 226 | ofImport "Line" "rc-progress" (createObj ["strokeWidth" ==> 5]) [] 227 | 228 | // You can also use anonymous records 229 | ofImport "Line" "rc-progress" {| strokeWidth = 5 |} [] 230 | ``` 231 | 232 | ## Edge cases 233 | 234 | This documentation needs to be extended to cover [Higher Order Components](https://reactjs.org/docs/higher-order-components.html) and maybe [Context](https://reactjs.org/docs/context.html), [Fragments](https://reactjs.org/docs/fragments.html) etc. Contributions are welcome! 235 | -------------------------------------------------------------------------------- /fable_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fable-compiler/fable-react/7ca5a7f9645e578aefcf244244047137aba36b13/fable_logo.png -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "9.0.200", 4 | "rollForward": "latestMinor" 5 | } 6 | } -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fable-react", 3 | "lockfileVersion": 2, 4 | "requires": true, 5 | "packages": {} 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "publish": "dotnet fsi build.fsx publish", 5 | "ssrsample-start": "webpack-dev-server --config SSRSample/webpack.config.js", 6 | "ssrsample-build": "webpack --config SSRSample/webpack.config.js", 7 | "ssrsample-build-lib": "fable-splitter SSRSample/src/Client/Client.fsproj -o SSRSample/src/Client/bin/lib --debug --commonjs" 8 | }, 9 | "dependencies": { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Fable.React.Types/Fable.React.Extensions.fs: -------------------------------------------------------------------------------- 1 | [] 2 | module Fable.React.Extensions 3 | 4 | open Fable.Core 5 | 6 | type Browser.Types.Event with 7 | /// Access the value from target 8 | /// Equivalent to `(this.target :?> HTMLInputElement).value` 9 | [] 10 | member this.Value: string = 11 | (this.target :?> Browser.Types.HTMLInputElement).value 12 | 13 | /// Access the checked property from target 14 | /// Equivalent to `(this.target :?> HTMLInputElement).checked` 15 | [] 16 | member this.Checked: bool = 17 | (this.target :?> Browser.Types.HTMLInputElement).``checked`` 18 | -------------------------------------------------------------------------------- /src/Fable.React.Types/Fable.React.Hooks.fs: -------------------------------------------------------------------------------- 1 | namespace Fable.React 2 | 3 | open Fable.Core 4 | 5 | type IStateHook<'T> = 6 | [] 7 | abstract current: 'T 8 | [] 9 | abstract update: 'T -> unit 10 | [] 11 | abstract update: ('T -> 'T) -> unit 12 | 13 | // Alias kept for backwards compatibility 14 | type IRefHook<'T> = IRefValue<'T> 15 | 16 | type IReducerHook<'State,'Msg> = 17 | [] 18 | abstract current: 'State 19 | [] 20 | abstract update: 'Msg -> unit 21 | 22 | type ITransitionHook = 23 | [] 24 | abstract isPending: bool 25 | [] 26 | abstract startTransition: callback: (unit -> unit) -> unit 27 | 28 | type IHooks = 29 | /// Returns the current state with a function to update it. 30 | /// More info at https://reactjs.org/docs/hooks-reference.html#usestate 31 | abstract useState: initialState: 'T -> IStateHook<'T> 32 | 33 | /// Returns the current state with a function to update it. 34 | /// More info at https://reactjs.org/docs/hooks-reference.html#usestate 35 | [] 36 | abstract useStateLazy: initialState: (unit->'T) -> IStateHook<'T> 37 | 38 | /// Accepts a function that contains imperative, possibly effectful code. 39 | /// More info at https://reactjs.org/docs/hooks-reference.html#useeffect 40 | abstract useEffect: effect: (unit->unit) * ?dependencies: obj[] -> unit 41 | 42 | /// Accepts a function that contains imperative, possibly effectful code. 43 | /// The signature is identical to useEffect, but it fires synchronously after 44 | /// all DOM mutations. Use this to read layout from the DOM and synchronously 45 | /// re-render. Updates scheduled inside useLayoutEffect will be flushed 46 | /// synchronously, before the browser has a chance to paint. 47 | /// More info at https://reactjs.org/docs/hooks-reference.html#uselayouteffect 48 | abstract useLayoutEffect: effect: (unit->unit) * ?dependencies: obj[] -> unit 49 | 50 | /// Accepts a function that contains effectful code and returns a disposable for clean-up 51 | /// More info at https://reactjs.org/docs/hooks-reference.html#useeffect 52 | [ { 53 | const disp = $1(); 54 | return () => disp.Dispose(); 55 | }{{, $2}})""")>] 56 | abstract useEffectDisposable: effect: (unit->System.IDisposable) * ?dependencies: obj[] -> unit 57 | 58 | // abstract useCallback (callback: unit -> unit, dependencies: obj[]): unit -> unit 59 | 60 | /// Accepts a "create" function and an array of dependencies and returns a memoized value 61 | /// More info at https://reactjs.org/docs/hooks-reference.html#usememo 62 | abstract useMemo: callback: (unit->'T) * dependencies: obj[] -> 'T 63 | 64 | /// The returned object will persist for the full lifetime of the component. 65 | /// More info at https://reactjs.org/docs/hooks-reference.html#useref 66 | abstract useRef: initialValue: 'T -> IRefValue<'T> 67 | 68 | /// Accepts a context object (the value returned from createContext) and 69 | /// returns the current context value for that context. The current context 70 | /// value is determined by the value prop of the nearest 71 | /// above the calling component in the tree. 72 | /// More info at https://reactjs.org/docs/hooks-reference.html#usecontext 73 | abstract useContext: ctx: IContext<'T> -> 'T 74 | 75 | /// Display a label for custom hooks in React DevTools. 76 | /// More info at https://reactjs.org/docs/hooks-reference.html#usedebugvalue 77 | abstract useDebugValue: label: string -> unit 78 | 79 | /// Defers formatting of debug value until the Hook is actually inspected 80 | /// More info at https://reactjs.org/docs/hooks-reference.html#usedebugvalue 81 | abstract useDebugValue: value: 'T * format: ('T->string) -> unit 82 | 83 | /// An alternative to useState. Accepts a reducer of type (state, action) => newState, and returns the current state paired with a dispatch method. 84 | /// More info at https://reactjs.org/docs/hooks-reference.html#usereducer 85 | abstract useReducer: reducer: ('State -> 'Msg -> 'State) * initialState: 'State -> IReducerHook<'State, 'Msg> 86 | 87 | /// An alternative to useState. Accepts a reducer of type (state, action) => newState, and returns the current state paired with a dispatch method. 88 | /// More info at https://reactjs.org/docs/hooks-reference.html#usereducer 89 | abstract useReducer: reducer: ('State -> 'Msg -> 'State) * initialArg: 'I * init: ('I -> 'State) -> IReducerHook<'State, 'Msg> 90 | 91 | /// Returns a stateful value for the pending state of the transition, and a function to start it. 92 | /// More info at https://reactjs.org/docs/hooks-reference.html#usetransition 93 | /// Requires React 18. 94 | abstract useTransition: unit -> ITransitionHook 95 | 96 | /// useId is a hook for generating unique IDs that are stable across the server and client, while avoiding hydration mismatches. 97 | /// More info at https://reactjs.org/docs/hooks-reference.html#useid 98 | /// Requires React 18. 99 | abstract useId: unit -> string 100 | 101 | /// useDeferredValue accepts a value and returns a new copy of the value that will defer to more urgent updates. If the current render is the result of an urgent update, like user input, React will return the previous value and then render the new value after the urgent render has completed. 102 | /// More info at https://reactjs.org/docs/hooks-reference.html#usedeferredvalue 103 | /// Requires React 18. 104 | abstract useDeferredValue: 'T -> 'T 105 | 106 | [] 107 | module HookBindings = 108 | let private makeDummyStateHook value = 109 | { new IStateHook<'T> with 110 | member __.current = value 111 | member __.update(x: 'T) = () 112 | member __.update(f: 'T->'T) = () } 113 | 114 | let private makeDummyReducerHook state = 115 | { new IReducerHook<'State,'Msg> with 116 | member __.current = state 117 | member __.update(msg: 'Msg) = () } 118 | 119 | let private makeDummyTransitionHook () = 120 | { new ITransitionHook with 121 | member __.isPending = false 122 | member __.startTransition callback = () } 123 | 124 | #if FABLE_REPL_LIB 125 | [] 126 | #else 127 | [] 128 | #endif 129 | let Hooks: IHooks = 130 | // Placeholder for SSR 131 | { new IHooks with 132 | member __.useState(initialState: 'T) = 133 | makeDummyStateHook initialState 134 | member __.useStateLazy(initialState) = 135 | makeDummyStateHook (initialState()) 136 | member __.useEffect(effect, dependencies) = () 137 | member __.useEffectDisposable(effect, dependencies) = () 138 | member __.useMemo(callback, dependencies) = callback() 139 | member __.useRef(initialValue) = 140 | { new IRefValue<_> with 141 | member __.current with get() = initialValue and set _ = () } 142 | member __.useContext ctx = 143 | (ctx :?> ISSRContext<_>).DefaultValue 144 | member __.useDebugValue(label): unit = () 145 | member __.useDebugValue(value, format): unit = () 146 | member __.useReducer(reducer,initialState) = makeDummyReducerHook initialState 147 | member __.useReducer(reducer, initialArgument, init) = makeDummyReducerHook (init initialArgument) 148 | member __.useTransition() = makeDummyTransitionHook() 149 | member __.useDeferredValue value = value 150 | member __.useId() = "" 151 | member __.useLayoutEffect(effect, dependencies) = () 152 | } -------------------------------------------------------------------------------- /src/Fable.React.Types/Fable.React.Types.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19.0.0 5 | 19.0.0-alpha.1 6 | netstandard2.0 7 | enable 8 | 9 | fsharp;fable;javascript;f#;js;react;fable-binding;fable-javascript 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/Fable.React.Types/Fable.React.fs: -------------------------------------------------------------------------------- 1 | namespace Fable.React 2 | 3 | open System 4 | open Fable.Core 5 | 6 | // The import statement on the type interface are here so 7 | // people using Fable => TypeScript can get the correct types in the output 8 | // without having to use hack via `unbox` in their code 9 | 10 | [] 11 | type [] ReactElement = 12 | interface end 13 | 14 | [] 15 | type ReactElementType = 16 | interface end 17 | 18 | [] 19 | type ReactElementType<'props> = 20 | inherit ReactElementType 21 | 22 | type IRefValue<'T> = 23 | abstract current: 'T with get, set 24 | 25 | type IContext<'T> = 26 | interface end 27 | 28 | type ISSRContext<'T> = 29 | inherit IContext<'T> 30 | abstract DefaultValue: 'T 31 | 32 | type IReactExports = 33 | /// Create and return a new React element of the given type. The type argument can be either a tag name string (such as 'div' or 'span'), a React component type (a class or a function), or a React fragment type. 34 | abstract createElement: comp: obj * props: objnull * [] children: ReactElement seq -> ReactElement 35 | 36 | /// Creates a Context object. When React renders a component that subscribes to this Context object it will read the current context value from the closest matching Provider above it in the tree. 37 | abstract createContext: defaultValue: 'T -> IContext<'T> 38 | 39 | /// React.createRef creates a ref that can be attached to React elements via the ref attribute. 40 | abstract createRef: initialValue: 'T -> IRefValue<'T> 41 | 42 | /// React.forwardRef creates a React component that forwards the ref attribute it receives to another component below in the tree. 43 | abstract forwardRef: fn: ('props -> IRefValue<'T> option -> ReactElement) -> ReactElementType<'props> 44 | 45 | /// If your component renders the same result given the same props, you can wrap it in a call to React.memo for a performance boost in some cases by memoizing the result. This means that React will skip rendering the component, and reuse the last rendered result. 46 | abstract memo: render: ('props -> ReactElement) * areEqual: ('props -> 'props -> bool) -> ReactElementType<'props> 47 | 48 | /// The React.Fragment component lets you return multiple elements in a render() method without creating an additional DOM element. 49 | abstract Fragment: ReactElementType 50 | 51 | /// React.Suspense lets you specify the loading indicator in case some components in the tree below it are not yet ready to render. In the future we plan to let Suspense handle more scenarios such as data fetching. 52 | abstract Suspense: ReactElementType 53 | 54 | /// React.lazy() lets you define a component that is loaded dynamically. This helps reduce the bundle size to delay loading components that aren’t used during the initial render. 55 | abstract ``lazy``: f: (unit -> JS.Promise<'TIn>) -> 'TOut 56 | 57 | /// React.startTransition lets you mark updates inside the provided callback as transitions. This method is designed to be used when React.useTransition is not available. 58 | /// Requires React 18. 59 | abstract startTransition: callback: (unit -> unit) -> unit 60 | 61 | /// The React version. 62 | abstract version: string 63 | 64 | module ReactBindings = 65 | /// Mainly intended for internal use 66 | #if FABLE_REPL_LIB 67 | [] 68 | #else 69 | [] 70 | #endif 71 | let React: IReactExports = jsNative 72 | 73 | /// Create a React component by inheriting this class as follows 74 | /// 75 | /// ``` 76 | /// type MyComponent(initialProps) = 77 | /// inherit React.Component(initialProps) 78 | /// base.setInitState({ value = 5 }) 79 | /// 80 | /// override this.render() = 81 | /// // Don't use captured `initialProps` from constructor, 82 | /// // use `this.props` instead (updated version) 83 | /// let msg = sprintf "Hello %s, you have %i €" 84 | /// this.props.name this.state.value 85 | /// div [] [ofString msg] 86 | /// ``` 87 | type [] Component<'P,'S>(initProps: 'P) = 88 | [] 89 | member __.props: 'P = initProps 90 | 91 | [] 92 | member val children: ReactElement array = [| |] with get, set 93 | 94 | [] 95 | member val state: 'S = Unchecked.defaultof<'S> with get, set 96 | 97 | /// ATTENTION: Within the constructor, use `setInitState` 98 | /// Enqueues changes to the component state and tells React that this component and its children need to be re-rendered with the updated state. This is the primary method you use to update the user interface in response to event handlers and server responses. 99 | /// Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately. 100 | /// setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall. Instead, use componentDidUpdate or a setState callback (setState(updater, callback)), either of which are guaranteed to fire after the update has been applied. If you need to set the state based on the previous state, read about the updater argument below. 101 | /// setState() will always lead to a re-render unless shouldComponentUpdate() returns false. If mutable objects are being used and conditional rendering logic cannot be implemented in shouldComponentUpdate(), calling setState() only when the new state differs from the previous state will avoid unnecessary re-renders. 102 | ['P->'S) instead.")>] 103 | [] 104 | member x.setState(value: 'S): unit = x.state <- value 105 | 106 | /// Overload of `setState` accepting updater function with the signature: `(prevState, props) => stateChange` 107 | /// prevState is a reference to the previous state. It should not be directly mutated. Instead, changes should be represented by building a new object based on the input from prevState and props. 108 | /// Both prevState and props received by the updater function are guaranteed to be up-to-date. The output of the updater is shallowly merged with prevState. 109 | [] 110 | member x.setState(updater: 'S->'P->'S): unit = x.state <- updater x.state x.props 111 | 112 | /// This method can only be called in the constructor 113 | [] 114 | member x.setInitState(value: 'S): unit = x.state <- value 115 | 116 | /// By default, when your component’s state or props change, your component will re-render. If your render() method depends on some other data, you can tell React that the component needs re-rendering by calling forceUpdate(). 117 | /// Calling forceUpdate() will cause render() to be called on the component, skipping shouldComponentUpdate(). This will trigger the normal lifecycle methods for child components, including the shouldComponentUpdate() method of each child. React will still only update the DOM if the markup changes. 118 | /// Normally you should try to avoid all uses of forceUpdate() and only read from this.props and this.state in render(). 119 | [] 120 | member __.forceUpdate(?callBack: unit->unit): unit = () 121 | 122 | [] 123 | member __.isMounted(): bool = false 124 | 125 | /// Invoked immediately before mounting occurs. It is called before render(), therefore calling setState() synchronously in this method will not trigger an extra rendering. Generally, we recommend using the constructor() instead. 126 | /// Avoid introducing any side-effects or subscriptions in this method. For those use cases, use componentDidMount() instead. 127 | /// This is the only lifecycle hook called on server rendering. 128 | abstract componentWillMount: unit -> unit 129 | default __.componentWillMount () = () 130 | 131 | /// Invoked immediately after a component is mounted. Initialization that requires DOM nodes should go here. If you need to load data from a remote endpoint, this is a good place to instantiate the network request. 132 | /// This method is a good place to set up any subscriptions. If you do that, don’t forget to unsubscribe in componentWillUnmount(). 133 | /// Calling setState() in this method will trigger an extra rendering, but it is guaranteed to flush during the same tick. This guarantees that even though the render() will be called twice in this case, the user won’t see the intermediate state. Use this pattern with caution because it often causes performance issues. It can, however, be necessary for cases like modals and tooltips when you need to measure a DOM node before rendering something that depends on its size or position. 134 | abstract componentDidMount: unit -> unit 135 | default __.componentDidMount () = () 136 | 137 | /// Invoked before a mounted component receives new props. If you need to update the state in response to prop changes (for example, to reset it), you may compare this.props and nextProps and perform state transitions using this.setState() in this method. 138 | /// Note that React may call this method even if the props have not changed, so make sure to compare the current and next values if you only want to handle changes. This may occur when the parent component causes your component to re-render. 139 | /// React doesn’t call componentWillReceiveProps() with initial props during mounting. It only calls this method if some of component’s props may update. Calling this.setState() generally doesn’t trigger componentWillReceiveProps(). 140 | abstract componentWillReceiveProps: nextProps: 'P -> unit 141 | default __.componentWillReceiveProps (_) = () 142 | 143 | /// Use shouldComponentUpdate() to let React know if a component’s output is not affected by the current change in state or props. The default behavior is to re-render on every state change, and in the vast majority of cases you should rely on the default behavior. 144 | /// shouldComponentUpdate() is invoked before rendering when new props or state are being received. Defaults to true. This method is not called for the initial render or when forceUpdate() is used. 145 | /// Returning false does not prevent child components from re-rendering when their state changes. 146 | /// Currently, if shouldComponentUpdate() returns false, then componentWillUpdate(), render(), and componentDidUpdate() will not be invoked. Note that in the future React may treat shouldComponentUpdate() as a hint rather than a strict directive, and returning false may still result in a re-rendering of the component. 147 | /// If you determine a specific component is slow after profiling, you may change it to inherit from React.PureComponent which implements shouldComponentUpdate() with a shallow prop and state comparison. If you are confident you want to write it by hand, you may compare this.props with nextProps and this.state with nextState and return false to tell React the update can be skipped. 148 | /// We do not recommend doing deep equality checks or using JSON.stringify() in shouldComponentUpdate(). It is very inefficient and will harm performance. 149 | abstract shouldComponentUpdate: nextProps: 'P * nextState: 'S -> bool 150 | default __.shouldComponentUpdate (_, _) = true 151 | 152 | /// Invoked immediately before rendering when new props or state are being received. Use this as an opportunity to perform preparation before an update occurs. This method is not called for the initial render. 153 | /// Note that you cannot call this.setState() here; nor should you do anything else (e.g. dispatch a Redux action) that would trigger an update to a React component before componentWillUpdate() returns. 154 | /// If you need to update state in response to props changes, use componentWillReceiveProps() instead. 155 | /// > componentWillUpdate() will not be invoked if shouldComponentUpdate() returns false. 156 | abstract componentWillUpdate: nextProps: 'P * nextState: 'S -> unit 157 | default __.componentWillUpdate (_, _) = () 158 | 159 | /// Invoked immediately after updating occurs. This method is not called for the initial render. 160 | /// Use this as an opportunity to operate on the DOM when the component has been updated. This is also a good place to do network requests as long as you compare the current props to previous props (e.g. a network request may not be necessary if the props have not changed). 161 | /// > componentDidUpdate() will not be invoked if shouldComponentUpdate() returns false. 162 | abstract componentDidUpdate: prevProps: 'P * prevState: 'S -> unit 163 | default __.componentDidUpdate (_, _) = () 164 | 165 | /// Invoked immediately before a component is unmounted and destroyed. Perform any necessary cleanup in this method, such as invalidating timers, canceling network requests, or cleaning up any subscriptions that were created in componentDidMount(). 166 | abstract componentWillUnmount: unit -> unit 167 | default __.componentWillUnmount () = () 168 | 169 | /// Error boundaries are React components that catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI instead of the component tree that crashed. Error boundaries catch errors during rendering, in lifecycle methods, and in constructors of the whole tree below them. 170 | /// A class component becomes an error boundary if it defines this lifecycle method. Calling setState() in it lets you capture an unhandled JavaScript error in the below tree and display a fallback UI. Only use error boundaries for recovering from unexpected exceptions; don’t try to use them for control flow. 171 | /// For more details, see [Error Handling in React 16](https://reactjs.org/blog/2017/07/26/error-handling-in-react-16.html). 172 | /// > Error boundaries only catch errors in the components below them in the tree. An error boundary can’t catch an error within itself. 173 | abstract componentDidCatch: error: Exception * info: obj -> unit 174 | default __.componentDidCatch (_, _) = () 175 | 176 | /// This function should be pure, meaning that it does not modify component state, it returns the same result each time it’s invoked, and it does not directly interact with the browser. If you need to interact with the browser, perform your work in componentDidMount() or the other lifecycle methods instead. Keeping render() pure makes components easier to think about. 177 | /// > render() will not be invoked if shouldComponentUpdate() returns false. 178 | abstract render: unit -> ReactElement 179 | 180 | interface ReactElement 181 | 182 | /// A react component that implements `shouldComponentUpdate()` with a shallow prop and state comparison. 183 | /// 184 | /// Usage: 185 | /// ``` 186 | /// type MyComponent(initialProps) = 187 | /// inherit React.PureComponent(initialProps) 188 | /// base.setInitState({ value = 5 }) 189 | /// override this.render() = 190 | /// let msg = sprintf "Hello %s, you have %i €" 191 | /// this.props.name this.state.value 192 | /// div [] [ofString msg] 193 | /// ``` 194 | type [] PureComponent<'P, 'S>(props: 'P) = 195 | inherit Component<'P, 'S>(props) 196 | 197 | /// A react component that implements `shouldComponentUpdate()` with a shallow prop comparison. 198 | /// 199 | /// Usage: 200 | /// ``` 201 | /// type MyComponent(initialProps) = 202 | /// inherit React.PureStatelessComponent(initialProps) 203 | /// override this.render() = 204 | /// let msg = sprintf "Hello %s, you have %i €" 205 | /// this.props.name this.props.value 206 | /// div [] [ofString msg] 207 | /// ``` 208 | type [] PureStatelessComponent<'P>(props: 'P) = 209 | inherit Component<'P, obj>(props) 210 | 211 | type FragmentProps = { key: string } 212 | 213 | type [] Fragment(props: FragmentProps) = 214 | interface ReactElement 215 | 216 | // These are not React interfaces but we add them here in case other Fable libraries need them 217 | 218 | type IProp = 219 | interface end 220 | 221 | type IHTMLProp = 222 | inherit IProp 223 | 224 | type IFragmentProp = 225 | inherit IProp 226 | -------------------------------------------------------------------------------- /src/Fable.React.Types/RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | ### 19.0.0-alpha.1 2 | 3 | - Upgrade `FSharp.Core` to 9.0.100 to support F# nullness 4 | - Make minimal adaptation to support F# nullness (`createElement` `props` become `objnull` instead of `obj`) 5 | - Compile will Nullable enabled 6 | 7 | ### 18.4.0 8 | 9 | - Add `Import` attribute to the React type definition allowing better type system when targeting TypeScript 10 | 11 | ### 18.3.0 12 | 13 | - Add tags for indexing in Fable packages project 14 | 15 | ### 18.2.0 16 | 17 | - Include source files under `fable` folder 18 | 19 | ### 18.1.0 20 | 21 | - Move react-dom bindings to own package 22 | 23 | ### 18.0.0 24 | 25 | - React 18 types 26 | -------------------------------------------------------------------------------- /src/Fable.React/Fable.React.FunctionComponent.fs: -------------------------------------------------------------------------------- 1 | namespace Fable.React 2 | 3 | open System 4 | open System.Runtime.CompilerServices 5 | open Fable.Core 6 | open Fable.Core.JsInterop 7 | 8 | #if FABLE_COMPILER 9 | type FunctionComponentPreparedRenderFunctionCache() = 10 | static let cache = 11 | let cache = JS.Constructors.Map.Create() 12 | #if DEBUG 13 | // Clear the cache when HMR is fired 14 | FunctionComponentPreparedRenderFunctionCache.OnHMR(fun () -> cache.clear()) 15 | #endif 16 | cache 17 | 18 | static member GetOrAdd( 19 | cacheKey: string, 20 | displayName: string, 21 | render: 'Props -> ReactElement, 22 | memoizeWith: ('Props -> 'Props -> bool) option, 23 | withKey: ('Props -> string) option, 24 | [] ?__callingMemberName: string) = 25 | let prepareRenderFunction () = 26 | render?displayName <- displayName 27 | let elemType = 28 | match memoizeWith with 29 | | Some areEqual -> 30 | #if DEBUG 31 | // In development mode, force rerenders always when HMR is fired 32 | let areEqual x y = 33 | not FunctionComponentPreparedRenderFunctionCache.IsHMRApplied && areEqual x y 34 | #endif 35 | let memoElement = ReactElementType.memoWith areEqual render 36 | memoElement?displayName <- "Memo(" + displayName + ")" 37 | memoElement 38 | | None -> ReactElementType.ofFunction render 39 | fun props -> 40 | let props = 41 | match withKey with 42 | | Some f -> props?key <- f props; props 43 | | None -> props 44 | ReactElementType.create elemType props [] 45 | 46 | if cache.has(cacheKey) then 47 | cache.get(cacheKey) :?> ('Props -> ReactElement) 48 | else 49 | let v = prepareRenderFunction () 50 | cache.set(cacheKey, box v) |> ignore 51 | v 52 | 53 | [ { if (status === 'apply') $0(); }) 57 | : void 0""")>] 58 | static member OnHMR(callback: unit->unit): unit = jsNative 59 | 60 | [] 64 | static member IsHMRApplied: bool = jsNative 65 | #endif 66 | 67 | type FunctionComponent = 68 | #if !FABLE_REPL_LIB 69 | /// Creates a lazy React component from a function in another file 70 | /// ATTENTION: Requires fable-compiler 2.3 or above 71 | /// Pass the external reference directly into the argument (avoid pipes) 72 | static member inline Lazy(f: 'Props -> ReactElement, fallback: ReactElement): 'Props -> ReactElement = 73 | #if FABLE_COMPILER 74 | let elemType = ReactBindings.React.``lazy``(fun () -> 75 | // React.lazy requires a default export 76 | (importValueDynamic f).``then``(fun x -> createObj ["default" ==> x])) 77 | fun props -> 78 | ReactElementType.create 79 | ReactBindings.React.Suspense 80 | (createObj ["fallback" ==> fallback]) 81 | [ReactElementType.create elemType props []] 82 | #else 83 | f // No React.lazy for SSR, just return component as is 84 | #endif 85 | #endif 86 | 87 | /// Creates a function React component that can use hooks to manage the component's life cycle, 88 | /// and is displayed in React dev tools (use `displayName` to customize the name). 89 | /// Uses React.memo if `memoizeWith` is specified (check `equalsButFunctions` and `memoEqualsButFunctions` helpers). 90 | /// When you need a key to optimize collections in React you can use `withKey` argument or define a `key` field in the props object. 91 | static member inline Of(render: 'Props->ReactElement, 92 | ?displayName: string, 93 | ?memoizeWith: 'Props -> 'Props -> bool, 94 | ?withKey: 'Props -> string 95 | #if FABLE_COMPILER 96 | ,[] ?__callingMemberName: string 97 | ,[] ?__callingSourceFile: string 98 | ,[] ?__callingSourceLine: int 99 | #endif 100 | ): 'Props -> ReactElement = 101 | #if FABLE_COMPILER 102 | // Cache the render function to prevent recreating the component every time when FunctionComponent.Of 103 | // is called inside another function (including generic values: let MyCom<'T> = ...) 104 | let cacheKey = 105 | __callingSourceFile.Value + 106 | "#L" + (string __callingSourceLine.Value) + 107 | // direct caller can also be generic, need separate cached func per 'Props argument 108 | ";" + typeof<'Props>.FullName 109 | let displayName = defaultArg displayName __callingMemberName.Value 110 | 111 | FunctionComponentPreparedRenderFunctionCache.GetOrAdd (cacheKey, displayName, render, memoizeWith, withKey) 112 | #else 113 | let elemType = ReactElementType.ofFunction render 114 | fun props -> 115 | ReactElementType.create elemType props [] 116 | #endif 117 | -------------------------------------------------------------------------------- /src/Fable.React/Fable.React.Helpers.fs: -------------------------------------------------------------------------------- 1 | namespace Fable.React 2 | 3 | open Fable.Core 4 | open Fable.Core.JsInterop 5 | open Browser 6 | open Props 7 | 8 | #if !FABLE_COMPILER 9 | type HTMLNode = 10 | | Text of string 11 | | RawText of string 12 | | Node of string * IProp seq * ReactElement seq 13 | | List of ReactElement seq 14 | | Empty 15 | with interface ReactElement 16 | 17 | type ServerElementType = 18 | | Tag 19 | | Fragment 20 | | Component 21 | 22 | type ReactElementTypeWrapper<'P> = 23 | | Comp of obj 24 | | Fn of ('P -> ReactElement) 25 | | HtmlTag of string 26 | interface ReactElementType<'P> 27 | 28 | [] 29 | module ServerRendering = 30 | let [] private ChildrenName = "children" 31 | 32 | let private createServerElementPrivate(tag: obj, props: obj, children: ReactElement seq, elementType: ServerElementType) = 33 | match elementType with 34 | | ServerElementType.Tag -> 35 | HTMLNode.Node (string tag, props :?> IProp seq, children) :> ReactElement 36 | | ServerElementType.Fragment -> 37 | HTMLNode.List children :> ReactElement 38 | | ServerElementType.Component -> 39 | let tag = tag :?> System.Type 40 | let comp = System.Activator.CreateInstance(tag, props) 41 | let childrenProp = tag.GetProperty(ChildrenName) 42 | childrenProp.SetValue(comp, children |> Seq.toArray) 43 | let render = tag.GetMethod("render") 44 | render.Invoke(comp, null) :?> ReactElement 45 | 46 | let private createServerElementByFnPrivate(f, props, children) = 47 | let propsType = props.GetType() 48 | let props = 49 | if propsType.GetProperty (ChildrenName) |> isNull then 50 | props 51 | else 52 | let values = ResizeArray () 53 | let properties = propsType.GetProperties() 54 | for p in properties do 55 | if p.Name = ChildrenName then 56 | values.Add (children |> Seq.toArray) 57 | else 58 | values.Add (FSharp.Reflection.FSharpValue.GetRecordField(props, p)) 59 | FSharp.Reflection.FSharpValue.MakeRecord(propsType, values.ToArray()) :?> 'P 60 | f props 61 | 62 | // In most cases these functions are inlined (mainly for Fable optimizations) 63 | // so we create a proxy to avoid inlining big functions every time 64 | 65 | let createServerElement(tag: obj, props: obj, children: ReactElement seq, elementType: ServerElementType) = 66 | createServerElementPrivate(tag, props, children, elementType) 67 | 68 | let createServerElementByFn(f, props, children) = 69 | createServerElementByFnPrivate(f, props, children) 70 | #endif 71 | 72 | [] 73 | [] 74 | module ReactElementType = 75 | let inline ofComponent<'comp, 'props, 'state when 'comp :> Component<'props, 'state>> : ReactElementType<'props> = 76 | #if FABLE_REPL_LIB 77 | failwith "Cannot create React components from types in Fable REPL" 78 | #else 79 | #if FABLE_COMPILER 80 | jsConstructor<'comp> |> unbox 81 | #else 82 | Comp (typeof<'comp>) :> _ 83 | #endif 84 | #endif 85 | 86 | let inline ofFunction<'props> (f: 'props -> ReactElement): ReactElementType<'props> = 87 | #if FABLE_COMPILER 88 | f |> unbox 89 | #else 90 | Fn f :> _ 91 | #endif 92 | 93 | let inline ofHtmlElement<'props> (name: string): ReactElementType<'props> = 94 | #if FABLE_COMPILER 95 | unbox name 96 | #else 97 | HtmlTag name :> ReactElementType<'props> 98 | #endif 99 | 100 | /// Create a ReactElement to be rendered from an element type, props and children 101 | let inline create<'props> (comp: ReactElementType<'props>) (props: 'props) (children: ReactElement seq): ReactElement = 102 | #if FABLE_COMPILER 103 | ReactBindings.React.createElement(comp, props, children) 104 | #else 105 | match (comp :?> ReactElementTypeWrapper<'props>) with 106 | | Comp obj -> ServerRendering.createServerElement(obj, props, children, ServerElementType.Component) 107 | | Fn f -> ServerRendering.createServerElementByFn(f, props, children) 108 | | HtmlTag obj -> ServerRendering.createServerElement(obj, props, children, ServerElementType.Tag) 109 | #endif 110 | 111 | /// React.memo is a higher order component. It’s similar to React.PureComponent but for function components instead of classes. 112 | /// If your function component renders the same result given the same props, you can wrap it in a call to React.memo. 113 | /// React will skip rendering the component, and reuse the last rendered result. 114 | /// By default it will only shallowly compare complex objects in the props object. If you want control over the comparison, you can use `memoWith`. 115 | let memo<'props> (render: 'props -> ReactElement) = 116 | #if FABLE_COMPILER 117 | ReactBindings.React.memo(render, unbox null) 118 | #else 119 | ofFunction render 120 | #endif 121 | 122 | /// React.memo is a higher order component. It’s similar to React.PureComponent but for function components instead of classes. 123 | /// If your function renders the same result given the "same" props (according to `areEqual`), you can wrap it in a call to React.memo. 124 | /// React will skip rendering the component, and reuse the last rendered result. 125 | /// By default it will only shallowly compare complex objects in the props object. If you want control over the comparison, you can use `memoWith`. 126 | /// This version allow you to control the comparison used instead of the default shallow one by provide a custom comparison function. 127 | let memoWith<'props> (areEqual: 'props -> 'props -> bool) (render: 'props -> ReactElement) = 128 | #if FABLE_COMPILER 129 | ReactBindings.React.memo(render, areEqual) 130 | #else 131 | ofFunction render 132 | #endif 133 | 134 | 135 | [] 136 | module Helpers = 137 | [] 138 | let inline createElement(comp: obj, props: obj, [] children: ReactElement seq): ReactElement = 139 | #if FABLE_COMPILER 140 | ReactBindings.React.createElement(comp, props, children) 141 | #else 142 | HTMLNode.Empty :> _ 143 | #endif 144 | 145 | /// Instantiate a component from a type inheriting React.Component 146 | /// Example: `ofType { myProps = 5 } []` 147 | let inline ofType<'T,'P,'S when 'T :> Component<'P,'S>> (props: 'P) (children: ReactElement seq): ReactElement = 148 | ReactElementType.create ReactElementType.ofComponent<'T,_,_> props children 149 | 150 | [] 151 | let inline com<'T,'P,'S when 'T :> Component<'P,'S>> (props: 'P) (children: ReactElement seq): ReactElement = 152 | ofType<'T, 'P, 'S> props children 153 | 154 | let inline ofFunction<'P> (f: 'P -> ReactElement) (props: 'P) (children: ReactElement seq): ReactElement = 155 | ReactElementType.create (ReactElementType.ofFunction f) props children 156 | 157 | /// Instantiate an imported React component. The first two arguments must be string literals, "default" can be used for the first one. 158 | /// Example: `ofImport "Map" "leaflet" { x = 10; y = 50 } []` 159 | let inline ofImport<'P> (importMember: string) (importPath: string) (props: 'P) (children: ReactElement seq): ReactElement = 160 | #if FABLE_REPL_LIB 161 | failwith "Cannot import React components in Fable REPL" 162 | #else 163 | #if FABLE_COMPILER 164 | ReactBindings.React.createElement(import importMember importPath, props, children) 165 | #else 166 | failwith "Cannot import React components in .NET" 167 | #endif 168 | #endif 169 | 170 | #if FABLE_COMPILER 171 | [] 172 | let private isFunction (x: obj): bool = jsNative 173 | 174 | [] 175 | let private isNonEnumerableObject (x: obj): bool = jsNative 176 | #endif 177 | 178 | /// Normal structural F# comparison, but ignores top-level functions (e.g. Elmish dispatch). 179 | /// Can be used e.g. with the `FunctionComponent.Of` `memoizeWith` parameter. 180 | let equalsButFunctions (x: 'a) (y: 'a) = 181 | #if FABLE_COMPILER 182 | if obj.ReferenceEquals(x, y) then 183 | true 184 | elif isNonEnumerableObject x && not(isNull(box y)) then 185 | let keys = JS.Constructors.Object.keys x 186 | let length = keys.Count 187 | let mutable i = 0 188 | let mutable result = true 189 | while i < length && result do 190 | let key = keys.[i] 191 | i <- i + 1 192 | let xValue = x?(key) 193 | result <- isFunction xValue || xValue = y?(key) 194 | result 195 | else 196 | (box x) = (box y) 197 | #else 198 | // Server rendering, won't be actually used 199 | // Avoid `x = y` because it will force 'a to implement structural equality 200 | false 201 | #endif 202 | 203 | /// Comparison similar to default React.memo, but ignores functions (e.g. Elmish dispatch). 204 | /// Performs a memberwise comparison where value types and strings are compared by value, 205 | /// and other types by reference. 206 | /// Can be used e.g. with the `FunctionComponent.Of` `memoizeWith` parameter. 207 | let memoEqualsButFunctions (x: 'a) (y: 'a) = 208 | #if FABLE_COMPILER 209 | if obj.ReferenceEquals(x, y) then 210 | true 211 | elif isNonEnumerableObject x && not(isNull(box y)) then 212 | let keys = JS.Constructors.Object.keys x 213 | let length = keys.Count 214 | let mutable i = 0 215 | let mutable result = true 216 | while i < length && result do 217 | let key = keys.[i] 218 | i <- i + 1 219 | let xValue = x?(key) 220 | result <- isFunction xValue || obj.ReferenceEquals(xValue, y?(key)) 221 | result 222 | else 223 | false 224 | #else 225 | // Server rendering, won't be actually used 226 | // Avoid `x = y` because it will force 'a to implement structural equality 227 | false 228 | #endif 229 | 230 | [] 231 | let memoBuilder<'props> (name: string) (render: 'props -> ReactElement) : 'props -> ReactElement = 232 | #if FABLE_COMPILER 233 | render?displayName <- name 234 | #endif 235 | let memoType = ReactElementType.memo render 236 | fun props -> 237 | ReactElementType.create memoType props [] 238 | 239 | [] 240 | let memoBuilderWith<'props> (name: string) (areEqual: 'props -> 'props -> bool) (render: 'props -> ReactElement) : 'props -> ReactElement = 241 | #if FABLE_COMPILER 242 | render?displayName <- name 243 | #endif 244 | let memoType = ReactElementType.memoWith areEqual render 245 | fun props -> 246 | ReactElementType.create memoType props [] 247 | 248 | [] 249 | let inline from<'P> (com: ReactElementType<'P>) (props: 'P) (children: ReactElement seq): ReactElement = 250 | ReactElementType.create com props children 251 | 252 | /// Alias of `ofString` 253 | let inline str (s: string): ReactElement = 254 | #if FABLE_COMPILER 255 | unbox s 256 | #else 257 | HTMLNode.Text s :> ReactElement 258 | #endif 259 | 260 | /// Cast a string to a React element (erased in runtime) 261 | let inline ofString (s: string): ReactElement = 262 | str s 263 | 264 | /// The equivalent of `sprintf (...) |> str` 265 | let inline strf format = 266 | Printf.kprintf str format 267 | 268 | /// Cast an option value to a React element (erased in runtime) 269 | let inline ofOption (o: ReactElement option): ReactElement = 270 | match o with Some o -> o | None -> null // Option.toObj(o) 271 | 272 | [] 273 | let opt (o: ReactElement option): ReactElement = 274 | ofOption o 275 | 276 | /// Cast an int to a React element (erased in runtime) 277 | let inline ofInt (i: int): ReactElement = 278 | #if FABLE_COMPILER 279 | unbox i 280 | #else 281 | HTMLNode.RawText (string i) :> ReactElement 282 | #endif 283 | 284 | /// Cast a float to a React element (erased in runtime) 285 | let inline ofFloat (f: float): ReactElement = 286 | #if FABLE_COMPILER 287 | unbox f 288 | #else 289 | HTMLNode.RawText (string f) :> ReactElement 290 | #endif 291 | 292 | /// Returns a list **from .render() method** 293 | let inline ofList (els: ReactElement list): ReactElement = 294 | #if FABLE_COMPILER 295 | unbox(List.toArray els) 296 | #else 297 | HTMLNode.List els :> ReactElement 298 | #endif 299 | 300 | /// Returns an array **from .render() method** 301 | let inline ofArray (els: ReactElement array): ReactElement = 302 | #if FABLE_COMPILER 303 | unbox els 304 | #else 305 | HTMLNode.List els :> ReactElement 306 | #endif 307 | 308 | /// A ReactElement when you don't want to render anything (null in javascript) 309 | let nothing: ReactElement = 310 | #if FABLE_COMPILER 311 | null 312 | #else 313 | HTMLNode.Empty :> ReactElement 314 | #endif 315 | 316 | /// Instantiate a DOM React element 317 | let inline domEl (tag: string) (props: IHTMLProp seq) (children: ReactElement seq): ReactElement = 318 | #if FABLE_COMPILER 319 | ReactBindings.React.createElement(tag, keyValueList CaseRules.LowerFirst props, children) 320 | #else 321 | ServerRendering.createServerElement(tag, (props |> Seq.cast), children, ServerElementType.Tag) 322 | #endif 323 | 324 | /// Instantiate a DOM React element (void) 325 | let inline voidEl (tag: string) (props: IHTMLProp seq) : ReactElement = 326 | #if FABLE_COMPILER 327 | ReactBindings.React.createElement(tag, keyValueList CaseRules.LowerFirst props, []) 328 | #else 329 | ServerRendering.createServerElement(tag, (props |> Seq.cast), [], ServerElementType.Tag) 330 | #endif 331 | 332 | /// Instantiate an SVG React element 333 | let inline svgEl (tag: string) (props: IProp seq) (children: ReactElement seq): ReactElement = 334 | #if FABLE_COMPILER 335 | ReactBindings.React.createElement(tag, keyValueList CaseRules.LowerFirst props, children) 336 | #else 337 | ServerRendering.createServerElement(tag, (props |> Seq.cast), children, ServerElementType.Tag) 338 | #endif 339 | 340 | /// Instantiate a React fragment 341 | let inline fragment (props: IFragmentProp seq) (children: ReactElement seq): ReactElement = 342 | #if FABLE_COMPILER 343 | ReactBindings.React.createElement(ReactBindings.React.Fragment, keyValueList CaseRules.LowerFirst props, children) 344 | #else 345 | ServerRendering.createServerElement(typeof, (props |> Seq.cast), children, ServerElementType.Fragment) 346 | #endif 347 | 348 | /// Accepts a context value to be passed to consuming components that are descendants of this Provider. 349 | /// One Provider can be connected to many consumers. Providers can be nested to override values deeper within the tree. 350 | /// Important: In SSR, this is ignored, and the DEFAULT value is consumed! 351 | /// More info at https://reactjs.org/docs/context.html#contextprovider 352 | let inline contextProvider (ctx: IContext<'T>) (value: 'T) (children: ReactElement seq): ReactElement = 353 | #if FABLE_COMPILER 354 | ReactBindings.React.createElement(ctx?Provider, createObj ["value" ==> value], children) 355 | #else 356 | fragment [] children 357 | #endif 358 | 359 | /// Consumes a context value, either from the nearest parent in the tree, or from the default value. 360 | /// Important: in SSR, this will always consume the context DEFAULT value! 361 | /// More info at https://reactjs.org/docs/context.html#contextconsumer 362 | let inline contextConsumer (ctx: IContext<'T>) (children: 'T -> ReactElement): ReactElement = 363 | #if FABLE_COMPILER 364 | ReactBindings.React.createElement(ctx?Consumer, null, [!!children]) 365 | #else 366 | let ctx = ctx :?> ISSRContext<_> 367 | fragment [] [children(ctx.DefaultValue)] 368 | #endif 369 | 370 | /// Creates a Context object. When React renders a component that subscribes to this Context 371 | /// object it will read the current context value from the closest matching Provider above it 372 | /// in the tree. More info at https://reactjs.org/docs/context.html#reactcreatecontext 373 | let inline createContext (defaultValue: 'T): IContext<'T> = 374 | #if FABLE_COMPILER 375 | ReactBindings.React.createContext(defaultValue) 376 | #else 377 | upcast { new ISSRContext<_> with member __.DefaultValue = defaultValue } 378 | #endif 379 | 380 | /// To be used in constructors of class components 381 | /// (for function components use `useRef` hook) 382 | let inline createRef (initialValue: 'T): IRefValue<'T> = 383 | #if FABLE_COMPILER 384 | ReactBindings.React.createRef(initialValue) 385 | #else 386 | { new IRefValue<_> with 387 | member __.current with get() = initialValue and set _ = () } 388 | #endif 389 | 390 | // Class list helpers 391 | let classBaseList baseClass classes = 392 | classes 393 | |> Seq.choose (fun (name, condition) -> 394 | if condition && not(System.String.IsNullOrEmpty(name)) then Some name 395 | else None) 396 | |> Seq.fold (fun state name -> state + " " + name) baseClass 397 | |> ClassName 398 | 399 | let classList classes = classBaseList "" classes 400 | 401 | /// Finds a DOM element by its ID and mounts the React element there 402 | /// Important: Not available in SSR 403 | let inline mountById (domElId: string) (reactEl: ReactElement): unit = 404 | #if FABLE_COMPILER 405 | ReactDom.render(reactEl, document.getElementById(domElId)) 406 | #else 407 | failwith "mountById is not available for SSR" 408 | #endif 409 | /// Finds the first DOM element matching a CSS selector and mounts the React element there 410 | /// Important: Not available in SSR 411 | let inline mountBySelector (domElSelector: string) (reactEl: ReactElement): unit = 412 | #if FABLE_COMPILER 413 | ReactDom.render(reactEl, document.querySelector(domElSelector)) 414 | #else 415 | failwith "mountBySelector is not available for SSR" 416 | #endif 417 | -------------------------------------------------------------------------------- /src/Fable.React/Fable.React.Isomorphic.fs: -------------------------------------------------------------------------------- 1 | module Fable.React.Isomorphic 2 | 3 | module Components = 4 | type HybridState = 5 | { isClient: bool } 6 | 7 | type HybridProps<'P> = 8 | { clientView: 'P -> ReactElement 9 | serverView: 'P -> ReactElement 10 | model: 'P } 11 | 12 | type HybridComponent<'P>(initProps) as this = 13 | inherit Component, HybridState>(initProps) with 14 | do this.setInitState { isClient=false } 15 | 16 | override __.componentDidMount() = 17 | this.setState(fun _ _ -> { isClient=true }) 18 | 19 | override x.render() = 20 | if x.state.isClient 21 | then x.props.clientView x.props.model 22 | else x.props.serverView x.props.model 23 | 24 | /// Isomorphic helper function for conditional executaion 25 | /// it will execute `clientFn model` on the client side and `serverFn model` on the server side 26 | let inline isomorphicExec (clientFn: 'a -> 'b) (serverFn: 'a -> 'b) (input: 'a) = 27 | #if FABLE_COMPILER 28 | clientFn input 29 | #else 30 | serverFn input 31 | #endif 32 | 33 | let isomorphicView (clientView: 'model -> ReactElement) (serverView: 'model -> ReactElement) (model: 'model) = 34 | #if FABLE_COMPILER 35 | ofType, _, _> 36 | { clientView=clientView; serverView=serverView; model=model } [] 37 | #else 38 | serverView model 39 | #endif 40 | -------------------------------------------------------------------------------- /src/Fable.React/Fable.React.ReactiveComponents.fs: -------------------------------------------------------------------------------- 1 | namespace Fable.React 2 | 3 | /// Helpers for ReactiveComponents (see #44) 4 | module ReactiveComponents = 5 | 6 | type Props<'P, 'S, 'Msg> = 7 | { key: string 8 | props: 'P 9 | update: 'Msg -> 'S -> 'S 10 | view: Model<'P, 'S> -> ('Msg->unit) -> ReactElement 11 | init: 'P -> 'S } 12 | 13 | and State<'T> = 14 | { value: 'T } 15 | 16 | and Model<'P, 'S> = 17 | { props: 'P 18 | state: 'S 19 | children: ReactElement[] } 20 | 21 | type ReactiveCom<'P, 'S, 'Msg>(initProps) = 22 | inherit Component, State<'S>>(initProps) 23 | do base.setInitState { value = initProps.init(initProps.props) } 24 | 25 | override this.render() = 26 | let model = 27 | { props = this.props.props 28 | state = this.state.value 29 | children = this.children } 30 | this.props.view model (fun msg -> 31 | let newState = this.props.update msg this.state.value 32 | this.setState(fun _ _ -> { value = newState })) 33 | 34 | [] 35 | module ReactiveComponentsHelpers = 36 | open ReactiveComponents 37 | 38 | /// Renders a stateful React component from functions similar to Elmish 39 | /// * `init` - Initializes component state with given props 40 | /// * `update` - Updates the state when `dispatch` is triggered 41 | /// * `view` - Render function, receives a `ReactiveComponents.Model` object and a `dispatch` function 42 | /// * `key` - The key is necessary to identify React elements in a list, an empty string can be passed otherwise 43 | /// * `props` - External properties passed to the component each time it's rendered, usually from its parent 44 | /// * `children` - A list of children React elements 45 | let reactiveCom<'P, 'S, 'Msg> 46 | (init: 'P -> 'S) 47 | (update: 'Msg -> 'S -> 'S) 48 | (view: Model<'P, 'S> -> ('Msg->unit) -> ReactElement) 49 | (key: string) 50 | (props: 'P) 51 | (children: ReactElement seq): ReactElement = 52 | ofType, Props<'P, 'S, 'Msg>, State<'S>> 53 | { key=key; props=props; update=update; view=view; init=init } 54 | children 55 | -------------------------------------------------------------------------------- /src/Fable.React/Fable.React.Standard.fs: -------------------------------------------------------------------------------- 1 | [] 2 | module Fable.React.Standard 3 | 4 | open Fable.React 5 | 6 | let inline a props children = domEl "a" props children 7 | let inline abbr props children = domEl "abbr" props children 8 | let inline address props children = domEl "address" props children 9 | let inline article props children = domEl "article" props children 10 | let inline aside props children = domEl "aside" props children 11 | let inline audio props children = domEl "audio" props children 12 | let inline b props children = domEl "b" props children 13 | let inline bdi props children = domEl "bdi" props children 14 | let inline bdo props children = domEl "bdo" props children 15 | let inline big props children = domEl "big" props children 16 | let inline blockquote props children = domEl "blockquote" props children 17 | let inline body props children = domEl "body" props children 18 | let inline button props children = domEl "button" props children 19 | let inline canvas props children = domEl "canvas" props children 20 | let inline caption props children = domEl "caption" props children 21 | let inline cite props children = domEl "cite" props children 22 | let inline code props children = domEl "code" props children 23 | let inline colgroup props children = domEl "colgroup" props children 24 | let inline data props children = domEl "data" props children 25 | let inline datalist props children = domEl "datalist" props children 26 | let inline dd props children = domEl "dd" props children 27 | let inline del props children = domEl "del" props children 28 | let inline details props children = domEl "details" props children 29 | let inline dfn props children = domEl "dfn" props children 30 | let inline dialog props children = domEl "dialog" props children 31 | let inline div props children = domEl "div" props children 32 | let inline dl props children = domEl "dl" props children 33 | let inline dt props children = domEl "dt" props children 34 | let inline em props children = domEl "em" props children 35 | let inline fieldset props children = domEl "fieldset" props children 36 | let inline figcaption props children = domEl "figcaption" props children 37 | let inline figure props children = domEl "figure" props children 38 | let inline footer props children = domEl "footer" props children 39 | let inline form props children = domEl "form" props children 40 | let inline h1 props children = domEl "h1" props children 41 | let inline h2 props children = domEl "h2" props children 42 | let inline h3 props children = domEl "h3" props children 43 | let inline h4 props children = domEl "h4" props children 44 | let inline h5 props children = domEl "h5" props children 45 | let inline h6 props children = domEl "h6" props children 46 | let inline head props children = domEl "head" props children 47 | let inline header props children = domEl "header" props children 48 | let inline hgroup props children = domEl "hgroup" props children 49 | let inline html props children = domEl "html" props children 50 | let inline i props children = domEl "i" props children 51 | let inline iframe props children = domEl "iframe" props children 52 | let inline ins props children = domEl "ins" props children 53 | let inline kbd props children = domEl "kbd" props children 54 | let inline label props children = domEl "label" props children 55 | let inline legend props children = domEl "legend" props children 56 | let inline li props children = domEl "li" props children 57 | let inline main props children = domEl "main" props children 58 | let inline map props children = domEl "map" props children 59 | let inline mark props children = domEl "mark" props children 60 | let inline menu props children = domEl "menu" props children 61 | let inline meter props children = domEl "meter" props children 62 | let inline nav props children = domEl "nav" props children 63 | let inline noscript props children = domEl "noscript" props children 64 | let inline object props children = domEl "object" props children 65 | let inline ol props children = domEl "ol" props children 66 | let inline optgroup props children = domEl "optgroup" props children 67 | let inline option props children = domEl "option" props children 68 | let inline output props children = domEl "output" props children 69 | let inline p props children = domEl "p" props children 70 | let inline picture props children = domEl "picture" props children 71 | let inline pre props children = domEl "pre" props children 72 | let inline progress props children = domEl "progress" props children 73 | let inline q props children = domEl "q" props children 74 | let inline rp props children = domEl "rp" props children 75 | let inline rt props children = domEl "rt" props children 76 | let inline ruby props children = domEl "ruby" props children 77 | let inline s props children = domEl "s" props children 78 | let inline samp props children = domEl "samp" props children 79 | let inline script props children = domEl "script" props children 80 | let inline section props children = domEl "section" props children 81 | let inline select props children = domEl "select" props children 82 | let inline small props children = domEl "small" props children 83 | let inline span props children = domEl "span" props children 84 | let inline strong props children = domEl "strong" props children 85 | let inline style props children = domEl "style" props children 86 | let inline sub props children = domEl "sub" props children 87 | let inline summary props children = domEl "summary" props children 88 | let inline sup props children = domEl "sup" props children 89 | let inline table props children = domEl "table" props children 90 | let inline tbody props children = domEl "tbody" props children 91 | let inline td props children = domEl "td" props children 92 | let inline textarea props children = domEl "textarea" props children 93 | let inline tfoot props children = domEl "tfoot" props children 94 | let inline th props children = domEl "th" props children 95 | let inline thead props children = domEl "thead" props children 96 | let inline time props children = domEl "time" props children 97 | let inline title props children = domEl "title" props children 98 | let inline tr props children = domEl "tr" props children 99 | let inline u props children = domEl "u" props children 100 | let inline ul props children = domEl "ul" props children 101 | let inline var props children = domEl "var" props children 102 | let inline video props children = domEl "video" props children 103 | 104 | // Void element 105 | let inline area props = voidEl "area" props 106 | let inline ``base`` props = voidEl "base" props 107 | let inline br props = voidEl "br" props 108 | let inline col props = voidEl "col" props 109 | let inline embed props = voidEl "embed" props 110 | let inline hr props = voidEl "hr" props 111 | let inline img props = voidEl "img" props 112 | let inline input props = voidEl "input" props 113 | let inline keygen props = voidEl "keygen" props 114 | let inline link props = voidEl "link" props 115 | let inline menuitem props = voidEl "menuitem" props 116 | let inline meta props = voidEl "meta" props 117 | let inline param props = voidEl "param" props 118 | let inline source props = voidEl "source" props 119 | let inline track props = voidEl "track" props 120 | let inline wbr props = voidEl "wbr" props 121 | 122 | // SVG elements 123 | let inline svg props children = svgEl "svg" props children 124 | let inline circle props children = svgEl "circle" props children 125 | let inline clipPath props children = svgEl "clipPath" props children 126 | let inline defs props children = svgEl "defs" props children 127 | let inline ellipse props children = svgEl "ellipse" props children 128 | let inline g props children = svgEl "g" props children 129 | let inline image props children = svgEl "image" props children 130 | let inline line props children = svgEl "line" props children 131 | let inline linearGradient props children = svgEl "linearGradient" props children 132 | let inline mask props children = svgEl "mask" props children 133 | let inline path props children = svgEl "path" props children 134 | let inline pattern props children = svgEl "pattern" props children 135 | let inline polygon props children = svgEl "polygon" props children 136 | let inline polyline props children = svgEl "polyline" props children 137 | let inline radialGradient props children = svgEl "radialGradient" props children 138 | let inline rect props children = svgEl "rect" props children 139 | let inline stop props children = svgEl "stop" props children 140 | let inline text props children = svgEl "text" props children 141 | let inline tspan props children = svgEl "tspan" props children 142 | -------------------------------------------------------------------------------- /src/Fable.React/Fable.React.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10.0.0 5 | 10.0.0-alpha.1 6 | netstandard2.0 7 | 8 | fsharp;fable;javascript;f#;js;react;fable-library;fable-javascript;fable-dotnet 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/Fable.React/RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | ### 10.0.0-alpha.1 2 | 3 | - Upgrade `FSharp.Core` to 9.0.100 to support F# nullness 4 | 5 | ### 9.4.0 6 | 7 | - Fix FunctionComponent's render func caching for generic usages 8 | 9 | ### 9.3.0 10 | 11 | - Add pointer events to `DOMAttr` 12 | 13 | ### 9.2.0 14 | 15 | - Add tags for indexing in Fable packages project 16 | 17 | ### 9.1.0 18 | 19 | - Add Fable.ReactDom.Types dependency 20 | 21 | ### 9.0.1 22 | 23 | - Fix compilation error 24 | 25 | ### 9.0.0 26 | 27 | - Move React types to Fable.React.Types package 28 | 29 | ### 8.0.1 30 | 31 | - Fix hydrateRoot @kerams 32 | 33 | ### 8.0.0 34 | 35 | - Add React 18 new APIs @kerams 36 | - Add Femto metadata @kerams 37 | 38 | ### 7.4.3 39 | 40 | - Fix FunctionComponent.Lazy for SSR @mvsmal 41 | 42 | ### 7.4.2 43 | 44 | - Add DOMAttr.Custom 45 | 46 | ### 7.4.1 47 | 48 | - Add license meta data (by @nojaf) 49 | 50 | ### 7.4.0 51 | 52 | - Lower FSharp.Core requirements 53 | 54 | ### 7.3.0 55 | 56 | - Supersede 7.1/7.2 from PR #208 57 | - Fix #210: defend hmr code 58 | 59 | ### 7.0.3 60 | 61 | - Fix FunctionComponent HMR, see Fable #2414 62 | 63 | ### 7.0.2 64 | 65 | - ReactServer: Optimize cssProps cache @medigor @delneg 66 | 67 | ### 7.0.1 68 | 69 | - Re-release version 7.0.0 because I unlisted it (I forgot to pull the latest changes from the master branch...) 70 | 71 | ### 7.0.0 72 | 73 | - Upgrade Fable.Browser.Dom to 2.0.1 making it easier for package dependant on Fable.React to use the new version of Fable.Browser.Dom 74 | 75 | ### 6.2.0 76 | 77 | - Fix notation for SVG attributes SSR (by @mvsmal) 78 | 79 | ### 6.1.0 80 | 81 | - FunctionComponent: Suspend memoize during HMR 82 | 83 | ### 6.0.0 84 | 85 | - Rework FunctionComponent to make it easier to use, it is also supporting HMR out of the box 86 | 87 | ### 5.4.0 88 | 89 | - Fix notation for SVG attributes SSR (by @mvsmal). The commit has been cherry picked from 6.2.0 release 90 | 91 | ### 5.3.6 92 | 93 | - Add `JustifySelf` used in Grid containers @Luiz-Monad 94 | - Add `StrokeDashoffset` to `SVGAttr` 95 | 96 | ### 5.3.5 97 | 98 | - Complete aria props @Krzysztof-Cieslak 99 | 100 | ### 5.3.4 101 | 102 | - Removed Hyphen from CssProp #183 @SCullman 103 | 104 | ### 5.3.3 105 | 106 | - Fix missing hyphens for some typed css props #181 @SCullman 107 | 108 | ### 5.3.2 109 | 110 | - Fix Hooks.useEffectDisposable closure @dbrattli 111 | 112 | ### 5.3.1 113 | 114 | - SSR: Fix race condition in CSS props cache @forki 115 | - Add CSSProp.UserSelect 116 | 117 | ### 5.3.0 118 | 119 | - Use typed options for remaining CSS props with untyped value @jannesiera 120 | - SSR: Void tags should contain a space @forki 121 | - SSR: Fix StringEnum CSS props 122 | - SSR: Cache CSS props 123 | 124 | ### 5.2.7 125 | 126 | - Add `forwardRef` @Luiz-Monad 127 | - Add `useReducer` @nojaf 128 | 129 | ### 5.2.6 130 | 131 | - Fix #167: Add `withKey` argument to `FunctionComponent.Of` 132 | 133 | ### 5.2.5 134 | 135 | - Fix #166: Expose `mountById`/`mountBySelector` in .NET 136 | 137 | ### 5.2.4 138 | 139 | - Add `contextConsumer` 140 | 141 | ### 5.2.3 142 | 143 | - Fix `Hooks.useStateLazy` 144 | 145 | ### 5.2.2 146 | 147 | - Undeprecate `ofFunction` 148 | 149 | ### 5.2.1 150 | 151 | - Add React Context 152 | 153 | ### 5.1.0 154 | 155 | - Add non-generic `ReactElementType` 156 | - Add memoEqualsButFunctions @vbfox 157 | 158 | ### 5.0.0 159 | 160 | - Function components 161 | - React hooks 162 | - Type-safe CSS props 163 | - Code splitting 164 | 165 | ### 5.0.0-beta-009 166 | 167 | - Add `FunctionComponent` 168 | 169 | ### 5.0.0-beta-007 170 | 171 | - Add `LazyComponent.FromExternalFunction` 172 | 173 | ### 5.0.0-beta-006 174 | 175 | - Add some additional hooks 176 | 177 | ### 5.0.0-beta-004 178 | 179 | - Fix `Hooks.useEffectDisposable` and `equalsButFunctions` 180 | 181 | ### 5.0.0-beta-003 182 | 183 | - Add `equalsButFunctions` @vbfox 184 | 185 | ### 5.0.0-beta-002 186 | 187 | - Fix `object` tag 188 | - Fix effect hook 189 | 190 | ### 5.0.0-beta-001 191 | 192 | - Add type-safe css properties @Zaid-Ajaj 193 | 194 | ### 5.0.0-alpha-005 195 | 196 | - Change dependency to Fable.Browser.Dom 197 | - Rename namespace to Fable.React 198 | - Add Hooks' support 199 | 200 | ### 5.0.0-alpha-004 201 | 202 | - Fix `b` 203 | 204 | ### 5.0.0-alpha-003 205 | 206 | - Fix compilation with fable-splitter and `allFiles` #122 @nojaf 207 | - Add `ValueMultiple` prop for `select` elements with `Multiple` prop #123 208 | 209 | ### 5.0.0-alpha-002 210 | 211 | - Fix typos in doc comment of `memoBuilderWith` and `memoBuilder` 212 | 213 | ### 5.0.0-alpha-001 214 | 215 | - Add bindings for `React.memo` 216 | - Add `nothing` helper 217 | 218 | ### 4.1.1 219 | 220 | - Mark `setState: 'S` as obsolete @SirUppyPancakes 221 | - Enable writing into a TextWritter in SSR @thinkbeforecoding 222 | 223 | ### 4.0.1 224 | 225 | - Update Fable 2 deps to stable version 226 | 227 | ### 4.0.0 228 | 229 | - Release stable version 230 | - Make `Data` type consistent across compiler directive 231 | 232 | ### 4.0.0-alpha-001 233 | 234 | Alpha version for Fable 2 235 | 236 | ### 3.1.2 237 | 238 | - Include documentation in the package 239 | - Add `FromEvent.Value` helper 240 | 241 | ### 3.1.1 242 | 243 | - Remove ReactiveComponents.Model.key as React doesn't allow access to props.key 244 | 245 | ### 3.1.0 246 | 247 | - Speed-up Server-Side rendering @forki 248 | - Make escapeHtml faster @zaaack 249 | 250 | ### 3.0.0 251 | 252 | - Stable version 253 | 254 | ### 3.0.0-alpha-003 255 | 256 | - Give precedence to CSSProps 257 | 258 | ### 3.0.0-alpha-002 259 | 260 | - Add Server Side Rendering Support 261 | 262 | ### 3.0.0-alpha-001 263 | 264 | - Alpha release of next major version 265 | 266 | ### 2.1.0 267 | 268 | - Add React PureComponent and Fragment (@vbfox) 269 | 270 | ### 2.0.0 271 | 272 | - Stable version 273 | 274 | ### 2.0.0-beta-002 275 | 276 | - Add BoxShadow CSSProp (@worldbeater) 277 | - Add Class as alias of ClassName 278 | 279 | ### 2.0.0-beta-001 280 | 281 | - Add `reactiveCom` helper (stateful mini-Elmish component) 282 | - Add `mountById` and `mountBySelectors` 283 | - Add abstract methods to `React.Component` so users have autocompletion and docs when overriding 284 | - Uniform helpers API: ofType, ofFunction, ofImport, ofOption, ofArray... 285 | 286 | ### 1.2.2 287 | 288 | - Add Height to SVGProp 289 | 290 | ### 1.2.1 291 | 292 | - Use new `ParamList` attribute 293 | 294 | ### 1.2.0 295 | 296 | - Release stable version 297 | 298 | ### 1.2.0-beta-003 299 | 300 | - Add CSSProp.OverflowY 301 | 302 | ### 1.2.0-beta-002 303 | 304 | - Target netstandard1.6 again 305 | 306 | ### 1.2.0-beta-001 307 | 308 | - Upgrade to netstandard2.0 309 | - Add react-dom/server methods 310 | - Add Animation and Transition events 311 | - Remove Erased union types from Prop helpers (breaking) 312 | - Give precedence to CSSProps 313 | - Fix SVG elements with mixed props 314 | - Comment Data and uncomment Height HTMLProps 315 | - Add CSSProp.Cursor 316 | 317 | ### 1.1.0 318 | 319 | - Release stable version 320 | -------------------------------------------------------------------------------- /src/Fable.ReactDom.Types/Fable.ReactDom.Types.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19.0.0 5 | 19.0.0-alpha.1 6 | netstandard2.0 7 | enable 8 | 9 | fsharp;fable;javascript;f#;js;react;fable-binding;fable-javascript 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/Fable.ReactDom.Types/Fable.ReactDom.fs: -------------------------------------------------------------------------------- 1 | namespace Fable.React 2 | 3 | open Fable.Core 4 | open Fable.React 5 | open Browser.Types 6 | 7 | type IReactRoot = 8 | /// Render a React element into the root. 9 | abstract render: element: ReactElement -> unit 10 | 11 | /// Remove the root from the DOM. 12 | abstract unmount: unit -> unit 13 | 14 | type IRootOptions = 15 | /// Optional callback called when React automatically recovers from errors. 16 | abstract onRecoverableError: (obj -> unit) with get, set 17 | 18 | /// Optional prefix React uses for ids generated by React.useId. Useful to avoid conflicts when using multiple roots on the same page. Must be the same prefix used on the server. 19 | abstract identifierPrefix: string with get, set 20 | 21 | type ICreateRootOptions = 22 | inherit IRootOptions 23 | 24 | type IHydrateRootOptions = 25 | inherit IRootOptions 26 | 27 | type IReactDom = 28 | /// Render a React element into the DOM in the supplied container. 29 | /// If the React element was previously rendered into container, this will perform an update on it and only mutate the DOM as necessary to reflect the latest React element. 30 | /// If the optional callback is provided, it will be executed after the component is rendered or updated. 31 | abstract render: element: ReactElement * container: Element * ?callback: (unit->unit) -> unit 32 | 33 | /// Same as render(), but is used to hydrate a container whose HTML contents were rendered by ReactDOMServer. React will attempt to attach event listeners to the existing markup. 34 | abstract hydrate: element: ReactElement * container: Element * ?callback: (unit->unit) -> unit 35 | 36 | /// Remove a mounted React component from the DOM and clean up its event handlers and state. If no component was mounted in the container, calling this function does nothing. Returns true if a component was unmounted and false if there was no component to unmount. 37 | abstract unmountComponentAtNode: container: Element -> bool 38 | 39 | /// Creates a portal. Portals provide a way to render children into a DOM node that exists outside the hierarchy of the DOM component. 40 | abstract createPortal: child: ReactElement * container: Element -> ReactElement 41 | 42 | /// Force React to flush any updates inside the provided callback synchronously. This ensures that the DOM is updated immediately. 43 | abstract flushSync: callback: (unit -> unit) -> unit 44 | 45 | type IReactDomClient = 46 | /// Create a React root for the supplied container and return the root. The root can be used to render a React element into the DOM with render. 47 | /// Requires React 18. 48 | abstract createRoot: container: Element * ?options: ICreateRootOptions -> IReactRoot 49 | 50 | /// Same as createRoot(), but is used to hydrate a container whose HTML contents were rendered by ReactDOMServer. React will attempt to attach event listeners to the existing markup. 51 | /// Requires React 18. 52 | abstract hydrateRoot: container: Element * element: ReactElement * ?options: IHydrateRootOptions -> IReactRoot 53 | 54 | type IReactDomServer = 55 | /// Render a React element to its initial HTML. This should only be used on the server. React will return an HTML string. You can use this method to generate HTML on the server and send the markup down on the initial request for faster page loads and to allow search engines to crawl your pages for SEO purposes. 56 | /// If you call ReactDOM.render() on a node that already has this server-rendered markup, React will preserve it and only attach event handlers, allowing you to have a very performant first-load experience. 57 | abstract renderToString: element: ReactElement -> string 58 | 59 | /// Similar to renderToString, except this doesn't create extra DOM attributes such as data-reactid, that React uses internally. This is useful if you want to use React as a simple static page generator, as stripping away the extra attributes can save lots of bytes. 60 | abstract renderToStaticMarkup: element: ReactElement -> string 61 | 62 | [] 63 | module ReactDomBindings = 64 | #if FABLE_REPL_LIB 65 | [] 66 | #else 67 | [] 68 | #endif 69 | let ReactDom: IReactDom = jsNative 70 | 71 | /// Requires React 18. 72 | #if FABLE_REPL_LIB 73 | [] 74 | #else 75 | [] 76 | #endif 77 | let ReactDomClient: IReactDomClient = jsNative 78 | 79 | #if !FABLE_REPL_LIB 80 | [] 81 | let ReactDomServer: IReactDomServer = jsNative 82 | #endif 83 | -------------------------------------------------------------------------------- /src/Fable.ReactDom.Types/RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | ### 19.0.0-alpha.1 2 | 3 | - Upgrade `FSharp.Core` to 9.0.100 to support F# nullness 4 | - Compile will Nullable enabled 5 | 6 | ### 18.2.0 7 | 8 | - Add tags for indexing in Fable packages project 9 | 10 | ### 18.1.0 11 | 12 | - Include source files under `fable` folder 13 | 14 | ### 18.0.0 15 | 16 | - React DOM 18 types 17 | --------------------------------------------------------------------------------