Skip to content

Commit

Permalink
Add guidance on how to use EUI with react-router. (#810)
Browse files Browse the repository at this point in the history
  • Loading branch information
cjcenizal authored May 16, 2018
1 parent 5e06ca1 commit 1b17e33
Show file tree
Hide file tree
Showing 2 changed files with 271 additions and 0 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ You can find documentation around creating and submitting new components in [CON
### Consumption

* [Consuming EUI][consuming]
* [Using EUI with react-router][react-router]

### Maintenance

Expand All @@ -80,4 +81,5 @@ You can find documentation around creating and submitting new components in [CON
[releasing-versions]: wiki/releasing-versions.md
[testing]: wiki/testing.md
[theming]: wiki/theming.md
[react-router]: wiki/react-router.md
[docs]: https://elastic.github.io/eui/
269 changes: 269 additions & 0 deletions wiki/react-router.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
# Using react-router with EUI

EUI doesn't prescribe the use of any particular routing library, and we also don't want to incur
the maintenance burden of supporting router-specific components. For these reasons, EUI doesn't
publish any tools for working with `react-router` (or any other routing lib). However,
integrating EUI with `react-router` on the consumer's side is fairly straightforward.

## How react-router works

Links in `react-router` accept a `to` prop and convert this to both `href` and `onClick` props
under the hood. The `onClick` is used to push a new `history` location, and the `href` allows you to
open the link in a new tab. Any mechanism for integrating EUI with `react-router` needs to bridge
this `to` prop with EUI components' `href` and `onClick` props.

## Techniques

There are many techniques for integrating EUI with `react-router` ([see below](#techniques-we-dont-recommend) for some techniques we don't recommend), but we think these two are the strongest:

### 1) Conversion function (recommended)

You can use a conversion function to convert a `to` value
to `href` and `onClick` values, which you can then pass to any EUI button or link component.
Many EUI components are designed to accept both props if they accept one.

This technique is recommended because of its flexibility. As a consumer, you have the option to
use either the `href` or `onClick` values, or both. It's also terser than the second option.

```jsx
<EuiLink {...getRouterLinkProps('/location')}>Link</EuiLink>
```

### 2) Adapter component

Alternatively, you can create a component which will consume or encapsulate the
`getRouterLinkProps` logic, and use that in conjunction with a
[`render` prop](https://reactjs.org/docs/render-props.html).

```jsx
const RouterLinkAdapter = ({to, children}) => {
const {href, onClick} = getRouterLinkProps(to);
return children(href, onClick);
};

<RouterLinkAdapter to="/location">
{(onClick, href) => <EuiLink onClick={onClick} href={href}>Link</EuiLink>}
<RouterLinkAdapter/>
```

## react-router 3.x

### Share `router` globally

To enable these techniques, you'll need to make the `router` instance available outside of React's
`context`. One method for doing this is to assign it to a globally-available singleton within your
app's root component.

```jsx
import { registerRouter } from './routing';

// App is your app's root component.
class App extends Component {
// NOTE: As an alternative to consuming context directly, you could use the `withRouter` HOC
// (https://github.com/ReactTraining/react-router/blob/v3/docs/API.md#withroutercomponent-options)
static contextTypes = {
router: PropTypes.shape({
createHref: PropTypes.func.isRequired,
push: PropTypes.func.isRequired,
}).isRequired,
}

componentDidMount() {
this.registerRouter();
}

registerRouter() {
// Share the router with the app without requiring React or context.
const { router } = this.context;
registerRouter(router);
}
}

ReactDOM.render(
<Router history={history}>
<Route path="/" component={App} />,
</Router>,
appRoot
)
```

### Hot module reloading

Note that if using HMR, you'll need to re-register the router after a hot reload.

```js
componentDidUpdate() {
// You may want to add some conditions here to cull this logic from a production build,
// e.g. `if (process.env.NODE_ENV !== 'production' && module.hot)`
this.registerRouter();
}
```

### `routing.js` service

You can create a `routing.js` service to surface the `registerRouter` method as well as your
conversion function (called `getRouterLinkProps` here).

```js
// routing.js

const isModifiedEvent = event => !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);

const isLeftClickEvent = event => event.button === 0;

const resolveToLocation = (to, router) => typeof to === 'function' ? to(router.location) : to;

let router;
export const registerRouter = reactRouter => {
router = reactRouter;
};

/**
* The logic for generating hrefs and onClick handlers from the `to` prop is largely borrowed from
* https://github.com/ReactTraining/react-router/blob/v3/modules/Link.js.
*/
export const getRouterLinkProps = to => {
const location = resolveToLocation(to, router);
const href = router.createHref(location);
const onClick = event => {
if (event.defaultPrevented) {
return;
}

// If target prop is set (e.g. to "_blank"), let browser handle link.
if (event.target.getAttribute('target')) {
return;
}

if (isModifiedEvent(event) || !isLeftClickEvent(event)) {
return;
}

// Prevent regular link behavior, which causes a browser refresh.
event.preventDefault();
router.push(location);
};

return {href, onClick}
};
```

## react-router 4.x

### Share `router` globally

Setup is slightly different with `react-router` 4.x. To enable these techniques, you'll need to make
the `router` instance available outside of React's `context`. One method for doing this is to assign
it to a globally-available singleton within your app's root component.

```jsx
import { registerRouter } from './routing';

// App is your app's root component.
class App extends Component {
static contextTypes = {
router: PropTypes.shape({
history: PropTypes.shape({
push: PropTypes.func.isRequired,
createHref: PropTypes.func.isRequired
}).isRequired
}).isRequired
}

componentDidMount() {
this.registerRouter();
}

registerRouter() {
// Share the router with the app without requiring React or context.
const { router } = this.context;
registerRouter(router);
}
}

ReactDOM.render(
<Router}>
<App />,
</Router>,
appRoot
)
```

### Hot module reloading

[See above](#hot-module-reloading).

### `routing.js` service

You can create a `routing.js` service to surface the `registerRouter` method as well as your
conversion function (called `getRouterLinkProps` here).

```js
// routing.js

import { createLocation } from 'history';

const isModifiedEvent = event => !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);

const isLeftClickEvent = event => event.button === 0;

let router;
export const registerRouter = reactRouter => {
router = reactRouter;
};

/**
* The logic for generating hrefs and onClick handlers from the `to` prop is largely borrowed from
* https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/modules/Link.js.
*/
export const getRouterLinkProps = to => {
const location = typeof to === "string"
? createLocation(to, null, null, router.history.location)
: to;

const href = router.history.createHref(location);

const onClick = event => {
if (event.defaultPrevented) {
return;
}

// If target prop is set (e.g. to "_blank"), let browser handle link.
if (event.target.getAttribute('target')) {
return;
}

if (isModifiedEvent(event) || !isLeftClickEvent(event)) {
return;
}

// Prevent regular link behavior, which causes a browser refresh.
event.preventDefault();
router.history.push(location);
};

return {href, onClick}
};

```

## Techniques we don't recommend

### Using EUI classes with the react-router `<Link>` component

It's possible to integrate EUI with `react-router` by using its CSS classes only:

```jsx
<Link className="euiLink" to="/location">Link</Link>
```

But it's important to be aware of two caveats to this approach:

* EUI's components contain a lot of useful behavior. For example, `EuiLink` will render either
a button or an anchor tag depending on the presence of `onClick` and `href` props. It will also
create a secure `rel` attribute if you add `target="_blank"`. Consumers lose out on these
features if they use EUI's CSS instead of its React components.
* This creates a brittle dependency upon the `euiLink` CSS class. If we were to rename this
class in EUI, this would constitute a breaking change and we'd make a note of it in the change
log. But if a consumer doesn't notice this note then the only way they could detect that something
in their UI has changed (and possibly broken) would be through manual testing.

0 comments on commit 1b17e33

Please sign in to comment.