United Kingdom: +44 (0)208 088 8978

F# wrappers for React components

In this series Akash builds an F# wrapper for react-d3-speedometer using Feliz!

Coming from a JS background I was spoiled with such a rich ecosystem of packages via npm. Pre-built components that work out the box with minimal effort so that I could focus on business logic.

Similarly the F# community have built incredible tools like Fable and Feliz that leverage the JS ecosystem by letting us build F# wrappers around React/JS components with full type safety!

The SAFE docs have recently been updated to show the steps needed to wrap react-d3-speedometer. In this series I'd like to flesh out the Feliz example and show how easy it can be to wrap and publish a React component for F#.

  1. Create a new SAFE app with dotnet new SAFE - depending on which SAFE template you're using you may or may not need to install Feliz. I'm using the latest version 3.0.0-beta004 which supports .NET 5 and Fable 3.

  2. Run npm install react-d3-speedometer at the root directory as specified in the SAFE docs

  3. In the Client directory create a new file Feliz.ReactSpeedometer.fs

  4. In Feliz.ReactSpeedometer.fs add the following:

module Feliz.ReactSpeedometer

open Feliz
open Fable.Core.JsInterop

let reactSpeedometer: obj = importDefault "react-d3-speedometer"

type ReactSpeedometer =

    static member inline Value (number: int) = "value" ==> number
    static member inline MinValue (number: int) = "minValue" ==> number
    static member inline MaxValue (number: int) = "maxValue" ==> number
    static member inline Segments (number: int) = "segments" ==> number

    static member inline create props = Interop.reactApi.createElement (reactSpeedometer, createObj !!props)
  1. If we head over to the Index.fs ensure Feliz.ReactSpeedometer is open and update the view function:
let view model dispatch =
    ReactSpeedometer.create [
        ReactSpeedometer.Value 10
        ReactSpeedometer.MaxValue 100
        ReactSpeedometer.MinValue 0
        ReactSpeedometer.Segments 4
    ]

That's the bare minimum we need to port over this component with type safety since we can never pass anything other than an int to the props we've defined.

But what if the props being passed to a component are more complicated? Luckily react-d3-speedometer has one such property - "customSegmentLabels".

Which from the prop list takes an Array<CustomSegmentLabel> and from the code is defined as:

enum CustomSegmentLabelPosition {
    Outside = "OUTSIDE",
    Inside = "INSIDE",
}

type CustomSegmentLabel = {
    text?: string
    position?: CustomSegmentLabelPosition
    fontSize?: string
    color?: string
}
  1. Add the F# translation of the above to Feliz.ReactSpeedometer.fs.
type CustomSegmentLabelPosition =
    | Outside
    | Inside
        with static member toJSValue = function 
            | Outside -> "OUTSIDE"
            | Inside -> "INSIDE"

type CustomSegmentLabel =
    { Text: string
      Position: CustomSegmentLabelPosition
      FontSize: string
      Color: string }
  1. Now we need to add the prop within the ReactSpeedometer type.
type ReactSpeedometer =
    ....
    static member inline CustomSegmentLabels (customSegmentLabels: CustomSegmentLabel []) = "customSegmentLabels" ==> customSegmentLabels
  1. Add our newest prop to the component in the view.
let view model dispatch =
    ReactSpeedometer.create [
        ReactSpeedometer.Value 10
        ReactSpeedometer.MaxValue 100
        ReactSpeedometer.MinValue 0
        ReactSpeedometer.Segments 4

        // new code to add below
        ReactSpeedometer.CustomSegmentLabels [|
            { Text = "Hi"
              Position = Inside
              FontSize = "20px"
              Color = "crimson" }
            { Text = "From"
              Position = Outside
              FontSize = "20px"
              Color = "blue" }
            { Text = "Feliz"
              Position = Inside
              FontSize = "20px"
              Color = "green" }
            { Text = "Speedometer"
              Position = Outside
              FontSize = "20px"
              Color = "purple" }
        |]
    ]

Annnd something is broken 😢, our awesome custom labels aren't showing?

This is because a F# record doesn't directly translate to a JS object. To overcome this we can add a member to CustomSegmentLabel which will map our F# record into an anonymous record with lower cased fields.

  1. Update the CustomSegmentLabel type to include the static member.
type CustomSegmentLabel =
    { Text: string
      Position: CustomSegmentLabelPosition
      FontSize: string
      Color: string }

        // new code to add below
        with static member toJSObj customLabel =
                {| text = customLabel.Text
                   position = customLabel.Position |> CustomSegmentLabelPosition.toJSValue
                   fontSize = customLabel.FontSize
                   color = customLabel.Color |}

You could alternatively define CustomSegmentLabel as an anonymous record with lower cased field names but I'd like to have the standard F# syntax when using a component and leave the JS Interop behind the scenes. So as eventual library authors it's on us to make the component a natural feel for F#.

  1. Finally, call the static member in the prop
static member inline CustomSegmentLabels (customSegmentLabels: CustomSegmentLabel []) =
    let jsCustomSegmentLabels = customSegmentLabels |> Array.map CustomSegmentLabel.toJSObj
    "customSegmentLabels" ==> jsCustomSegmentLabels

Now we've done it!

I hope you enjoyed this post and join me next time when we will publish this wrapper to Nuget. In the mean time try mapping some of the remaining props yourself, you can use the react-d3-speedometer storybook to help see the what props are left and how they're implemented.