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

react-fp-ts-router

v0.0.32

Published

Updates global state with a routing callback, and narrows global state type further down the component tree.

Downloads

7

Readme

react-fp-ts-router

An HOC that builds a router that represents the current route in react state as an ADT (another more in depth explanation of ADTs here) and safely manages an interceptable.

Alternative to react-router-dom

Thanks to Giulio Canti for fp-ts and fp-ts-routing. Thanks React Training for history.

Installation

yarn add react-fp-ts-router

Usage

Check out the fp-ts-routing docs for more info on parsers and formatters.

These examples use unionize for their route ADTs, but you could use simple union types and type guards, or you could use the fp-ts-codegen playground to easily generate ADTs and their associated functions.

withStaticRouter

If your app has no state that depends on or affects the way the current route changes, you can use withStaticRouter. Be advised, however, that if routing anti-patterns start to creep into your app, you should use withInterceptingRouter and an interceptable instead.

This example creates a web app with the following rules:

  • At the '/' route, it renders a 'show' button that reroutes to '/show'
  • At the '/show' route, it renders a 'hide' button that reroutes to '/'.
  • At a route it doesn't recognize, it behaves as though it's at the '/' route.

Example code

Live site

withInterceptingRouter example

This example uses a simple optional string as its interceptable. This interceptable will be set differently depending on how the app's route is changed.

It creates a version of the above example, with the following additional rules:

  • it displays an interceptable at '/show'
  • the interceptable is set to 'from button click' when routed to '/show' from the 'show' button
  • the interceptable is set to 'from route' when routed to '/show' directly from the browser
  • it redirects any unrecognized route to '/'

Example code

Live site

What is an interceptable?

An interceptable models state that depends on or affects the way the current route changes.

An interceptable is used to de-couple routing logic from the component lifecycle.

When do I need an interceptable?

Here are common routing anti-patterns that appear when using withStaticRouter, and alternative solutions that use interceptable and withInterceptingRouter.

Stateful redirection

If you're using withStaticRouter and you find yourself doing a stateful redirect like this:

// Redirector.tsx
componentDidMount() {
  navigate(N.push(MyRouteADT.badRoute()));
}
render() {
  return null;
}
// in parent component
{route === MyRouteADT.goodRoute() && (
  data.type === 'bad'
    ? (
      <Redirector />
    )
    : (
      <Comp
        data={data.good}
      />
    )
)}

You might be frustrated that Redirector is forced to implement render. You might also recognize this as UNSAFE_componentWillReceiveProps in disguise.

You should use withInterceptingRouter instead, and move data into your interceptable:

const interceptRoute = (
  route: MyRouteADT,
  interceptable: MyInterceptable,
): InterceptRouteResponse<MyRouteADT, MyInterceptable> => {
  if (route === 'goodRoute' && interceptable.data.type === 'bad') {
    return {
      sync: {
        redirect: Navigate.push(RouteADT.badRoute()),
      }
    }
  } 
}
// in parent component
{route === 'goodRoute' && interceptable.data.type !== 'bad' (
  <Comp
    goodData={data.good}
  />
)}

Loading data after a reroute

If you're using withStaticRouter and you find yourself initializing data after a reroute like this:

// HandleDataInitialization.tsx
state = { data: undefined };
componentDidMount() {
  if (this.props.route === RouteADT.loadedRoute()) {
    // this.state.data can't be initialized yet,
    // because this.state.data is always
    // undefined on componentDidMount(), so
    // redirect to RouteADT.loadingRoute()
    navigate(N.push(RouteADT.loadingRoute()));
  }
  // runInitialize() will only be invoked once, even
  // after a redirect from RouteADT.loadedRoute(),
  // because this component is rendered at both
  // RouteADT.loadingRoute() and RouteADT.loadedRoute()
  // so a redirect will not trigger a new componentDidMount()
  const runInitialize = T.task.map(initializeData(), data => {
    this.setState({ data });
    navigate(N.push(RouteADT.loadedRoute()));
  });
  runInitialize();
}
render(){
  if (this.state.data === undefined) return (
    <LoadingSpinner />
  );
  return (
    <DataComp
      data={this.state.data}
    />
  );
}
// in parent component
{ (route === RouteADT.loadedRoute() || route === RouteADT.loadingRoute()) && (
  <HandleDataInitialization />
)}

You should be appalled at your impenetrable routing logic. You might also recognize this as UNSAFE_componentWillReceiveProps in an even sneakier disguise.

You should use withInterceptingRouter instead, and move data into your interceptable:

const interceptRoute = (
  route: MyRoute,
  interceptable: MyInterceptable,
): InterceptRouteResponse<MyRouteADT, MyInterceptable> => {
  if (
    route === RouteADT.loadedRoute()
    && interceptable.data === undefined
  ) {
    return {
      sync: {
        redirect: N.push(RouteADT.loadingRoute()),
      }
    };
  }
  if (route === RouteADT.loadingRoute()) {
    return {
      async: T.task.map(initializeData(), data => ({
        interceptable: {
          ...this.props.interceptable,
          data,
        }
        redirect: N.push(RouteADT.loadedRoute())
      })),
    }
  }
}
// in parent component
{(
  route === routeADT.loadedRoute() || route === RouteADT.loadingRoute()
) && (
  interceptable.data !== undefined
    ? <DataComp
      data={interceptable.data}
    />
    : <LoadingSpinner />
)}

FAQ

Why does setInterceptable return a Task<I>? It's annoying that I have to remember to invoke it every time I use it

Loading data before a reroute

If you do this:

// in a component
<button onClick={() => {
  const runReroutedData = T.task.map(
    loadReroutedData(),
    reroutedData => {
      navigate(RouteADT.loadedRoute());
      const runSetInterceptable = this.props.setInterceptable(reroutedData);
      runSetInterceptable();
    }
  );
  runReroutedData();
}}>reroute</button>
// in your interceptRoute
const interceptRoute = (
  route: MyRouteADT,
  interceptable: MyInterceptable
): InterceptRouteResponse<MyRouteADT, MyInterceptable> => {
  if (route === RouteADT.loadedRoute() && interceptable === undefined) {
    return {
      async: T.task.map(loadInitializedData, initializedData => {
        return {
          interceptable: initializedData,
        }
      }),
    }
  }
}

You may be surprised that clicking reroute causes interceptable to be set to initializedData.

This is because navigate triggers interceptRoute before setInterceptable can enqueue changes to interceptable. This causes interceptRoute to think that interceptable is uninitialized, which will trigger a Task that returns initializedData, which will clobber reroutedData.

You should do this instead:

<button onClick={() => {
  const runReroutedData = pipe(
    preLoadData(),
    T.chain(this.props.setInterceptable),
    T.map(() => navigate(N.push(RouteADT.route()))),
  );
  runReroutedData();
}}>load stuff</button>

The Task returned by setInterceptable uses a setState callback to ensure interceptable is updated before it resolves. It resolves into the latest interceptable state.

While it may be annoying to have to invoke this task every time you want to use setInterceptable, it forces you to consider its runtime asynchronicity at compile time. As we have seen, it can be dangerous to think of setInterceptable as synchronous in relation to navigation.

Can I have more than one router in my app?

You can, but you shouldn't. React offers no way to enforce this at compile time, but if react-fp-ts-router could prevent multiple router components or multiple instances of router components, it would. withInterceptingRouter is an HOC only because its parameters are constants, so passing them in through props wouldn't make sense. It's not meant to create a reusable component.

The route prop provided to the router is meant to be the single source of truth of the browser's current route.

Isn't it cumbersome to drill the current route through all of my components's props?

While you are encouraged to use react context to avoid drilling setInterceptable, drilling route is actually a feature.

A good practice with this library is to nest several routing ADTs together to mirror your app's component tree hierarchy. This enables you to ensure the correctness of your render logic at compile time. In this example, we see that the LoggedIn and LoggedOut components are relieved of the responsibility of handling irrelevant routes. This is one advantage we gain by having the current route represented globally.

type AppRoute = {
  type: 'loggedIn';
  loggedInRoute: LoggedInRoute;
} | {
  type: 'loggedOut';
  loggedOutRoute: LoggedOutRoute;
}
type LoggedInRoute = ...;
type LoggedOutRoute = ...;
const Root = ({ route }: { route: AppRoute }) => {
  if (route.type === 'loggedIn') {
    return (
      <LoggedIn
        loggedInRoute={route.loggedInRoute}
      />
    )
  }
  return {
    return (
      <LoggedOut
        loggedOutRoute={route.loggedOutRoute}
      />
    );
  }
}

Why is interceptable global?

At first, this seems unintuitive. One of the advantages of React is that it allows state to be distributed across many components. This minimizes re-renders and localizes interrelated data within the nodes of a deeply nested tree. All of these advantages seem lost when state is consolidated in your topmost component.

However, interceptable is tightly coupled to the current route because, by definition, it depends on or affects the way the current route changes. Since the current route is global, interceptable must also be global.

On closer analysis, this makes sense. interceptRoute must handle any incoming route, so it wouldn't make sense to have multiple interceptRoutes that handled different interceptables because they would have overlapping domains.

Minimizing re-renders functionally

For these reasons listed above, interceptable should be minimal. State unrelated to the current route should be handled elsewhere.

Use shouldComponentUpdate to prevent unwanted re-renders, or its function component analog, React.memo:

const Memoized = React.memo(
  MyComponent,
  // returning true prevents a re-render
  (prevProps, nextProps) => prevProps.id === nextProps.id
)

react-fp-ts-routing provides a helper function called reactMemoEq that wraps React.memo, using an Eq to compare props.

import * as E from 'fp-ts/lib/Eq';
import { reactMemoEq } from 'react-fp-ts-router';

interface InnerCompProps { text: string, num: number }
const InnerComp = ({ text, num }: InnerCompProps) => (
  <div> text: {data} num: {num} <div/>
)
const Memoized = reactMemoEq(
  Inner,
  E.getStructEq<InnerCompProps>({
    text: E.eqString,
    num: E.eqNum,
  }),
)
const Landing = ({ interceptable }) => (
  <Memoized
    text={interceptable.text}
    num={interceptable.num}
  />
);

Transforming deeply nested state functionally

Optics can help you transform deeply nested interceptable:

import * as M from 'monocle-ts';
import * as T from 'fp-ts/lib/Task';
import { pipe } from 'fp-ts/lib/pipeable';
interface MyInterceptable {
  user: {
    memories: {
      childhood: {
        favoriteColor?: string;
      }
    }
  }
}
const favoriteColorLens = M.Lens.fromPath<MyInterceptable>()([
  'user', 'memories', 'childhood', 'favoriteColor',
]);
const interceptRoute = (route: R, interceptable: MyInterceptable) => {
  if (
    route.type === 'favoriteColorRoute'
    && favoriteColorLens.get(interceptable) === undefined
  ) {
    return {
      async: {
        interceptable: pipe(
          loadFavoriteColor(),
          T.map(
            (favoriteColor) => favoriteColorLens.set(favoriteColor)(interceptable)
          )
        )
      },
    }
  }
};

Docs

createNavigator

createNavigator creates a function that you can export and use anywhere in your app to reroute using the provided routing ADT. Internally, withInterceptingRouter uses createNavigator for redirects.

createNavigator Function Type

import { Navigation } from 'react-fp-ts-routing';
export function createNavigator <R>(
  formatter: ((r: R) => string),
): (navigation: Navigation<R>) => void

| Type Variable | Description | | ------------- | ----------- | | R | Routing ADT type |

| Param | Description | | ------ | ------------ | | formatter | Converts routing ADT into a url path string |

withStaticRouter HOC

withStaticRouter Output Prop Types

The Router component that withInterceptingRouter wraps is given the props SimpleRouterProps<R>:

interface SimpleRouterProps<R> {
  route: R;
}

| Type Variable | Description | | ------------- | ----------- | | R | Routing ADT type |

| Prop | Description | | ------ | ------------ | | route | Your app's current route, represented as your routing ADT |

withStaticRouter Function Type

import { Parser } from 'fp-ts-routing'
import * as History from 'history'
function withStaticRouter<R, T extends {} = {}>(
  Router: React.ComponentType<T & SimpleRouterProps<R>>,
  parser: Parser<R>,
  formatter: ((r: R) => string),
  notFoundRoute: R,
): React.ComponentType<T>

| Type Variable | Description | | ------------- | ----------- | | R | Routing ADT type | | T | Other arbitrary props passed into Router, defaults to the empty object |

| Param | Description | | ------ | ------------ | | Router | Your app's router component | | parser | Converts url path strings into routing ADT | | notFoundRoute | ADT to use when parser can't find a route |

withInterceptingRouter HOC

withInterceptingRouter Output Prop Types

The Router component that withInterceptingRouter wraps is given the props RouterProps<S, R>:

import * as N from 'react-fp-ts-routing/lib/Navigation';
export interface InterceptingRouterProps<R, I> {
  route: R;
  interceptable: I;
  setInterceptable: SetInterceptable<I>;
}
export type SetInterceptable<I> = (newInterceptable?: I) => T.Task<void>;

| Type Variable | Description | | ------------- | ----------- | | R | Routing ADT type | | I | interceptable type |

| Prop | Description | | ------ | ------------ | | interceptable | Your router's interceptable | | route | Your app's current route, represented as your routing ADT | | updateRouter | Optionally updates interceptable and then optionally invokes a Navigation |

withInterceptingRouter Function Type

import { Parser } from 'fp-ts-routing'
import { Navigation, Action } from 'react-fp-ts-routing';
import * as History from 'history'
type InterceptRoute<R, I> = (
  newRoute: R,
  interceptable: I,
  oldRoute: R,
  Action: Action,
) => InterceptRouteResponse<R, I>;
interface InterceptRouteResponse<R, I> {
  sync?: Interception<R, I>;
  async?: T.Task<Interception<R, I>>;
}
interface Interception<R, I> {
  interceptable?: I;
  redirect?: Navigation<R>;
}
function withInterceptingRouter<S, R, T extends {} = {}>(
  Router: React.ComponentType<T & ManagedStateRouterProps<S, R>>,
  parser: Parser<R>,
  formatter: ((r: R) => string),
  notFoundRoute: R,
  defaultManagedState: S,
  interceptRoute?: interceptRoute<S, R>,
): React.ComponentType<T>

| Type Variable | Description | | ------------- | ----------- | | R | Routing ADT type | | I | interceptable type | | T | Other arbitrary props passed into Router, defaults to the empty object |

| Param | Description | | ------ | ------------ | | Router | Your app's router component | | parser | Converts url path strings into routing ADT | | formatter | Converts routing ADT into a url path string | | notFoundRoute | ADT to use when parser can't find a route | | defaultinterceptable | Populates interceptable before component is mounted | | interceptRoute | Updates the router using the new route and preexisting interceptable |

Internal ADTs

Navigation

Navigation is an ADT representing the different possible history navigations.

| Navigation<R> Type | Description | | ------------- | ----------- | | Navigation.push(route: R) | Reroutes to routing ADT R | | Navigation.replace(route: R) | Reroutes to routing ADT R without pushing new entry onto the history stack, so the browser's back button won't be able to go back to original location | | Navigation.pushExt(path: string) | N.push that can reroute to somewhere outside of your app | | Navigation.replaceExt(path: string) | N.replace that can reroute to somewhere outside of your app | | Navigation.go(delta: number) | Moves delta number of times through the session history stack. Can be positive or negative. | | Navigation.goBack | Moves back one page in the history stack | | Navigation.goForward | Moves forward one pack in the history stack |

Action

Action is an ADT representing the different possible ways the browser arrived at its current location.

| Action Type | Description | |---------------|-------------| | Action.push | The url was pushed onto the stack. (The user clicked a link, or your app used N.push) | | Action.pop | The url was popped from the stack. (The user hit the browser's back button, or your app used N.go or N.goBack) | | Action.replace | The url replaced the top entry of the stack. (Your app used N.replace) |

TODO