Feliz + Vite + fable + react = merry fsAdvent2024
Run 🏃 Now!
git clone https://github.com/jkone27/feliz-vite.git
npm ci
npm run dev
Vite 🍇⚡️
Now the “easy” part, Vite is a great frontend tool developed in the context of Vue.js but now adopted for all kinds of usages from React\Next to Svelte to many other frameworks.
In practice Vite is a great development server and production bundler, in this blog post we will mostly check the dev server capabilities of Vite, when united with a super magic Fable-Vite-Plugin, we will be able to lazily transpile .fs files to .js via Fable but using vite HMR a.k.a. hot module replacement technology!
Vite, using Rollup and ES6 is an ES module-aware bundler and dev server, which makes it awesome for many scenarios when working with frontend technology. Below is the official description of Vite from the official website:
React 🪐
Some might not like it, but React makes up most of the world’s web today (39.5% according to Statista).
Although even more declarative and hyper-cool alternatives like HTMX exist (big fan here), we shouldn’t forget the fact that this SPA framework is ruling the web market, and aging code is there to stay for quite some time too.
If we are willing to bring F# to the client side of the web dev world, having it work well with existing React.js repositories is a top-notch feature.
Being able to evolve bits by bits existing react codebases in JS or TS, (with .tsx
nd .jsx
components) might be a great + for beginners! Or even being able to write React component libraries for pure React from purely F# codebases!
Really, but HOW you might ask?
Santa comes to our help!
Fable 🐉 and Feliz
If you want to know more about Fable and/or Feliz (pure HTML F# list DSL) you might want to read this previous blog post where I blogged about writing React js in F# using those two together, so I suggest reading that article for a quick initiation on the subject.
Here is a sample of a simple React component using Feliz and how you would mount the main App in a root div for the id `root` for example:
// module Components [Components.fs]
open Feliz
[<ReactComponent>]
let Counter() =
let (count, setCount) = React.useState(0)
Html.div [
Html.button [
prop.style [ style.marginRight 5 ]
prop.onClick (fun _ -> setCount(count + 1))
prop.text "Increment"
]
Html.button [
prop.style [ style.marginLeft 5 ]
prop.onClick (fun _ -> setCount(count - 1))
prop.text "Decrement"
]
Html.h1 count
]
// module App [App.fs]
open Browser.Dom
ReactDOM.render(Counter(), document.getElementById "root")
You can mix and match the native F# approach with.tsx
files and Jsx inline string literals as well
Importing React Components
You can import from jsx/tsx
in Feliz, the same approach works also for npm react component libraries or any react module, just remember to use the right import if you are using default export vs named exports.
open Feliz
type Components =
[<ReactComponent(import="About", from="./About.jsx")>]
static member About (title: string) = React.imported()
// assuming we ran `npm i some-imported-react-components`
// and it had default exports for button in its index file
[<ReactComponent(import="default", from="some-imported-react-components/button")>]
static member Button (text: string, children: ReactElement seq) = React.imported()
Once you have imported your components, you can use them in your app, remember to include any .fs
file in the .fsproj
file as F# follows strict compilation order for files. F# needs you to be explicit because it prevents cyclic dependencies with this “simple” compilation strategy. Here an example of how you can use your components:
Html.div [
Html.h1 "Content"
Components.About(title="Feliz")
Components.Button(text="Click ME",
children = [
Html.span "TEST"
]
)
]
Imports in Fable
When working with Vite is common to need to import static assets like icons and images, and have the ability to import CSS to specific components/elements (e.g. in React or Vue).
this is how we can do import operations using Fable in place of Javascript directly.
To import a CSS file use the importSideEffects
function:
importSideEffects "./index.css"
To import static assets (e.g. icons/svgs/urls) you can use dynamic lazy imports setting the signature type to unit -> unit
and using the import
default function.
let viteLogo: unit -> unit = import "default" "./assets/vite.svg"
let reactLogo: unit -> unit = import "default" "./assets/react.svg"
Read a bit further down the line to see how to use it in “real” code.
Fable Vite Plugin
This plugin is a community-maintained OS plugin for F#/Fable to allow the Fable compiler to be invoked by Vite HRM, but is just awesome, and works great together with Feliz React DSL as well!
Auto-magic install 🧞♂️
An interesting fact is that if you initialize a repository with vite
or bun
, using the fable vite plugin, you do not need to install Fable as a dotnet tool, all the installation for the required Fable compiler should run behind the scenes for you!
Let’s Go 🦌
Now before getting started, you need dotnet-sdk
setup on your machine, on mac you can just run your usual brew command.
We could install a working Feliz template, which already works as Fable Feliz, but does not have the HMR vite reloading capabilities yet, and is not up to date.
Let’s create a react js vite app instead, and add the fable plugin and feliz instead as additional steps!
npm create vite@latest -o FsharpAdvent2024
and install the fable vite plugin
npm install -D vite-plugin-fable
# if error in post install script:
# npm --prefix ./node_modules/vite-plugin-fable/ run postinstall
Then we update the index.html
file to load our react Feliz vite app and move it to your src
folder, together with your other src files
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Fable</title>
</head>
<body>
<div id="feliz-app"></div>
<!-- the module issue mentioned on the website has been resolved?! -->
<script type="module" src="App.fs"></script>
</body>
</html>
Then update vite.config.js
with the new Fable plugin, we are going to use Feliz and enable also Jsx inline strings (just in case you need it later), if you want a custom configuration see in the available recipes, also we will adjust it for the /src
directory project of the Feliz template, and add some minor fixes for nodejs
import { defineConfig } from 'vite';
import fable from 'vite-plugin-fable';
import react from '@vitejs/plugin-react';
// https://nojaf.com/vite-plugin-fable/recipes.html#Using-React
// https://nojaf.com/vite-plugin-fable/recipes.html#Fable-Core-JSX
export default defineConfig({
// order of plugins matters, fable needs to be first
plugins: [
fable({
fsproj: "./src/App.fsproj",
jsx: 'automatic'
}),
react({
include: /\.(fs|js|jsx|ts|tsx)$/,
jsxRuntime: "classic"
})
],
root: "./src",
build: {
outDir: "../dist",
//sourcemap: 'inline' ?? couldn't make vscode debug in .fs work
},
define: {
// required if u have: `process is undefined`
// while loading react jsoncomponents
'process.env': {}
}
})
Then we can create our F# code and remove un-needed js code:
cd src && dotnet new console --language F#
dotnet add package Feliz
dotnet add package Feliz.Router
then rename your project as App.fsproj
delete Program.fs
and add the following files: Component.fs
and App.fs
and include them in the project in the following order
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType><!-- can remove this line -->
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="Components.fs" />
<Compile Include="App.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Feliz" Version="2.9.0" />
<PackageReference Include="Feliz.Router" Version="4.0.0" />
</ItemGroup>
</Project>
Now edit your App.fs
file to look like this one
// App.fs
module App
open Feliz
open Browser.Dom
open Fable.Core.JsInterop
importSideEffects "./index.css"
let root = ReactDOM.createRoot(document.getElementById "root")
root.render(App.Components.Counter())
And then edit yourComponents.fs
file like the one below
// Components.fs
namespace App
open Feliz
open Fable.Core
open Fable.Core.JsInterop
type Components() =
[<ReactComponent>]
static member Counter() =
let (count, setCount) = React.useState(0)
let viteLogo: unit -> unit = import "default" "./assets/vite.svg"
let reactLogo: unit -> unit = import "default" "./assets/react.svg"
importSideEffects "./App.css"
Html.div [
Html.div [
Html.a [
prop.href "https://vite.dev"
prop.target "_blank"
prop.children [
Html.img [
prop.src $"{viteLogo}"
prop.className "logo"
prop.alt "Vite logo"
]
]
]
Html.a [
prop.href "https://react.dev"
prop.target "_blank"
prop.children [
Html.img [
prop.src $"{reactLogo}"
prop.className "logo react"
prop.alt "React logo"
]
]
]
Html.a [
prop.href "https://nojaf.com/vite-plugin-fable"
prop.target "_blank"
prop.children [
Html.img [
prop.src "https://nojaf.com/vite-plugin-fable/img/logo.png"
prop.className "logo"
prop.alt "fable vite plugin logo"
]
]
]
]
Html.h1 "Vite + React + Feliz"
Html.div [
prop.className "card"
prop.children [
Html.button [
prop.onClick (fun _ -> setCount(count + 1))
prop.text $"count is {count}"
]
Html.p [
Html.text "Edit "
Html.code "src/App.fs"
Html.text " and save to test HMR"
]
]
]
Html.p [
prop.className "read-the-docs"
prop.text " Click on the Vite, React or Fable Vite Plugin logos to learn more"
]
]
Cool! now you are ready to Roll, let’s run our Feliz Vite app with HMR reloading now! npm run dev
if you feel like checking your production bundle you can run npm run build
and then npm run preview
for having it served by vite in preview mode!
Ain’t that cool? 😎
I wish you and your dears a happy Xmas 🎅🎄 and lots of tasty meals!
F# is an awesome language, try it out and spread the love 🧡
Here is a link to the repo in case you missed it 👇 https://github.com/jkone27/feliz-vite
References
- https://fable.io/
- https://zaid-ajaj.github.io/Feliz/
- https://vite.dev/
- https://react.dev/
- https://vite.dev/guide/api-hmr
- https://github.com/nojaf/vite-plugin-fable/tree/main/sample-project
- https://jkone27-3876.medium.com/react-makes-me-feliz-ff7909777dba
- https://github.com/laurentpayot/fsharp-fable-elmish-example
- https://github.com/jasiozet/fable-feliz-template
- https://nojaf.com/vite-plugin-fable/
- https://nojaf.com/vite-plugin-fable/getting-started.html
- https://github.com/Zaid-Ajaj/Feliz/blob/master/Feliz.Template/Content/README.md
- https://zaid-ajaj.github.io/Feliz/#/Feliz/ProjectTemplate
- https://www.statista.com/statistics/1124699/worldwide-developer-survey-most-used-frameworks-web/
- https://zaid-ajaj.github.io/Feliz/#/Feliz/UsingJsx
- https://fable.io/docs/javascript/features.html#import
- https://github.com/jkone27/feliz-vite