I’ve had to implement the same thing just a week ago, so I think I can help 
If you change the underlying Monad for your top-level Halogen component you’ll need to use the hoist function to transform it back into Aff before handing it off to runUI.
For your example you’d need to write:
runAppM :: forall a. AppEnv -> AppM a -> Aff a
runAppM env (AppM m) = runReaderT m env
and then change your main from:
app <- runUI R.router location.path body
to:
app <- runUI (H.hoist (runAppM appEnv) R.router) location.path body
Now you can change your Router component from:
router :: H.Component HH.HTML Query String Message Aff
to
router :: H.Component HH.HTML Query String Message AppM
One problem you’ll run into is that you can’t actually construct the AppEnv the way it’s set up just yet. It needs a reference to the driver (you called it app) that is returned from runUI in order to send routing events into the application, but at the same time you need to construct an AppEnv to even get at that app value.
The way I solved it is to change the Env into navigate :: Route -> Ref (Effect Unit), fill it with a dummyRouter that just logs out the Routes its supposed to go to (might help you debug later on if you run into bugs) and then overwrite it with the proper Router as soon as the app started up:
-- in main
navigate ← liftEffect (Ref.new dummyRouter)
let env = AppEnv { navigate }
body ← awaitBody
let ui = H.hoist (runAppM env) R.router
io ← runUI ui unit body
liftEffect (Ref.write (mkRouter io) router)
where mkRouter might look something like this:
mkRouter ∷ HalogenIO Query Void Aff → Route → Effect Unit
mkRouter io r = do
pushHash (printRoute r)
launchAff_ case r of
Route1 → do
io.query $ H.action LoadRoute1
Route2 someData → do
io.query $ H.action $ LoadRoute2 someData
One more thing I did to make it a little nicer to use this NavigateDSL interface, was to use instance chains to automatically lift it through HalogenM (this allows you to freely mix calls to navigate with H.modify without lifting) and to implement it in terms of MonadAsk which makes it easier to use in a testing scenario (as long as your test monad implements the right MonadAsk you don’t need to write another instance):
class Monad m ⇐ NavigateDSL m where
navigate ∷ Route → m Unit
instance halogenMNavigateDSL ∷ NavigateDSL m ⇒ NavigateDSL (HalogenM s f ps o m) where
navigate = HalogenM ∘ liftF ∘ Lift ∘ navigate
else instance mAskNavigateDSL ∷ (MonadAsk { router ∷ Ref (Route → Effect Unit) | e } m, MonadEffect m) ⇒ NavigateDSL m where
navigate r = do
router ← liftEffect ∘ Ref.read =<< asks _.router
liftEffect (router r)
In order to make this work you’ll also need to implement MonadAsk and MonadEffect for your AppM:
import Type.Equality as TE
derive newtype instance monadEffectAppM :: MonadEffect AppM
instance monadAskAppM ∷ TE.TypeEquals e AppEnv ⇒ MonadAsk e AppM where
ask = AppM (map TE.from ask)
I’m sure this was a lot and I’m not sure if I forgot something, but maybe you can use this as a starting point and let me know if you run into problems.