Introduction
A couple of weeks ago Facebook presented React hooks, a new way of accessing React features inside functional components.
In this blogpost I would like to show that the new hooks can easily be used in combination with Fable.
Disclaimer: Hooks are an experimental proposal to React, currently available in 16.7 alpha
, and the api is not final yet. Use at your own risk.
useState()
A first example of a hook is useState().
type SetState<'t> = 't -> unit
let useState<'t> (t: 't) : ('t * SetState<'t>) = import "useState" "react"
When the useState
hook is called, we receive a variable representing the state and a function to update the state.
Example:
let (name, setName) = useState("nojaf")
Note that useState
accepts a default value for the state.
To demonstrate this hook, we will create a todo list application.
custom useInputValue hook
useState
can also be used as a building block to create our own hook:
let useInputValue (initialValue : string) =
let (value, setValue) = useState (initialValue)
let onChange (e : Fable.Import.React.FormEvent) =
let value : string = e.target?value
setValue (value)
let resetValue() = setValue (System.String.Empty)
value, onChange, resetValue
useInputValue
provides an easy way to capture text from an input field.
type FormProps = { OnSubmit : string -> unit }
let formComponent (props : FormProps) =
let (value, onChange, resetValue) = useInputValue ""
let onSubmit (ev : FormEvent) =
ev.preventDefault()
props.OnSubmit(value)
resetValue()
form [ OnSubmit onSubmit; ] [
input [ Value value; OnChange onChange; Placeholder "Enter todo"; ClassName "input" ]
]
formComponent
is an ideal dumb component to which we can add our todo from our smart component.
Top level component
let appComponent() =
let (todos, setTodos) = useState<Todo list> ([])
let toggleComplete i =
todos
|> List.mapi (fun k todo ->
if k = i then { todo with Complete = not todo.Complete } else todo
)
|> setTodos
let renderTodos =
todos
|> List.mapi (fun idx todo ->
let style =
CSSProp.TextDecoration(if todo.Complete then "line-through" else "")
|> List.singleton
let key = sprintf "todo_%i" idx
div [ Key key; OnClick(fun _ -> toggleComplete idx) ] [
label [ClassName "checkbox"; Style style] [
input [Type "checkbox"; Checked todo.Complete; OnChange(fun _ -> toggleComplete idx)]
str todo.Text
]
]
)
let onSubmit text =
{ Text = text; Complete = false }
|> List.singleton
|> (@) todos
|> setTodos
div [] [
h1 [ Class "title is-4" ] [ str "Todos" ]
ofFunction formComponent { OnSubmit = onSubmit } []
div [ClassName "notification"] renderTodos
]
Notice how we can use hooks at multiple levels and how well this works together with F#.
useReducer()
The next hook we will explorer is useReducer().
type ReduceFn<'state,'msg> = ('state -> 'msg -> 'state)
type Dispatch<'msg> ='msg -> unit
let useReducer<'state,'msg> (reducer: ReduceFn<'state,'msg>) (initialState:'state) : ('state * Dispatch<'msg>) = import "useReducer" "react"
This has an extremely Elm/Redux/Elmish vibe to it, so let’s build the mandatory counter example. The code is so self-explanatory that you should instantly see where this is going.
type Msg =
| Increase
| Decrease
| Reset
type Model = { Value: int }
let intialState = { Value = 0}
let update model msg =
match msg with
| Increase -> { model with Value = model.Value + 1}
| Decrease -> { model with Value = model.Value - 1}
| Reset -> intialState
let reducerComponent () =
let (model, dispatch) = useReducer update intialState
div [] [
button [ClassName "button"; OnClick (fun _ -> dispatch Increase)] [str "Increase"]
button [ClassName "button"; OnClick (fun _ -> dispatch Decrease)] [str "Decrease"]
button [ClassName "button"; OnClick (fun _ -> dispatch Reset)] [str "Reset"]
p [ClassName "title is-2 has-text-centered"] [sprintf "%i" model.Value |> str]
]
And there you have it. I believe this will be a nice alternative to Elmish if you are building something small.
useEffect()
Lastly, a demo on useEffect(), think of it as a way to launch side effects when a component renders.
let useEffect (effect: (unit -> U2<unit, (unit -> unit)>)) (dependsOn: obj array) : unit = import "useEffect" "react"
We would like to download a list of github repositories of a selected user/organization.
For the download itself we use fetch
from Fable.PowerPack.
let githubUsers =
[ "fable-compiler"; "fsprojects"; "nojaf" ]
let decodeRepoItem =
Decode.field "name" Decode.string
let decodeResonse = Decode.array decodeRepoItem
let loadRepos updateRepos user =
let url = sprintf "https://api.github.com/users/%s/repos" user
Fetch.fetch url []
|> Promise.bind (fun res -> res.text())
|> Promise.map (fun json -> Decode.fromString decodeResonse json)
|> Promise.mapResult updateRepos
|> ignore
This code will execute the updateRepos
function when the download was successful and the decoding of the json worked out.
let effectComponent() =
let options =
githubUsers
|> List.map (fun name ->
option [ Value name; Key name ] [ str name ]
)
|> (@) (List.singleton (option [Value ""; Key "empty"] []))
let (selectedOrg, setOrganisation) = useState ("")
let (repos, setRepos) = useState(Array.empty)
let onChange (ev : FormEvent) = setOrganisation (ev.Value)
useEffect (fun () ->
match System.String.IsNullOrWhiteSpace(selectedOrg) with
| true -> setRepos Array.empty
| false -> loadRepos setRepos selectedOrg
|> U2.Case1
) [| selectedOrg |]
let repoListItems =
repos
|> Array.sortWith (fun a b -> String.Compare(a,b, System.StringComparison.OrdinalIgnoreCase))
|> Array.map (fun r -> li [Key r] [str r])
div [ClassName "content"] [
div [ClassName "select"] [
select [ Value selectedOrg; OnChange onChange ] options
]
ul [] repoListItems
]
Let us take a closer look to the useEffect
code:
useEffect (fun () ->
match System.String.IsNullOrWhiteSpace(selectedOrg) with
| true -> ()
| false -> loadRepos setRepos selectedOrg
|> U2.Case1
) [| selectedOrg |]
The first argument of useEffect
takes a function that returns unit
or a new function that returns unit
. The latter can be used to clean up the effect, if for example we were to wire some event handlers.
That array we pass as second argument with the value of selectedOrg
, serves to tell the hook that we only need to re-evaluate when that value changes.
The Source
All samples can be found on github, or you can try them online.
Remarks
- I liked how easy it was to create bindings for hooks. Fable clearly is a match for this.
- Hooks work really well for small application. I believe they might also shine in larger applications. To be investigated.
- You need React
16.7.0-alpha.0
to play with hooks. - If you are using webpack, hot module reloading and hooks don’t mix at the time of writing.
Final words
I hope you enjoyed this blogpost and it all makes sense to you. If you have any suggestions or questions, please leave a comment.
Yours truly,
nojaf
Photo by Alan Bishop