Purescript - React Basic Hooks Routing

Posted on March 26, 2020

When I started doing web applications with purescript react and halogen I had no clue how to do things. One of those things is routing. It wasn’t that hard for halogen because of the realworld example. It’s built ready for production. It even has great documentation!

Unfortunately, when it came to react-basic-hooks this information wasn’t readily available. So here’s my take on routing with react-basic/react-basic-hooks.

The router component is the parent of all the components. The router component will decide which component to render depending on the Route.

Router

module Component.Router where

import Prelude
import Data.Either ( hush )
import Data.Maybe ( fromMaybe )
-- Internal Page
import Page.Home as Home
import Page.About as About
-- Internal Service
import Service.Route
import Service.Navigate
-- Internal Component
import Component.Store ( mkRouteStore )
-- Effect
import Effect ( Effect )
-- Routing
import Routing.Duplex ( parse )
import Routing.Hash ( getHash )
-- React
import React.Basic.Hooks ( ReactComponent, ReactContext )
import React.Basic.Hooks as React
import React.Basic.DOM as RD
import React.Basic.Events as RE

mkComponent :: Effect ( ReactComponent {} )
mkComponent = do
  -- Grab initial route. 
  -- This will try to match the browser's hash route. 
  mInitialRoute <- hush <<< ( parse routeCodec ) <$> getHash
  -- If it doesn't find a match it will default to the home route.
  -- Then a context is created on that route.
  routeContext <- React.createContext ( fromMaybe Home mInitialRoute )
  store <- mkRouteStore routeContext
  nav <- mkRouter routeContext
  React.component "RouterContainer" \props -> do
    pure $ React.element store { content: [ React.element nav {} ]}

-- This is the function that will match Route and render the right element that
-- matches that route.
mkRouter
  :: ReactContext Route
  -> Effect ( ReactComponent {} )
mkRouter routeContext = do
  home <- Home.mkComponent
  about <- About.mkComponent
  navbar <- mkNavbar
  React.component "Router" \props -> React.do
    route <- React.useContext routeContext
    pure
      $ React.fragment
        [ React.element navbar {}
        , case route of
             Home -> React.element home {}
             About -> React.element about {}
        ]

mkNavbar :: Effect ( ReactComponent {} )
mkNavbar =
  React.component "Navbar" $ const $ do
    pure
      $ RD.nav
        { children:
          [ RD.button
            { children: [ RD.text "Home" ]
            , onClick: RE.handler_ $ navigate Home
            }
          , RD.button
            { children: [ RD.text "About" ]
            , onClick: RE.handler_ $ navigate About
            }
          ]
        }

Route

This is how Route is defined. It’s a sum type of all possible routes in the application. The rest of this code is definition for the routing-duplex interpreter and printer. The routes can be directly written as strings but safety with types is what I prefer; routing and routing-duplex provide that for me.

Page

The page components are defined here. They’re trivially defined components that will display the text “Home” and “About”. In a non-trivial app, these would be the components that will encapsulate an entire page.

Route Store

This is the component that will watch the route changes. Everytime the hash route changes, it will run setRoute and updates the Route. This component will then pass it on to its content.

The only capability of this app is navigation, but if there are other capabilities like requesting data, logging, and authentication it will also be defined similar to this.

I thought this was a great article on tagless-final-encoding. This is the technique being used here. Code re-use can be easier achieved with this technique because I don’t have to change big chunks of the app if I need to implement it in another context. This app runs on Effect so I only have to define an instance for that. If the application needs to run on Aff then I’ll define a new instance for Aff

React runs on Effect so that’s why I’ve defined an Effect instance.

Main

Finally, the Main module. This is where purescript-react-basic-hooks runs application. Nothing really special, it looks for an element with id of app then appends the application to that DOM node.

Sources

Purescript Halogen Realworld

React Basic Hooks

Routing Duplex

Routing

Tagless Final Encoding by Juan Pablo Royo

Introduction to Tagless Final by Serokell