fp-ts-routing-redux
This library presents three differrent integrations of fp-ts-routing into redux. Listed in order of increasing purity, they are routeMiddleware
, routeObservable
, and routeStream
. navigationMiddleware
is the only provided integration for navigation
Philosophy
The goal of this library is to represent routes in state, in the purely functional spirit of fp-ts-routing
Redux
is the purest state management system at the time of publication*
Additionally, Redux
manages a single global state, and since the current route is a global value, this is a good fit
On top of that, Redux
and fp-ts-routing
use ADTs in a usefully composable way (RouteAction
s and NavigationAction
s can compose Route
s), so it's a really good fit
Redux
manages a function from some arbitrary ADT to a state transformation
ADT => State => State
Except it's old so it's not curried so it looks like this instead
This function is called the reducer
, and the ADT is called an Action. Your reducer, along with initial state, is given to a simple state manager called the store
;
Doing this:
store.dispatchsomeActionADT
Will invoke our reducer
and trigger a global state transformation. This allows us to encapsulate our app's side effects into our reducer
Our example application's ADTs
ADT | Usage |
---|---|
MyRoute |
Used by fp-ts-routing to represent a route the browser can point to |
MyState |
Used by Redux to represent your app's global state |
MyAction |
Used by Redux to represent a state transformation |
RouteAction |
Used by fp-ts-routing-redux to represent a route event that can transform state with Redux |
ResponseAction |
Will be used later by redux-observable to represent the response of a fetch that can transform state with Redux |
Navigation |
Will be used later by fp-ts-routing-redux to represent a change to the browser's current URL |
For the sake of sanity, we will implement these ADTs using morphic-ts
// define our app's ADTs ;;
Redux
middleware
Handling route events with Redux can accept middlewares. This is a good fit for our router
; // handle our app's routing ;; ; // will invoke the store's `dispatch` on each new route event; ;
However, we often want to trigger asynchronous code as a result of a route event, which we are unable to do in our reducer
We must consider redux
asynchronous middlewares
redux-observable
Triggering asynchronous side-effects from route events with redux-observable
is the redux
asynchronous middleware that best fits our usage**
redux-observable
ties redux
together with rxjs
(the best streaming solution in typescript) with Epic
s that return Observable
s that are in turn subscribed to your store
's dispatch
with middleware
In fact, since our router
is naturally an Observable
, we can replace our routeMiddleware
with a routeObservable
We can map our RouteAction
s to make asynchronous calls to fetch
that push ResponseAction
s
We must push the original RouteAction
as well so we can update our Route
in our app's state
We can return routeObservable
from our Epic
to subscribe our RouteAction
s and ResponseAction
s to our dispatch
(RouteType
is just a wrapper for a history
action
with a less confusing name in this context)
; ; MyAction, ResponseAction, MyState> =myRouteObservable; ; ; epicMiddleware.runmyRouteEpic;
If we want to have other asynchonous side effects, Epic
s represent your redux
state
and action
as Observable
s called $state
and $action
. We can merge routeObservable
with whatever Observable
s you need
;
This is still impure. We are using side effects without safely demarcating them as IO. How would we mock this for testing?
@matechs/epics
Triggering asynchronous side-effects from route events with @matechs/effect
is part of the fp-ts
ecosystem that borrows concepts from scala ZIO that allow us to invoke syncronous and asynchronous side effects with Effect
s purely by separating them from their environments using Provider
s
A Stream
is an Effect
ful Observable
Our routeStream
is a Stream
that accepts a NavigationProvider
@matechs-epics
allows us to represent our redux-observable
Epic
as a Stream
So our routeStream
is a Stream
that, alongside our NavigationProvider
, goes inside an Epic
that goes inside redux-observable
middleware that goes inside redux
;;
We are able to easily mock our NavigationProvider
for testing
;// TODO - implement thisassert.deepStrictEqualTBD
Rerouting with redux
Since rerouting is simply a dispatch
ed side effect, we represent it as its own redux middleware
We can use an NavigationProvider
to separate the middleware from its side effect. The default NavigationProvider
is HistoryNavigationProvider
, but lets roll our own for testing purposes
; ; ; ; ; epicMiddleware.runmyRouteEpic; // run some testsassert.deepStrictEqualTBD
Note: we must use separate RouteAction
s and navgiationAction
s so that our RouteAction
s don't dispatch navigationAction
s. RouteActions
s are for storing the current route in global state, navigationAction
s are for modifying the brower's url. navigationAction
s should dispatch RouteAction
s, but not the other way around.
* Redux
uses function composition, while Flux
uses callback registration. Redux
state is immutable, while Mobx
state is mutable.
** redux-thunk
accepts any function, while redux-observable
enforces purity by requiring your impure asynchronous function to be demarcated as an Observer
. redux-saga
has a similar approach using generator functions, but Observable
s are monadic. redux-loop
, rather than being a middleware, allows the reducer itself to behave asynchronously, but Cmd
has no way to compose with outside event streams, which is what our router must do.