United Kingdom: +44 (0)208 088 8978

Using F# to build React apps: npm packages

Did you know that you can use F# to build React apps? The third post in our series on that topic focuses on how to use npm packages from F#.

We're hiring Software Developers

Click here to find out more

Using F# to build React apps: npm

This is the third post in a series about how to build React apps using F#, covering a few topics:

  1. Generating JS from F#
  2. Writing React code from F#
  3. Using npm packages from F# (this post)
  4. Using the Elm architecture in your React components

In this post, we'll see how to call code from npm packages in F# and that there's a healthy ecosystem making it easy to use popular npm packages. Let's go!

Using npm packages from F#

So far in this series, we've discussed how you can use the Fable compiler to transpile F# to JavaScript, and how the Feliz library lets you easily create React components using F#. At this point, you may be convinced of the benefits of writing React code in F#, but concerned that doing so might make it hard to use the npm packages that make working in the JS and React ecosystems so productive. In this post, you'll see that using your favourite npm packages from F# is easy! There a few different options to choose from, which we'll look at in turn, discussing the pros and cons.

The code to get the below examples working can be found in the accompanying GitHub repo for this post. We've opted to get things running using Vite.

Importing a named function

npm packages often export named functions and importing them into your Fable code is easy. As an example, we'll use the uuid npm package, which allows generating new UUIDs on demand. After installing it via npm (npm install uuid), it's possible to call its function called "v4" from F# (there are different versions of UUIDs):

open Fable.Core
open Fable.Core.JsInterop

// Import the v4 function from the uuid package.
let [<Import("v4", from="uuid")>] uuidv4 : unit -> string = jsNative

// Use the imported uuidv4 function from F# to create a new UUID.
let myUuid = uuidv4()

// Print the result.
printfn $"Generated UUID: %s{myUuid}"

The bit between square brackets is an annotation for the uuidv4 function that we're defining, to direct Fable on what to do when executing JS. : unit -> string is us saying that uuidv4 takes () as input and returns a string. jsNative is a placeholder function that satisfies the compiler and throws an error if you accidentally run this code in .NET rather than a JS runtime. It is erased during Fable compilation, and the function from the npm package is called instead.

When the generated JS script is loaded, you will see output like the following in your browser's console.

Generated UUID: 65d729e8-8b5d-446e-8212-99c171e49b46

Calling a function that takes in POJOs

Sometimes the packages that you load contain functions that require POJOs (plain old JavaScript objects) as arguments. There are a variety of ways to create POJOs from F#; one simple way is to use F#'s anonymous records:

// Import lodash. The import function behaves very similarly to the Import
// attribute in the above snippet. See
// https://fable.io/docs/javascript/features.html#imports for more details.
let lodash: obj = import "default" "lodash"

// Create two anonymous records (POJOs).
let obj1 = {| name = "Johan"; age = 30 |}
let obj2 = {| age = 31; city = "Nairobi" |}

// Merge the objects using lodash, from F#.
let merged : obj = lodash?merge obj1 obj2

// Print the result
JS.console.log merged

The output in your browser's console is what you would expect:

{age: 31, name: 'Johan', city: 'Nairobi'}

Note that we called the merge function via lodash?merge. The ? operator allows dynamic access; basically, this is just us saying to the compiler "trust me, there's a merge field on the lodash object that is a function that takes in two objects and returns another object." While this can be handy for quick experimentation, it's not making the best use of F#'s strengths as a statically-typed programming language. Let's look at how we can add type safety in the next section.

Adding type safety with F# types

Instead of using the ? operator that Fable provides, we can instead add a type annotation to the value that we bind when importing from packages:

// Define an interface for the Lodash import.
type Lodash =
    abstract snakeCase: string -> string

// Import lodash, telling the compiler that _l matches the new interface.
[<Import("default", "lodash")>]
let _l: Lodash = jsNative

// Use the snakeCase function safely - the compiler checks that the usage
// is consistent with the Lodash interface that we declared.
let sc = _l.snakeCase "A sentence with some camelCase words."
JS.console.log sc // Prints "a_sentence_with_some_camel_case_words".

Because we're using . rather than ? when calling _l.snakeCase, the compiler checks that snakeCase is a valid method on _l (based on what the Lodash type supports), and that its first (only in this case) parameter is a string. It knows that the value returned is a string too. This makes it much safer to then take the values that library functions return and use them in more complex F# code.

Of course, it's still possible to make a mistake when defining the interface: the F# compiler checks that you're using the Lodash type that you made correctly, not that the default export from the lodash package has a function called snakeCase. The benefit of the interface is that you only have to get the binding right in one place; with the ? operator, every place where you want to call out to imported code needs to be checked separately.

One concern that you might have is that adding new F# types might clutter your JS bundle. In this case it's not a problem because interface types are automatically erased by Fable. For other types, know that you can use the Fable REPL to inspect what types are being generated in your Fable code, and use Fable's Erase attribute to ensure that the types are only used for F# type checking, and not adding weight to your JS bundle.

Another thing that you might be thinking at this point is that you don't want to write the interface yourself for everything that you want to import. There are a few things that mean that this is not as bad as it first seems:

  • You only need to cover the parts of the interface that you're using.
  • In large applications, you'll typically only write these bindings once and benefit from them across many places.
  • The Fable community has created a clever way to share the bindings for npm packages as NuGet packages (the equivalent of npm packages in the .NET ecosystem).
  • The Glutinum CLI, a tool for converting TS definitions (.d.ts files) into Fable bindings, is under active development.

Using wrappers provided by the F# community

Thanks to the Fable wrapper packages that the community have provided, you can use Bulma or DaisyUI, interact with a canvas and use chart.js. At Compositional IT, we've even made our own wrapper for AG Grid. Because all of these wrappers are open source, you and everyone else in the community can contribute to them, or create your own, to make the Fable ecosystem even richer!

There are even great tools like Femto that help you to ensure that the npm packages required by a Fable (NuGet) package are installed.

Summary

In this post, we've seen that using npm packages from F# code is easy. You can import named functions, use the ? operator for quick experimentation and bind imports to interfaces for type safety. Thanks to the wrapper packages provided by the wonderful Fable community, it's simple to get type-safe F# access to many popular npm packages.

The power of F# combined with the productivity of npm is a winning combination 💪 Try it yourself, you won't be disappointed!