DEV Community

Ken Aguilar
Ken Aguilar

Posted on • Edited on

Purescript - React Basic Hooks Routing

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.

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 derive instance genericRoute :: Generic Route _ derive instance eqRoute :: Eq Route derive instance ordRoute :: Ord Route instance showRoute :: Show Route where show = genericShow routeCodec :: RouteDuplex' Route routeCodec = root $ sum { "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 = React.component "Store" \props -> React.do r <- React.useContext context route /\ setRoute <- React.useState r React.useEffect route $ matchesWith ( parse routeCodec ) \mOld new -> do when ( mOld /= Just new ) $ setRoute $ const new 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 navigate = setHash <<< print routeCodec 

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 main = do mApp <- getElementById "app" =<< ( map toNonElementParentNode $ document =<< window ) case mApp of Nothing -> throw "App element not found." Just app -> do mainComponent <- Router.mkComponent R.render ( element mainComponent {} ) app 

References

Purescript Halogen Realworld

React Basic Hooks

Routing Duplex

Routing

Tagless Final Encoding by Juan Pablo Royo

Introduction to Tagless Final by Serokell

Top comments (0)