diff --git a/README.md b/README.md
index f53e9b7..ebc446c 100644
--- a/README.md
+++ b/README.md
@@ -76,15 +76,38 @@ Additionally components will have access to properties that has been set using `
## Client API
The custom redial router middleware `useRedial` makes it easy to add support for redial on the client side using the `render` property from `Router` in React Router. It provides the following properties as a way to configure how the data loading should behave.
```
-locals Extra locals that should be provided to the hooks other than the default ones
-beforeTransition Hooks that should be completed before a route transition is completed
-afterTransition Hooks that are not needed before making a route transition
-parallel If set to true the afterTransition hooks will run in parallel with the beforeTransition ones
-initialLoading Component should be shown on initial client load, useful if server rendering is not used
-onStarted(force) Invoked when a route transition has been detected and when redial hooks will be invoked
-onError(error, metaData) Invoked when an error happens, see below for more info
-onAborted(becauseError) Invoked if it was prematurely aborted through manual interaction or an error
-onCompleted(type) Invoked if everything was completed successfully, with type being either "beforeTransition" or "afterTransition"
+locals Extra locals that should be provided to the hooks other than the default ones
+beforeTransition Hooks that should be completed before a route transition is completed
+afterTransition Hooks that are not needed before making a route transition
+parallel If set to true the afterTransition hooks will run in parallel with the beforeTransition ones
+initialLoading Component should be shown on initial client load, useful if server rendering is not used
+syncWithHistory Default to true, and will when true make sure that the history matches successful transitions
+onStarted(force) Invoked when a route transition has been detected and when redial hooks will be invoked
+onError(error, metaData) Invoked when an error happens, see below for more info
+onAborted(becauseError, metaData) Invoked if it was prematurely aborted through manual interaction or an error
+onCompleted(type) Invoked if everything was completed successfully, with type being either "beforeTransition" or "afterTransition"
+```
+
+### `onAborted(becauseError, metaData)`
+__`metaData`__
+```
+previousLocation The previous location, before the loading started, can be used to reset the browser URL for example
+router React Router instance https://github.com/ReactTraining/react-router/blob/master/docs/API.md#contextrouter
+```
+
+#### Example
+We can use `onAborted` to add handling for when we abort loading. The example below shows how we can make the client replace the URL in the browser with what was defined before starting the loading, reseting it, when manually aborting.
+
+__NOTE: This is done automatically when `syncWithHistory` is set to true and no custom `onAborted` is defined.__
+
+```javascript
+// Function that can be used as a setting for useRedial
+function onAborted(becauseError, { previousLocation, router }) {
+ // If the loading was aborted manually we want to go back to the previous URL
+ if (!becauseError) {
+ router.replace(previousLocation);
+ }
+}
```
### `onError(error, metaData)`
@@ -94,6 +117,7 @@ abort() Function that can be used to abort current loading
beforeTransition If the error originated from a beforeTransition hook or not
reason The reason for the error, can be either a "location-changed", "aborted" or "other"
router React Router instance https://github.com/ReactTraining/react-router/blob/master/docs/API.md#contextrouter
+location The location that the error occurred for
```
#### Example
diff --git a/examples/simple/components/App.js b/examples/simple/components/App.js
index d19667a..0b62a11 100644
--- a/examples/simple/components/App.js
+++ b/examples/simple/components/App.js
@@ -21,6 +21,7 @@ export default class App extends Component {
return (
Slow, takes { this.props.route.seconds } to complete
+
+ );
+ }
+}
diff --git a/examples/simple/render/client.js b/examples/simple/render/client.js
index 2dcc88f..6c1b6a3 100644
--- a/examples/simple/render/client.js
+++ b/examples/simple/render/client.js
@@ -10,9 +10,9 @@ export default (container, routes) => {
const goBackOnError = false;
// Function that can be used as a setting for useRedial
- function onError(err, { abort, beforeTransition, reason, router }) {
+ function onError(err, { location, abort, beforeTransition, reason, router }) {
if (process.env.NODE_ENV !== 'production') {
- console.error(reason, err);
+ console.error(reason, err, location);
}
// We only what to do this if it was a blocking hook that failed
@@ -27,6 +27,21 @@ export default (container, routes) => {
}
}
+ function onAborted(becauseError, { previousLocation, router }) {
+ if (process.env.NODE_ENV !== 'production') {
+ if (becauseError) {
+ console.warn('Loading was aborted from an error');
+ } else {
+ console.warn('Loading was aborted manually');
+ }
+ }
+
+ // If the loading was aborted manually we want to go back to the previous URL
+ if (!becauseError) {
+ router.replace(previousLocation);
+ }
+ }
+
const component = (
{
parallel: true,
initialLoading: () =>
Loading…
,
onError,
+ onAborted,
}))}
/>
);
diff --git a/examples/simple/routes.js b/examples/simple/routes.js
index f62a8c3..ce5b47e 100644
--- a/examples/simple/routes.js
+++ b/examples/simple/routes.js
@@ -6,6 +6,7 @@ import Index from './components/Index';
import Github from './components/Github';
import User from './components/User';
import Fetch from './components/Fetch';
+import Slow from './components/Slow';
import Defer from './components/Defer';
export default (
@@ -15,6 +16,8 @@ export default (
+
+
)
diff --git a/src/RedialContext.js b/src/RedialContext.js
index ed08322..49b1c5a 100755
--- a/src/RedialContext.js
+++ b/src/RedialContext.js
@@ -19,6 +19,10 @@ function hydrate(renderProps) {
return createMap();
}
+function getCompleteLocation(location) {
+ return `${location.pathname}${location.search}${location.hash}`;
+}
+
export default class RedialContext extends Component {
static displayName = 'RedialContext';
@@ -34,6 +38,7 @@ export default class RedialContext extends Component {
afterTransition: PropTypes.array,
parallel: PropTypes.bool,
initialLoading: PropTypes.func,
+ syncWithHistory: PropTypes.bool,
onError: PropTypes.func,
onAborted: PropTypes.func,
onStarted: PropTypes.func,
@@ -47,11 +52,12 @@ export default class RedialContext extends Component {
beforeTransition: [],
afterTransition: [],
parallel: false,
+ syncWithHistory: true,
- onError(err, { beforeTransition }) {
+ onError(err, { beforeTransition, location }) {
if (process.env.NODE_ENV !== 'production') {
const type = beforeTransition ? 'beforeTransition' : 'afterTransition';
- console.error(type, err);
+ console.error(type, err, location);
}
},
@@ -150,14 +156,27 @@ export default class RedialContext extends Component {
afterTransitionLoading: false,
});
+ if (this.props.syncWithHistory && !becauseError) {
+ this.props.renderProps.router
+ .replace(this.state.prevRenderProps.location);
+ }
+
if (this.props.onAborted) {
- this.props.onAborted(becauseError);
+ this.props.onAborted(becauseError, {
+ router: this.props.renderProps.router,
+ previousLocation: this.state.prevRenderProps.location,
+ });
+ } else if (this.props.syncWithHistory && !becauseError) {
+ this.props.renderProps.router
+ .replace(this.state.prevRenderProps.location);
}
}
}
}
load(components, renderProps, force = false) {
+ let unregister = () => {};
+ const location = renderProps.location;
let isAborted = false;
const abort = () => {
isAborted = true;
@@ -181,6 +200,24 @@ export default class RedialContext extends Component {
this.completed.beforeTransition = false;
+ if (this.props.syncWithHistory) {
+ // Prevent adding a new entry to the history stack if a new link is pushed before
+ // we have completed the loading
+ unregister = this.props.renderProps.router.listenBefore(
+ (nextLocation) => {
+ if (
+ this.state.loading &&
+ this.state.prevRenderProps &&
+ getCompleteLocation(nextLocation) !== getCompleteLocation(renderProps.location)
+ ) {
+ unregister();
+ this.props.renderProps.router.replace(nextLocation);
+ return false;
+ }
+ return true;
+ });
+ }
+
this.setState({
aborted,
abort,
@@ -205,12 +242,14 @@ export default class RedialContext extends Component {
}
this.props.onError(error, {
+ location,
reason: bail() || 'other',
// If not defined before it's a beforeTransition error
beforeTransition: !afterTransition,
router: this.props.renderProps.router,
abort: () => this.abort(true, abort),
});
+ unregister();
});
if (this.props.parallel) {
@@ -230,12 +269,16 @@ export default class RedialContext extends Component {
.catch((err) => {
// We will only propagate this error if beforeTransition have been completed
// This because the beforeTransition error is more critical
- const error = () => this.props.onError(err, {
- reason: bail() || 'other',
- beforeTransition: false,
- router: this.props.renderProps.router,
- abort: () => this.abort(true, abort),
- });
+ const error = () => {
+ this.props.onError(err, {
+ location,
+ reason: bail() || 'other',
+ beforeTransition: false,
+ router: this.props.renderProps.router,
+ abort: () => this.abort(true, abort),
+ });
+ unregister();
+ };
if (this.completed.beforeTransition) {
error();