Output JSX
If you want to use JSX output you need to change the file extension of your transpiled files to *.jsx
. This is required for the JSX output to be correctly interpreted by bundlers like webpack or tools like eslint. You can do this by updating your Fable transpile command:
dotnet fable Example.fsproj -e .jsx
This does not impact your ability to use the established Feliz API, which will still work as expected in *.jsx
files.
Currently, the default way for Feliz to create React Elements is through React.createElement
. This allows us to fully leverage f# list expression syntax and makes it easy to create elements dynamically. However, using modern JSX output makes the transpiled code more readable and easier to debug, and might lead to performance increases.
For more detailed information about React concepts and APIs, please refer to the official documentation for createElement.
Thanks to the awesome work of the Fable team (big shoutout to @MangelMaxime), the support for JSX output was greatly increased. And there are two ways to use it in Feliz. One is the Feliz.JSX
module, which provides a similar API to the existing Feliz
module, but outputs JSX. The other is through template strings, which allow you to write JSX directly in your F# code.
In my opinion this feature is still experimental, and it might be best to mix and match between the existing Feliz API and the new JSX output, depending on your needs. But it's definitely worth exploring!
Using the JSX module
The Feliz.JSX
module uses Fables JSX.create
function to output JSX. This abstraction from JSX.create
to JSX output is done directly on the Fable compiler level, resulting in a very clean output.
If you want to give it a try you can open Feliz.JSX
in your file. This will shadow the existing Html.*
so you can easily switch between the two.
Below you can find an example for using the Feliz.JSX
module. It fully duplicates the syntax from the default Html.*
type. Make sure to check out the transpiled outputs!
Hello from JSX!
This is a paragraph inside a div.
Counter - Reactivity Test
- No items
Show code
- JSX.fs
- JSXOutout.jsx
- DefaultOutput.js
module Examples.Guides.JSXModule
open Feliz
open Feliz.JSX
[<ReactComponent(true)>]
let TestComponent() =
let counter, setCounter = React.useState(0)
Html.div [
prop.id "my-div"
prop.className "container"
prop.children [
Html.h1 "Hello from JSX!"
Html.p "This is a paragraph inside a div."
Html.div [
Html.h1 "Counter - Reactivity Test"
Html.button [
prop.text "Increment"
prop.onClick (fun _ -> setCounter(counter + 1))
]
Html.ul [
if counter = 0 then
Html.li "No items"
else
for i in 1..counter do
Html.li [
prop.key i
prop.text (sprintf "Item %d" i)
]
]
]
]
]
import { useState } from "react";
import React from "react";
import { map, singleton, delay, toList } from "../../fable_modules/fable-library-js.5.0.0-alpha.14/Seq.js";
import { printf, toText } from "../../fable_modules/fable-library-js.5.0.0-alpha.14/String.js";
import { rangeDouble } from "../../fable_modules/fable-library-js.5.0.0-alpha.14/Range.js";
export function TestComponent() {
const patternInput = useState(0);
const counter = patternInput[0] | 0;
return <div id="my-div"
className="container">
<h1>
Hello from JSX!
</h1>
<p>
This is a paragraph inside a div.
</p>
<div>
<h1>
Counter - Reactivity Test
</h1>
<button onClick={(_arg) => {
patternInput[1](counter + 1);
}}>
Increment
</button>
<ul>
{(counter === 0) ? (<li>
No items
</li>) : (map((i) => <li key={i}>
{toText(printf("Item %d"))(i)}
</li>, rangeDouble(1, 1, counter)))}
</ul>
</div>
</div>;
}
export default TestComponent;
import { createElement, useState } from "react";
import React from "react";
import { HtmlHelper_createElement } from "../../src/Feliz/Html.jsx";
import { defaultOf } from "../../fable_modules/fable-library-js.5.0.0-alpha.14/Util.js";
import { ofArray } from "../../fable_modules/fable-library-js.5.0.0-alpha.14/List.js";
import { map, singleton, delay, toList } from "../../fable_modules/fable-library-js.5.0.0-alpha.14/Seq.js";
import { printf, toText } from "../../fable_modules/fable-library-js.5.0.0-alpha.14/String.js";
import { rangeDouble } from "../../fable_modules/fable-library-js.5.0.0-alpha.14/Range.js";
export function TestComponent() {
let children_11, children_8;
const patternInput = useState(0);
const setCounter = patternInput[1];
const counter = patternInput[0] | 0;
return HtmlHelper_createElement("div", ofArray([["id", "my-div"], ["className", "container"], ["children", [createElement("h1", defaultOf(), "Hello from JSX!"), createElement("p", defaultOf(), "This is a paragraph inside a div."), (children_11 = ofArray([createElement("h1", defaultOf(), "Counter - Reactivity Test"), HtmlHelper_createElement("button", ofArray([["children", "Increment"], ["onClick", (_arg) => {
setCounter(counter + 1);
}]])), (children_8 = toList(delay(() => ((counter === 0) ? singleton(createElement("li", defaultOf(), "No items")) : map((i) => HtmlHelper_createElement("li", ofArray([["key", i], ["children", toText(printf("Item %d"))(i)]])), rangeDouble(1, 1, counter))))), createElement("ul", defaultOf(), ...children_8))]), createElement("div", defaultOf(), ...children_11))]]]));
}
export default TestComponent;
Alias the Feliz.JSX module
If you want to mix both JSX output and previous output in one file you can alias the module:
type Jsx = Feliz.JSX.Html
Jsx.div [
Jsx.h1 "Hello from JSX!"
Jsx.p "This is a paragraph inside a div."
]
Hello from JSX!
This is a paragraph inside a div.
Counter - Reactivity Test
- No items
Show code
- JSXAlias.fs
- JSXAlias.jsx
module Examples.Guides.JSXModuleAlias
open Feliz
type JSX = Feliz.JSX.Html
[<ReactComponent(true)>]
let TestComponent() =
let counter, setCounter = React.useState(0)
JSX.div [
prop.id "my-div"
prop.className "container"
prop.children [
JSX.h1 "Hello from JSX!"
JSX.p "This is a paragraph inside a div."
JSX.div [
JSX.h1 "Counter - Reactivity Test"
JSX.button [
prop.text "Increment"
prop.onClick (fun _ -> setCounter(counter + 1))
]
Html.ul [
if counter = 0 then
Html.li "No items"
else
for i in 1..counter do
Html.li [
prop.key i
prop.text (sprintf "Item %d" i)
]
]
]
]
]
import { createElement, useState } from "react";
import React from "react";
import { map, singleton, delay, toList } from "../../fable_modules/fable-library-js.5.0.0-alpha.14/Seq.js";
import { defaultOf } from "../../fable_modules/fable-library-js.5.0.0-alpha.14/Util.js";
import { HtmlHelper_createElement } from "../../src/Feliz/Html.jsx";
import { printf, toText } from "../../fable_modules/fable-library-js.5.0.0-alpha.14/String.js";
import { ofArray } from "../../fable_modules/fable-library-js.5.0.0-alpha.14/List.js";
import { rangeDouble } from "../../fable_modules/fable-library-js.5.0.0-alpha.14/Range.js";
export function TestComponent() {
let children_5;
const patternInput = useState(0);
const counter = patternInput[0] | 0;
return <div id="my-div"
className="container">
<h1>
Hello from JSX!
</h1>
<p>
This is a paragraph inside a div.
</p>
<div>
<h1>
Counter - Reactivity Test
</h1>
<button onClick={(_arg) => {
patternInput[1](counter + 1);
}}>
Increment
</button>
{(children_5 = toList(delay(() => ((counter === 0) ? singleton(createElement("li", defaultOf(), "No items")) : map((i) => HtmlHelper_createElement("li", ofArray([["key", i], ["children", toText(printf("Item %d"))(i)]])), rangeDouble(1, 1, counter))))), createElement("ul", defaultOf(), ...children_5))}
</div>
</div>;
}
export default TestComponent;
FAQ
These FAQs are errors or issues that you might encounter when using the JSX module. (Tested for Fable version 5.0.0-alpha.14).
match ... with
in children 🐛
Using match ... with
directly inside prop.children
is currently not correctly handled by the fable compiler and will lead to runtime errors. A workaround is to use if
syntax instead or to bind the result of the match to a variable first.
Tracked in this GitHub issue.
- Error: match ... with
- Workaround: if ... else
- Workaround: let
[<ReactComponent(true)>]
let TestComponent() =
let counter, setCounter = React.useState(0)
Html.ul [
match counter with
| 0 -> Html.li "No items!"
| items ->
for i in 1..items do
Html.li [
prop.key i
prop.text (sprintf "Item %d" i)
]
]
Html.ul [
if counter = 0 then
Html.li "No items"
else
for i in 1..counter do
Html.li [
prop.key i
prop.text (sprintf "Item %d" i)
]
]
let ChildList =
match counter with
| 0 ->
Html.li "No items"
| items ->
React.Fragment [
for i in 1..items do
Html.li [
prop.key i
prop.text (sprintf "Item %d" i)
]
]
Html.ul [
ChildList
]
conditional properties ⚠️
error FABLE: Expecting a static list or array literal (no generator) for JSX props
Tracked in this GitHub issue.
You might encounter issues when using conditional properties.
In Feliz you normally can do conditional properties in any way F# list syntax allows.
Html.div [
if isActive then prop.custom("data-active", true)
if isDisabled then prop.className "disabled"
]
This does currently not work in JSX syntax. This issue arises due to JSX output syntax where conditional properties are done like this:
<div>
data-active={isActive ? true : undefined}
className={isDisabled ? "disabled" : undefined}
</div>
Following this line of thought, in which the property key is always present we need to shift the conditional inside the property value. So the correct way to do conditional properties in JSX syntax is:
Html.div [
prop.custom("data-active", isActive)
prop.className(if isDisabled then "disabled" else "")
]
Using template strings
This is based on the following blog post by @alfonsogcnunez
Using f# template strings you can write JSX directly in your f# code. The syntax used is very similiar to the one introduced by Lit, which is a popular library for building web components.
We use Html.jsx $""" .. """
to define a JSX template. Inside the template you can use ${ ... }
to insert f# expressions.
Hello from JSX Template!
This is a paragraph inside a div.
Counter - Reactivity Test
- No items
Show code
- RawJSXTemplate.fs
- RawJSXTemplate.jsx
module Examples.Guides.JSXTemplate
open Feliz
open Feliz.JSX
[<ReactComponent(true)>]
let TestComponent() =
let counter, setCounter = React.useState(0)
let ChildList =
if counter = 0 then
Html.jsx $"""
<li>No items</li>
"""
else
React.Fragment [
for i in 1..counter do
Html.jsx $"""
<li key={i} id={sprintf "item-%d" i}>Item {i}</li>
"""
]
Html.jsx
$"""
<div>
<h1>Hello from JSX Template!</h1>
<p>This is a paragraph inside a div.</p>
<div>
<h1>Counter - Reactivity Test</h1>
<button onClick={fun _ -> setCounter(counter + 1) } >Increment</button>
<ul>
{ChildList}
</ul>
</div>
</div>
"""
import { Fragment, createElement, useState } from "react";
import React from "react";
import { map, delay, toList } from "../../fable_modules/fable-library-js.5.0.0-alpha.14/Seq.js";
import { printf, toText } from "../../fable_modules/fable-library-js.5.0.0-alpha.14/String.js";
import { rangeDouble } from "../../fable_modules/fable-library-js.5.0.0-alpha.14/Range.js";
import { defaultOf } from "../../fable_modules/fable-library-js.5.0.0-alpha.14/Util.js";
export function TestComponent() {
let xs;
const patternInput = useState(0);
const counter = patternInput[0] | 0;
return <div>
<h1>Hello from JSX Template!</h1>
<p>This is a paragraph inside a div.</p>
<div>
<h1>Counter - Reactivity Test</h1>
<button onClick={(_arg) => {
patternInput[1](counter + 1);
}} >Increment</button>
<ul>
{(counter === 0) ? <li>No items</li>
: ((xs = toList(delay(() => map((i) => <li key={i} id={toText(printf("item-%d"))(i)}>Item {i}</li>
, rangeDouble(1, 1, counter)))), createElement(Fragment, defaultOf(), ...xs)))}
</ul>
</div>
</div>
;
}
export default TestComponent;
Editor support
To get syntax highlighting and intellisense support in VSCode you can use the plugin Highlight templates in F#.
Make sure to follow the exact syntax for template strings to get hightlighting and intellisense support.
// This must be one line with a linebreak after the $"""
Html.jsx $"""
<div>
<h1>Hello from JSX Template!</h1>
<p>This is a paragraph inside a div.</p>
</div>
"""
// The closing """ must be on a new line