npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

fp-ts-routing-redux

v0.0.4

Published

Increasingly pure integrations of fp-ts-routing into redux

Downloads

7

Readme

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 (RouteActions and NavigationActions can compose Routes), 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

const myReducer: (s: State, a: ADT) => State = ...

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

const store = createStore(myReducer, initialState);

Doing this:

store.dispatch(someActionADT)

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

import { makeADT, ofType } from '@morphic-ts/batteries/lib/summoner-BASTJ'
import { Navigation } from 'fp-ts-routing-redux'

// define our app's ADTs

const _MyRoute = makeADT('type')({
  ...
  notFound: ofType<{}>(),
});
type MyRoute = typeof _MyRoute

const _RouteAction = makeADT('type')({
  ...
  route: ofType<MyRoute>(),
})
type RouteAction = typeof _RouteAction
const _ResponseAction = makeADT('type')({
  ...
  data: ofType<...>(),
})
type ResponseAction = typeof _ResponseAction
const _MyAction = makeADT('type')({
  ...
  routeAction: ofType<RouteAction>(),
  responseAction: ofType<ResponseAction>(),
  navigationAction: ofType<Navigation<MyRoute>>(),
});
type MyAction = typeof _MyAction

interface MyState {
  ...
  currentRoute: MyRoute;
}

Handling route events with Redux middleware

Redux can accept middlewares. This is a good fit for our router

import { createStore, applyMiddleware } from 'redux'
import * as R from 'fp-ts-routing';
import { routeMiddleware } from 'fp-ts-routing-redux'

// handle our app's routing

const myParser = R.zero<MyRoute>().map(...);
const myDefaultRoute = _MyRoute.of.notFound({});
const initialState = {
  ...
  currentRoute: MyRoute.notFound({}),
}

const myReducer = (state: MyState, action: MyAction) => MyAction.match({
  ...,
  routeAction: (route: MyRoute) => {...state, currentRoute: route},
  responseAction: (newData) => {...state, data: newData},
})(action);

// will invoke the store's `dispatch` on each new route event
const myRouteMidleware = routeMiddleware<MyRoute, MyAction>(
  myParser,
  myDefaultRoute,
  (r: MyRoute): MyAction => _MyAction.of.responseAction(...),
);

const store = createStore(
  myReducer,
  initialState,
  applyMiddleware(myRouteMidleware),
);

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

Triggering asynchronous side-effects from route events with redux-observable

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 Epics that return Observables 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 RouteActions to make asynchronous calls to fetch that push ResponseActions

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 RouteActions and ResponseActions to our dispatch

(RouteType is just a wrapper for a history action with a less confusing name in this context)

import * as Rx from 'rxjs'
import {
  Epic,
  createEpicMiddleware,
} from 'redux-observable'
import { routeObservable, RouteType } from 'fp-ts-routing-redux';

const myRouteObservable: Rx.Observable<ResponseAction> = routeObservable<MyRoute>(
  parser,
  notFoundRoute
).pipe(
  Rx.map(([route]: [MyRoute, RouteType]): MyAction => _RouteAction.of.route(route)),
  Rx.map(
    (action: RouteAction) => Rx.merge(
      routeAction,
      routeAction.pipe(
        Rx.map((action: RouteAction): Observable<Response> => fromFetch(
            'https://jsonplaceholder.typicode.com/posts/1',
        )),
        Rx.mergeAll(),
        Rx.map((resp: Response): ResponseAction => _ResponseAction.of....)
      ),
    ),
  ),
  Rx.mergeAll(),
);

const myRouteEpic: Epic<
  MyAction, ResponseAction, MyState
> = (): Rx.Observable<ResponseAction> => myRouteObservable;

const myEpicMiddleware = createEpicMiddleware();

const store = createStore(
  myReducer,
  applyMiddleware(myEpicMiddleware)
);

epicMiddleware.run(myRouteEpic);

If we want to have other asynchonous side effects, Epics represent your redux state and action as Observables called $state and $action. We can merge routeObservable with whatever Observables you need

const myRouteEpic: Epic<MyAction, MyAction, MyState> = (
  action$: Rx.Observable<MyAction>,
): Rx.Observable<MyAction> => Rx.merge(
  myRouteObservable,
  action$.pipe(
    // your other asynchronous code
    Rx.filter(...),
    ...
  ),
);

This is still impure. We are using side effects without safely demarcating them as IO. How would we mock this for testing?

Triggering asynchronous side-effects from route events with @matechs/epics

@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 Effects purely by separating them from their environments using Providers

A Stream is an Effectful 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

import { stream as S, effect as T } from '@matechs/effect';
import * as Ep from '@matechs/epics';

const fetchUser = Ep.epic<MyState, MyAction>()((_, action$) =>
// TODO - implement this
// https://arnaldimichael.gitbook.io/matechs-effect/core/the-effect-system
// https://arnaldimichael.gitbook.io/matechs-effect/core/play-with-streams
// https://github.com/Matechs-Garage/matechs-effect/blob/master/packages/epics/tests/Epics.test.ts

We are able to easily mock our NavigationProvider for testing

import * as assert from "assert";
// TODO - implement this
assert.deepStrictEqual(TBD)

Rerouting with redux

Since rerouting is simply a dispatched 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

import * as O from 'fp-ts/lib/Option'
import * as assert from "assert";
import { compose } from 'redux'
import { createStore, applyMiddleware, compose } from 'redux'

const formatter: (r: MyRoute) => string = MyRoute.match({ ... });

const testNavigationProvider = TBD;

const myNavigationMiddlware = navigationMiddleware<MyRoute, MyAction>(
  formatter,
  (routeAction: MyAction): O.Option<Navigation<MyRoute>> => MyAction.is.RouteAction(routeAction)
    ? O.some(Navigation.push(routeAction.route))
    : O.none,
  testNavigationProvider,
);

const store = createStore(
  myReducer,
  compose(
    applyMiddleware(myEpicMiddleware),
    applyMiddleware(myNavigationMiddleware),
  ),
);

epicMiddleware.run(myRouteEpic);

// run some tests
assert.deepStrictEqual(TBD)

Note: we must use separate RouteActions and navgiationActions so that our RouteActions don't dispatch navigationActions. RouteActionss are for storing the current route in global state, navigationActions are for modifying the brower's url. navigationActions should dispatch RouteActions, 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 Observables 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.