Application State
When we make apps using the SAFE stack, our client-side state is usually managed by Elmish.
One of the benefits of this ‘Model-View-Update’ (MVU) pattern is that having a single, immutable record of your app’s complete state makes it easy to reason about and debug.
Sometimes, however, we have user-interface components which require some persistent state.
Examples of this are pop-out menus and drop-down lists. Whether a drop-down is expanded usually has no relevance to an application. Storing this information in the application model can therefore feel inappropriate.
React
Whilst you can plug any compatible renderer into the Elmish loop, the most common (and included with the SAFE template) is React.
The view
function returns a standard React component, albeit defined in F# using Fable.React or Feliz, which is then converted to standard JS by Fable.
This means we have full access to React hooks such as useState
, which allow a view component to maintain some internal state outside of our Elmish loop.
Feliz even provides a useElmish
hook to attach a stand-alone MVU loop to the component. This could be useful for reusable, self-contained UI elements with more complex internal state management.
Implementing a Push Sidebar Menu
In this example, we will store a simple boolean value in a sidebar menu component which tracks whether it is open or closed.
We will use Feliz to define our React component as it has a concise and easy to understand syntax. You can of course use Fable.React instead if you prefer, and even mix and match between the two.
We will take the basic pattern for a push sidebar from this w3schools example.
Styling
We will essentially copy the styling from the example. Rather than write CSS directly, this is achieved using Feliz’s type-safe style API which provides great autocompletion and hints.
Here’s the menu button style:
let menuButtonStyle = [
style.position.absolute
style.top 0
style.right (length.px 10)
style.fontSize (length.px 36)
style.cursor.pointer
]
Next, we define the sidebar style. This will need to set its width dynamically, depending upon whether it is open or closed:
let sideNavWithWidth (width : double) = [
style.height (length.percent 100)
style.width (length.px width)
style.position.fixedRelativeToWindow
style.top 0
style.zIndex 1
style.left 0
style.backgroundColor (color.rgb (0,184,156))
style.overflowX.hidden
style.transitionDurationSeconds 0.5
]
Finally, we define the content container style. This must set its left margin dynamically, again depending on whether the menu is open or closed.
let containerWithMargin (margin : int) = [
style.transitionProperty [transitionProperty.marginLeft]
style.transitionDuration (TimeSpan.FromSeconds 0.5)
style.marginLeft margin
]
Function Component
Finally, we need to create the React component itself.
Traditionally, if we wanted to use React state we would create a class which inherits from React.Component
and override various lifecycle and render methods, which was rather cumbersome and not very functional.
Now however we can use Function Components which simplify things a great deal.
Using Feliz, we simply wrap our view elements in a call to React.functionComponent
, and then access hooks with React.useXXX
:
let view =
React.functionComponent(fun {props} ->
let (currentState, setState) = React.useState({initial state})
{view code}
)
With Fable 3, Feliz uses an attribute
[<ReactComponent>]
rather than theReact.functionComponent(...)
syntax. At the date of publication, the SAFE template is still using Fable 2 so I have shown the legacy approach here.
Putting it all together
We now have all the parts we need to create our push menu.
To recap, the goals are
- Have a sidebar that becomes wider when the menu is open (and vice versa).
- Have a container around the main website content which increases its left margin by the same amount, so that it appears to be ‘pushed’ over by the menu.
- Have the menu keep track of its open / closed state.
Here is the complete code:
let view =
React.functionComponent(fun (props: {| content : ReactElement seq |}) ->
let (isOpen, setOpen) = React.useState(false)
Html.div [
Html.div [
prop.style (sideNavStyle(if isOpen then 250.0 else 68.0))
prop.children [
Html.div[
prop.style menuButtonStyle
prop.onClick (fun _ -> setOpen (not isOpen))
prop.text "🍔"
]
]
]
Html.div [
prop.style (containerStyle(if isOpen then 250 else 68))
prop.children [ yield! props.content ]
]
])
// have to wrap in an object here, using the seq directly errors
let inline menu content = view {| content = content |}
Full example
I have taken the out-of-the-box Todo application provided by the SAFE template and added this menu to it as a worked example, which you can find on our Github.
Conclusion
I hope to have demonstrated that creating custom user interface components and styles using React is easy to do, using the same techniques as a standard JS application, and with all the added power of F#!