Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add guidance on how to use EUI with react-router. #810

Merged
merged 7 commits into from
May 16, 2018
Merged
Show file tree
Hide file tree
Changes from 2 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
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/
229 changes: 229 additions & 0 deletions wiki/react-router.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
# 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`, 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, render}) => {
const {href, onClick} = getRouterLinkProps(to);
return render(href, onClick);
};

<RouterLinkAdapter
to="/location"
render={(onClick, href) => <EuiLink onClick={onClick} href={href}>Link</EuiLink>}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@chandlerprall I stuck with the render prop instead of using children, because that's how they do it in the examples in the React docs, which I link to above.

/>;
```

## react-router 3.x

To enable these techniques, you'll need to make the `router` instance available outside of React's
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you try using the withRouter HOC on App? That provides router, only in v3 though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh cool, I didn't know about this HOC. Looking through the docs, it looks like the only useful object this provides you with is history, which does provide the push method but not the createHref method, which seems to be only available on router.

I haven't dug deep enough into this method to understand its role and decide if we can work around it, but I think it's better to provide reliable guidance than clever guidance so I think we should just use the current example, since it's drawn directly from the react-router source.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did specifically say v3 - in that version, withRouter() gives you a router prop, but they don't expose that in v4. AFAIR it's exactly the object you'd hope it is.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh I didn't realize I was looking at the v4 docs. Do you know where I can find the v3 docs? All I could find wrt withRouter was the source.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, added a note.

`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({
createHref: PropTypes.func.isRequired,
push: PropTypes.func.isRequired,
}).isRequired,
}

componentDidMount() {
this.registerRouter();
}

componentDidUpdate() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd move this to the bottom of the react-router 3.xsection

-  componentDidUpdate() {
-    // If using HMR, you'll need to re-register the the router after a hot reload. Note that
-    // 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();
-  }

   // …

+ #### Hot Module Reloading
+
+ When using HMR, you'll need to re-register the router after a hot reload.
+ We encourage adding conditionals here to cull this logic from a production build, like so:
+  
+  ```js
+  componentDidUpdate() {
+    if (process.env.NODE_ENV !== `production` && module.hot) {
+      this.registerRouter();
+    }
+  }
+  ```

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Probably just after this file rather than the bottom of this section)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, thanks!

// If using HMR, you'll need to re-register the router after a hot reload. Note that
// 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();
}

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

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

You can create a `routing.js` lib 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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to bother returning an unregister callback and implementing a componentDidUnmount that frees this up, right?

Copy link
Contributor Author

@cjcenizal cjcenizal May 9, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so. I think that would be an unusual situation should it arise, but I also think that if it did come up the consumer would be able to figure out that need and make this change without guidance.

};

/**
* 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 (isModifiedEvent(event) || !isLeftClickEvent(event)) {
return;
}

// Prevent regular link behavior, which causes a browser refresh.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point, react-router also executes the following, which seems sensible.

// If target prop is set (e.g. to "_blank"), let browser handle link.
if (this.props.target) return;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice spot! Though this function has no knowledge of props so we can't do the same kind of logic. If a consumer sets target="_blank" then they don't need to use this conversion function anyway since they're no longer integrating with the router (they can just use a plain old EuiLink or whatever).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function can look for a target attribute from the onClick's event.target.getAttribute('target'). Probably should add that to be safe.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea! Done.

event.preventDefault();
router.push(location);
};

return {href, onClick}
};

```

## react-router 4.x

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 = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we bother with propTypes for a simple usage example? I'd avoid doing this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about the new context API, but the original context API requires you to declare contextTypes for you to access this.context.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yuck!

router: PropTypes.shape({
history: PropTypes.shape({
push: PropTypes.func.isRequired,
createHref: PropTypes.func.isRequired
}).isRequired
}).isRequired
}

componentDidMount() {
this.registerRouter();
}

componentDidUpdate() {
// If using HMR, you'll need to re-register the router after a hot reload. Note that
// 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();
}

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

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

You can create a `routing.js` lib 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 (isModifiedEvent(event) || !isLeftClickEvent(event)) {
return;
}

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

return {href, onClick}
};

```