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 {} )
= do
mkComponent -- Grab initial route.
-- This will try to match the browser's hash route.
<- hush <<< ( parse routeCodec ) <$> getHash
mInitialRoute -- If it doesn't find a match it will default to the home route.
-- Then a context is created on that route.
<- React.createContext ( fromMaybe Home mInitialRoute )
routeContext <- mkRouteStore routeContext
store <- mkRouter routeContext
nav "RouterContainer" \props -> do
React.component 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 {} )
= do
mkRouter routeContext <- Home.mkComponent
home <- About.mkComponent
about <- mkNavbar
navbar "Router" \props -> React.do
React.component <- React.useContext routeContext
route pure
$ React.fragment
[ React.element navbar {}case route of
, Home -> React.element home {}
About -> React.element about {}
]
mkNavbar :: Effect ( ReactComponent {} )
=
mkNavbar "Navbar" $ const $ do
React.component pure
$ RD.nav
:
{ children
[ RD.button: [ RD.text "Home" ]
{ children: RE.handler_ $ navigate Home
, onClick
}
, RD.button: [ RD.text "About" ]
{ children: RE.handler_ $ navigate About
, onClick
}
] }
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.
module Service.Route where
import Prelude hiding ((/))
-- Generic
import Data.Generic.Rep ( class Generic )
import Data.Generic.Rep.Show ( genericShow )
-- Routing
import Routing.Duplex
import Routing.Duplex.Generic
import Routing.Duplex.Generic.Syntax ( (/) )
-- All possible routes in the application
data Route
= Home
| About
instance genericRoute :: Generic Route _
derive instance eqRoute :: Eq Route
derive instance ordRoute :: Ord Route
derive
instance showRoute :: Show Route where
show = genericShow
routeCodec :: RouteDuplex' Route
= root $ sum
routeCodec "Home": noArgs
{ "About": "about" / noArgs
, }
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
.
module Component.Store where
import Prelude
import Data.Maybe ( Maybe(..) )
-- Internal Service
import Service.Route
-- Effect
import Effect ( Effect )
-- Routing
import Routing.Hash ( matchesWith )
import Routing.Duplex ( parse )
-- React
import React.Basic.Hooks ( ReactComponent, ReactContext, (/\), JSX )
import React.Basic.Hooks as React
mkRouteStore :: ReactContext Route -> Effect ( ReactComponent { content :: Array JSX } )
=
mkRouteStore context "Store" \props -> React.do
React.component <- React.useContext context
r /\ setRoute <- React.useState r
route $ matchesWith ( parse routeCodec ) \mOld new -> do
React.useEffect route /= Just new ) $ setRoute $ const new
when ( mOld pure
$ React.provider context route props.content
Navigation
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.
module Service.Navigate where
import Prelude
-- Internal Service
import Service.Route
-- Effect
import Effect ( Effect )
-- Routing
import Routing.Duplex
import Routing.Hash
class Monad m <= Navigate m where
navigate :: Route -> m Unit
instance navigateEffect :: Navigate Effect where
= setHash <<< print routeCodec navigate
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.
module Main where
import Prelude
import Data.Maybe ( Maybe(..) )
-- Web
import Web.DOM.NonElementParentNode ( getElementById )
import Web.HTML.HTMLDocument ( toNonElementParentNode )
import Web.HTML.Window ( document )
import Web.HTML ( window )
-- Internal
import Component.Router as Router
-- Effect
import Effect ( Effect )
import Effect.Exception ( throw )
-- React
import React.Basic.Hooks ( element )
import React.Basic.DOM as R
main :: Effect Unit
= do
main <- getElementById "app" =<< ( map toNonElementParentNode $ document =<< window )
mApp case mApp of
Nothing -> throw "App element not found."
Just app -> do
<- Router.mkComponent
mainComponent R.render ( element mainComponent {} ) app