Skip to main content

Whereabouts

Whereabouts - a (fairly) lightweight Redux location manager

Redux first abstraction of browser history management. Stores normalised current location and location history in Redux to be consumed by React components to render routes accordingly.

See concept for more details.

Usage

Installation

$ yarn add @rexlabs/whereabouts

Examples

Setting up the Redux store

import { createStore, applyMiddleware, combineReducers } from 'redux';
import {
whereaboutsReducer as whereabouts,
whereaboutsMiddleware
} from '@rexlabs/whereabouts';

const store = createStore(
combineReducers({ whereabouts: whereabouts }), // here we choose "whereabouts" as the state key
{},
applyMiddleware([whereaboutsMiddleware]) // needed to initialize history listener
);

export default store;

Connecting a component

To get the current location data from Redux, you can either @connect manually, or use the HoC react-whereabouts provides in order to de-normalize the state. It uses memoization under the hood, so there won't be unnecessary rerenders due to the state mapping.

import { withWhereabouts } from '@rexlabs/whereabouts';

@withWhereabouts
class Example extends Component {
render() {
const { whereabouts } = this.props;
return (
<div>
<h1>Current Location:</h1>
<ul>
<li>Path: {whereabouts.path}</li>
<li>Hash: {whereabouts.hash}</li>
<li>Query: {whereabouts.query}</li>
</ul>
<h1>History:</h1>
<pre>{JSON.stringify(whereabouts.history, null, 2)}</pre>
</div>
);
}
}
export default Example;

Using a different Redux store key to "whereabouts"

If you've setup your Redux store with whereabout's reducer under a key other than "whereabouts", then you will need to specify that key when using the withWhereabouts HOC.

A suggestion would be to do this once in your App, and use that "namespaced" version of our withWhereabouts everywhere else in your App.

// ./src/utils/with-router.js
import { withWhereabouts } from '@rexlabs/whereabouts';

const withRouter = withWhereabouts('router');
export default withRouter;

Dispatching actions

Whereabouts splits the concerns of source of truth for location data (Redux) and actions (History). Since the location manager should be able to receive actions from anywhere (you, the browser, 3rd party libs, etc), it needs to rely on history actions rather than Redux actions. The history actions then trigger Redux actions in the core.

import {
push,
replace,
pop,
go,
goForwards,
goBackwards
} from '@rexlabs/whereabouts';

push({
path: '/example',
query: { foo: 'bar' }
});

// => /example?foo=bar

URL placeholders

You can define placeholders in your URL configs that will be passed down as param variables, e.g. entity ids, etc. Whereabouts uses path-to-regex in the core to parse the URLs, so all its rules apply here as well, e.g.:

Normal placeholder

/path/:var

Regex

/path/:var([0-9]+)

Optional

/path/:var?

Wildcard

Wildcards are basically just unnamed regex placeholders.

/path/(.*)/something

Unnamed placeholders will not be passed down as params! Besides the path whereabouts also parses hash strings for possible url placeholders, so you can use the same syntax and patterns there as well.

As part of the location management piece, whereabouts also keeps track of breadcrumbs. As a concept, breadcrumbs are aiming at helping the user navigate through the app, especially around seing record relations the user has clicked through.

We essentially support 4 behaviours when navigating (e.g. via push or the Link component):

  1. clear breadcrumbs (default) - when going to a new route, we want to clear the current breadcrumb stack
  2. add breadcrumb - this keeps the current breadcrumb stack and adds another one on top
  3. keep breadcrumbs - e.g. when opening dialogs, we usually want to just maintain the current stack without any changes to it
  4. reset breadcrumbs to a specific index - this should really just happen when clicking on a breadcrumb itself, where we want to reset the current stack to the clicked index

In practise, these behaviours can be controlled through the following options e.g. in push:

push(route, {
addBreadcrumb: {...},
keepBreadcrumbs: true,
resetBreadcrumbsToIndex: 1
})

The Link components supports equivalent props with the same names.

If there are no breadcrumbs for the current location, screens in the app can define fallbacks and use updateBreadcrumbs to push them into whereabouts. The logic currently needs to be implemented on app level, probably worth abstracting out into a Screen component in vivid at some point. E.g.

import { updateBreadcrumbs } from '@rexlabs/whereabouts';

function Screen({ breadcrumbs = [], whereabouts, children }) {
useEffect(() => {
if (breadcrumbs?.length && !whereabouts?.state?.breadcrumbs?.length) {
updateBreadcrumbs(breadcrumbs);
}
});

return children;
}

Breadcrumb shape

Breadcrumbs passed into addBreadcrumb need to be serialisable, so we usally want to just pass in a key, and then in the app map the key to the desired behaviour (e.g. the data we want to load, the label we want to show, the route the breadcrumb should link to, etc).

push(route, {
addBreadcrumb: {
type: 'contact',
id: contactId,
foo: 'whatever other meta information'
}
});

Persisting breadcrumbs

To properly deal with breadcrumbs, we provide a persistor for the whereabouts state which persists the state in sessionStorage (= bound to the current tab). When rehydrating the state, we check the current location and only rehydrate if it matches the last location in the persisted state.

More Reading

This is by no means a new concept. Other libraries already follow the same concept, so if you think whereabouts is not the perfect match for you, definitely have a look at other implementations:

Libraries used

Development

Install dependencies

$ yarn

Available Commands

$ yarn test               # runs all units tests
$ yarn test:watch # runs unit tests when files change
$ yarn build # bundles the package for production
$ yarn start # runs storybook

See Routing Examples

$ yarn
$ yarn start

This will open up storybook with several example stories, that represent the routing requirements we had in past projects (Satchel, Flow, Rex) as well as an example story that just visualised the data flows.

Copyright © 2019 Rex Software All Rights Reserved.