Feliz.UseElmish 
Besides being able to use Feliz in existing Elmish applications, you can also use Elmish as part of your Feliz application. This is a different approach to building standalone React components that use Elmish internally to manage the state of the component but from the perspective of the consumer, it is just another React component.
This approach simplifies the original Elmish model where the application state is explicitly passed down in parts to children and events are passed up to the parent components.
The implementation of this approach is made possible using a React hook called React.useElmish
. The following examples demonstrate how to use it:
Install into your project
dotnet add package Feliz.UseElmish
or
dotnet femto install Feliz.UseElmish
Feliz.UseElmish does not support Server-Side Rendering (SSR). Help me out by contributing a PR if you need this feature.
Here is an example to demonstrate how to build such component:
module Example.ElmishCounter
open Fable.Core
open Feliz
open Feliz.UseElmish
open Elmish
type Msg =
| Increment
| Decrement
type State = { Count : int }
let init() = { Count = 0 }, Cmd.none
let update msg state =
match msg with
| Increment -> { state with Count = state.Count + 1 }, Cmd.none
| Decrement -> { state with Count = state.Count - 1 }, Cmd.none
[<Erase; Mangle(false)>]
type Main =
[<ReactComponent(true)>]
static member Main() =
let state, dispatch = React.useElmish(init, update, [| |])
Html.div [
Html.h1 state.Count
Html.button [
prop.text "Increment"
prop.onClick (fun _ -> dispatch Increment)
]
Html.button [
prop.text "Decrement"
prop.onClick (fun _ -> dispatch Decrement)
]
]
The difference here from a full-fledged Elmish applications is that there isn't an "Elmish entry point" to run the component and manage its life-cycle. Instead, the React.useElmish
hooks manages the Elmish life-cycle internally within the React component so that it can run standalone inside other React components:
[<ReactComponent>]
let Counters() =
Html.div [
Counter()
Counter()
Counter()
]
When you need to trigger events from such an Elmish component, use React patterns where you pass a callback via the props instead of passing the dispatch
function from the parent component.
Understading the dependencies array
It is also important to understand the dependencies array of the React.useElmish
function
// dependencies array
// |
// |
// ↓
let state, dispatch = React.useElmish(init, update, [| |])
This array is responsible for the re-initialization of the component. For example, if your mini Elmish component loads user profile based on an input user ID like this:
[<ReactComponent>]
let UserProfile(userId: int) =
// will initialize once even if the component is re-rendered using a different userId
let state, dispatch = React.useElmish(init userId, update, [| |])
renderUserProfile state disptch
Then you must add the userId
to the dependencies array so that the hook knows to call init
again and re-initialize the component:
[<ReactComponent>]
let UserProfile(userId: int) =
// inititialization dependency
// |
// |
// |
// now every time this component is rendered using |
// a different userId, it will reinitialize the component ↓
let state, dispatch = React.useElmish(init userId, update, [| box userId |])
renderUserProfile state disptch
// Here, we are using a router so that every time the URL changes
// say from /user/20 to /user/21 then the UserProfile will be reload that user
open Feliz.Router
[<ReactComponent>]
let App() =
let currentUrl, updateCurrentUrl = React.useState(Router.currentUrl())
React.router [
router.onUrlChanged updateCurrentUrl
router.children [
match currentUrl with
| [ "user"; Route.Int userId ] -> UserProfile(userId)
| _ -> Html.h1 "Not found"
]
]
open Browser.Dom
ReactDOM.render(App(), document.getElementById "feliz-app")
The dependencies array follows the same rules of
React.useEffect
Combining with other hooks
Next, let's combine this hook with other React hooks such as React.useState
and React.useEffect
:
module Example.ElmishCounterSubscription
open Fable.Core
open Feliz
open Feliz.UseElmish
open Elmish
open System
open Fable.Core.JS
type Msg =
| Increment
| Decrement
type State = { Count : int }
let init() = { Count = 0 }, Cmd.none
let update msg state =
match msg with
| Increment -> { state with Count = state.Count + 1 }, Cmd.none
| Decrement -> { state with Count = state.Count - 1 }, Cmd.none
[<Erase; Mangle(false)>]
type Main =
[<ReactComponent(true)>]
static member Main() =
let localCount, setLocalCount = React.useState(0)
let state, dispatch = React.useElmish(init, update, [| |])
let subscriptionId = React.useRef(0)
let subscribeToTimer() =
// start the ticking
let rec loop = fun () ->
// dispatch Increment every second
dispatch Increment
console.log("Incremented count")
let id = setTimeout loop 1000
subscriptionId.current <- id
loop()
// return IDisposable with cleanup code
{ new IDisposable with member this.Dispose() = clearTimeout(subscriptionId.current) }
React.useEffect(subscribeToTimer, [| |])
Html.div [
Html.h1 (state.Count + localCount)
Html.button [
prop.text "Increment"
prop.onClick (fun _ -> dispatch Increment)
]
Html.button [
prop.text "Decrement"
prop.onClick (fun _ -> dispatch Decrement)
]
Html.button [
prop.text "Increment local state"
prop.onClick (fun _ -> setLocalCount(localCount + 1))
]
]
Disposing of resources
Documentation/Samples WIP
TL;DR: Have your
State/Model
type implementIDisposable
andReact.useElmish
will take care of calling the dispose function when the component unmounts.