Fable.Store

Fable.store

Manage state logic in Svelte, React apps and more

Fable.Store

This repository was originally born as bindings to write Svelte apps with Fable, but has evolved into providing stores à-la-Svelte that can also be used with other UI rendering mechanisms like React, and also in non-Fable environments like dotnet console apps, Fabulous or Bolero.

What's a store?

A store is just an observable that keeps an internal value and exposes an Update function to change it and report to subscribers. Because it's just an observable, it's compatible with the Observable module in FSharp.Core and other Reactive libraries. But the stores have an additional interesting property: they report immediately their current value upon subscription, which makes them directly usable by UI components.

You may be thinking: "An update function? This looks like mutation, not very functional". You're right, this is why Fable.Store provides helpers to build stores after common functional patterns, like Elmish MVU, FRP with Fable.Reaction or async workflows to represent state matchines (based on an original idea by Tomas Petricek).

What's Svelte?

Svelte is a new way to write web apps in a declarative way without using a Virtual DOM. The best way to learn about Svelte and its merit is by checking their great website with interactive tutorials.

Svelte uses HTML-like templates. If you like it but prefer an all-F# approach, check David Dawkins awesome work with Sutil!

How do I use F#/Fable with Svelte?

Svelte has its own mechanism to create bindings, but it allows you to use external stores with a simple API for interaction, so you probably already guessed where we are heading to.

Setting up the project

First, if you are using VS Code, install the "Svelte for VS Code" extension which provides great tooling to work with .svelte files.

In your F# project, install the Fable.SvelteStore dependency (--prerelease at the time of writing). Then from npm also install svelte itself and the svelte-loader for Webpack. All the dependencies (both .NET and JS) can be installed automatically by using Femto.

In a Svelte-only app, you'll likely have a main .js file like this one pointing to the root Svelte component as the entry point in your Webpack config.

Interacting between .svelte and .fs files

Svelte uses files with the .svelte (surprise!) extension for component declaration. From these you can just import code from the JS files generated by Fable. We've found that an effective way is to export a function to make the logic store from F# and then import and call it from Svelte.

// TodoMVC.fs

let makeStore props =
  let store, dispatch = SvelteStore.makeElmish init update ignore props
  store, SvelteStore.makeDispatcher dispatch
// TodoMVC.svelte

<script>
    import { makeStore } from "./TodoMVC.fs.js";
    const [store, dispatch] = makeStore(props);
    // ...
</script>

Note Svelte uses a special syntax ($ prefix) to access the value from the store. Check their tutorial and the .svelte files in samples/App/src.

The dispatcher

Ok, so you can get the model from your F# logic to render the UI. But in most cases you will also want to listen to UI events and send messages to the logic. In Fable apps it's common to model the messages with a union type, but we cannot instantiate F# unions from JS (at least not easily). Using something like string literals plus an object array for the arguments is not ideal either. Fable.SvelteStore provides a makeDispatcher helper to create a JS object at compile-time (through a Fable plugin) from a dispatch: Msg -> unit function. Let's see it with an example, if we have the following F# code:

type Msg =
    | MouseDown of x:float * y:float * offsetX:float * offsetY:float
    | MouseMove of x:float * y:float
    | MouseUp

// ...

let dispatch (msg: Msg) = stream.Trigger(msg, store.update)

let dispatcher = SvelteStore.makeDispatcher dispatch

The following JS code will be generated:

export dispatcher = {
    mouseDown: (x, y, offsetX, offsetY) => dispatch(new Msg(0, x, y, offsetX, offsetY)),
    mouseMove: (x, y) => dispatch(new Msg(1, x, y)),
    mouseUp: () => dispatch(new Msg(2)),
};

Now we can easily send messages to F# from Svelte:

<svelte:body
    on:mousemove={(ev) => dispatch.mouseMove(ev.x, ev.y)}
    on:mouseup={(_) => dispatch.mouseUp()} />

Type checking

The dispatcher is still not super useful unless we have some help from the IDE to give us a warning when we make a misspelling in JS. To solve this we can use a Typescript .d.ts declaration file like this one:

// Dragging.fs.d.ts

import { Readable } from "svelte/store";

export function makeStore(): [Readable<{
    position: [number, number],
    offset: [number, number],
}>, {
    mouseDown: (x: number, y: number, offsetX: number, offsetY: number) => void,
    mouseMove: (x: number, y: number) => void,
    mouseUp: () => void,
}]

You can generate the .d.ts automatically by decorating the makeStore function with the SveltePlugins.GenerateDeclaration attribute. Just be aware the plugin may not handle complex cases.

With this, you can use Typescript in your .svelte file or just the // @ts-check declaration to get type checking even in Javascript!

// Dragging.svelte

// @ts-check
import { makeStore } from "./Dragging.fs.js";

let [store, dispatch] = makeStore();

// This gives an error because numeric arguments are expected
dispatch.mouseDown("foo")

Try it out!

Run the samples to see how all these pieces work together:

cd samples/App
npm install && npm start

Why would I want to use a store in non-Svelte apps?

The store allows to you link your logic directly to individual UI components instead of imposing a rigid structure for your whole app. If you're using the MVU pattern this is very similar to useElmish in Feliz apps, but the store provides you with more flexibility to integrate different patterns as well as to communicate with multiple UI components. Anyways, this belongs to a future, more detailed post. For now you can have a look at this React example.

Svelte looks cool, do I need to re-write my whole Fable app to use it?

No! Thanks to Zaid Ajaj you can easily integrate Svelte components into your Feliz/React apps using the SvelteComponent plugin. Check how it's done in samples/FelizSvelte!

Top categories

Loading Svelte Themes