Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 33 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)`
Expand All @@ -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
Expand Down
7 changes: 7 additions & 0 deletions examples/simple/components/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export default class App extends Component {
return (
<div style={style}>
<h1>React Router Redial Example</h1>
<button onClick={this.props.abort}>Abort</button>
<ul>
<li>
<IndexLink to="/">Start</IndexLink>
Expand All @@ -31,6 +32,12 @@ export default class App extends Component {
<li>
<Link to="/fetch">Fetch, with client error</Link>
</li>
<li>
<Link to="/slow_5">Slow, takes 5 seconds to complete</Link>
</li>
<li>
<Link to="/slow_3">Slow, takes 3 seconds to complete</Link>
</li>
<li>
<Link to="/defer">Defer, with client error</Link>
</li>
Expand Down
21 changes: 21 additions & 0 deletions examples/simple/components/Slow.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React, { Component } from 'react';
import { provideHooks } from 'redial';

@provideHooks({
fetch: ({ routeProps, setProps }) => new Promise((resolve) => {
setTimeout(() => {
const getValue = () => Math.round(Math.random() * 255);
setProps({color: `rgb(${getValue()}, ${getValue()}, ${getValue()})`});
resolve();
}, routeProps.seconds * 1000);
})
})
export default class Slow extends Component {
render() {
return (
<div>
<h1 style={{ color: this.props.color }}>Slow, takes { this.props.route.seconds } to complete</h1>
</div>
);
}
}
20 changes: 18 additions & 2 deletions examples/simple/render/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 = (
<Router
history={browserHistory}
Expand All @@ -37,6 +52,7 @@ export default (container, routes) => {
parallel: true,
initialLoading: () => <div>Loading…</div>,
onError,
onAborted,
}))}
/>
);
Expand Down
3 changes: 3 additions & 0 deletions examples/simple/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -15,6 +16,8 @@ export default (
<Route path="user/:id" component={User} />
</Route>
<Route path="fetch" component={Fetch} />
<Route path="slow_5" component={Slow} seconds="5" />
<Route path="slow_3" component={Slow} seconds="3" />
<Route path="defer" component={Defer} />
</Route>
)
61 changes: 52 additions & 9 deletions src/RedialContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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,
Expand All @@ -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);
}
},

Expand Down Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -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) {
Expand All @@ -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();
Expand Down