United Kingdom: +44 (0)208 088 8978

Component communication using React Context

This week Akash takes a look at React Context to see if we can avoid passing data down the entire component tree.

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 ReactElements. 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. ❤