One of the toughest parts of any frontend application is effective component communication. There are a number of ways for components to communicate from prop drilling, context or even external tools like Redux. So when I moved to the SAFE stack, it seemed that the parent model handled everything and state was being passed all the way down to the components that needed it. Similar to prop drilling in React which is why global state management tools exist.
In this post I'd like to learn how we can use context to stop passing data all the way down the component tree.
Below is a common structure of an application where we have root level children Home
and Login
which are sibling components and a nested component DisplayUser
that belongs to Home
.
- Index
- Home
- DisplayUser
- Login
Typically in an Elmish app to pass data from Login
to DisplayUser
we would have to:
- Pass the data back up to
Index
(parent) - Then to
Home
- Finally to
DisplayUser
Goal
To be able to update the value of the User
from Login
and pass that data directly to DisplayUser
skipping Home
.
First we'll define a User
and create context:
type User =
| User of username: string
| Guest
let userContext = React.createContext()
I'm defining each component as a module so you can lift them into their own files if you want to. In the Feliz docs there's an example of how to structure a project that uses context.
module DisplayUser =
[<ReactComponent>]
let render () =
let (user, _) = React.useContext(userContext)
Html.div [
Html.p "Inner page has access to the user value"
match user with
| User name -> Html.p $"Welcome {name}, you are a User"
| Guest -> Html.p "Welcome, you are a Guest"
]
In DisplayUser
we use the useContext
hook and pass in the created context in order to gain access to a tuple. The first element is of the type User
and for now we can discard the second. We then display different content depending on the User
.
module Home =
[<ReactComponent>]
let render () =
Html.div [
Html.p "Home page doesn't contain user data"
DisplayUser.render()
]
Home
is just a wrapper to prove that it knows nothing of the User
.
module Login =
[<ReactComponent>]
let render () =
let (_, userDispatcher) = React.useContext(userContext)
let (input, setInput) = React.useState("")
Html.div [
Html.p "User Login"
Html.input [
prop.placeholder "Username"
prop.value input
prop.onChange setInput
]
Html.button [
prop.text "Login"
prop.onClick (fun _ -> input |> User |> userDispatcher)
]
]
Login
uses the second tuple value from the useContext
hook. The userDispatcher
is an updater function that will take a User
and update our model
. Anywhere using model.User
will be updated straight away once the button is clicked!
type Model = { User: User }
type Msg = SetUser of User
let init () : Model * Cmd<Msg> =
let model = { User = Guest }
model, Cmd.none
let update (msg: Msg) (model: Model) : Model * Cmd<Msg> =
match msg with
| SetUser user -> { model with User = user }, Cmd.none
Here we have our typical Elmish setup.
[<ReactComponent>]
let Provider user userDispatcher (children: ReactElement seq) =
React.contextProvider(userContext, (user, userDispatcher), children)
This is where the magic happens. We create a Provider
component that takes a User
, an updater function and ReactElement
s. Using contextProvider
we pass in our created context, the value we want our context to take (tuple of the user value and the updater) and the children. We could also use a record for the value:
React.contextProvider(UserContext, {| Value = user; Dispatcher = userDispatcher |}, children)
Now to setup the view with everything we've defined:
let view (model: Model) (dispatch: Msg -> unit) =
Bulma.box [
prop.children [
Provider model.User (SetUser >> dispatch) [
Home.render()
Login.render()
]
]
]
We're now able to update data in one part of our application and have another part reflect the changes, without the need for passing props up and down. 🙌
As always thanks to the F# slack for helping me put this post together. ❤