Feliz Navidad 🎄

jkone27
8 min read6 days ago

--

Feliz + Vite + fable + react = merry fsAdvent2024

Run 🏃 Now!

git clone https://github.com/jkone27/feliz-vite.git

npm ci

npm run dev

Vite 🍇⚡️

Vite splash page

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:

Vite official description from its 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!

Santa this year swapped his raindeers with something bigger! a Fable dragon!

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

Fable vite plugin (and its logo) are dope

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

this should be running on your localhost if the compilation succeded! congrats

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

--

--

Responses (1)