-
Notifications
You must be signed in to change notification settings - Fork 842
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
Changes from 6 commits
56f2c8c
c46ed1c
2a1bf27
fee1dde
1d01753
d0dfcb4
21c4a17
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,259 @@ | ||
# 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Be aware that v3 won't hot-reload changes to the routes (this is deliberate). A hard refresh is needed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ++ Good point, but I don't think we need to mention this here since that won't have an effect on how EUI integrates with the router. |
||
|
||
```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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we need to bother returning an There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At this point, // If target prop is set (e.g. to "_blank"), let browser handle link.
if (this.props.target) return; There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The function can look for a There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
||
### 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 = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); | ||
} | ||
|
||
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 (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. |
There was a problem hiding this comment.
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 onApp
? That providesrouter
, only in v3 though.There was a problem hiding this comment.
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 thecreateHref
method, which seems to be only available onrouter
.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.
There was a problem hiding this comment.
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 arouter
prop, but they don't expose that inv4
. AFAIR it's exactly the object you'd hope it is.There was a problem hiding this comment.
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.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://github.com/ReactTraining/react-router/blob/v3/docs/API.md#withroutercomponent-options
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, added a note.