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

Router #244

Merged
merged 53 commits into from
Aug 1, 2017
Merged

Router #244

merged 53 commits into from
Aug 1, 2017

Conversation

fahad19
Copy link
Member

@fahad19 fahad19 commented Jul 28, 2017

NOTE: this PR should be rebase-merged to keep multiple contributors name in the history

Will be merged by @fahad19 once approved.


What's done

Introduced two new packages:

  • frint-router
  • frint-router-react

Documentation can be found in the diff.

@coveralls
Copy link

Coverage Status

Coverage increased (+0.1%) to 97.519% when pulling 395a3de on router into 927667e on master.

@coveralls
Copy link

Coverage Status

Coverage increased (+0.1%) to 97.519% when pulling d098ec7 on router into 927667e on master.

Copy link

@ghost ghost left a comment

Choose a reason for hiding this comment

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

Amazing job guys 😍

linkProps.className += ` ${activeClassName}`;
}

if (typeof type === 'undefined') {
Copy link

Choose a reason for hiding this comment

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

what happened if type is equal to null ?

Copy link
Contributor

Choose a reason for hiding this comment

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

type prop is meant to contain type property for a button element. if somebody passes a type as a prop, then a <button> gets rendered, otherwise an <a>. I'm not even sure what should happen when somebody passes null there and what the developer expects in that case.

const linkContent = <span className="decorated">About</span>;

const wrapper = shallow(
<Link className="fancy-button" to="/about" type="button">{linkContent}</Link>,
Copy link

Choose a reason for hiding this comment

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

fancy-button ❤️

path: PropTypes.string,
exact: PropTypes.bool,
computedMatch: PropTypes.object,
component: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
Copy link

Choose a reason for hiding this comment

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

Shouldn't we accept PropTypes.element as well here?

Copy link
Contributor

Choose a reason for hiding this comment

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

here we expect a definition of component, a class, not an instance (Component, not <Component />). that's why PropTypes.element doesn't fit here. so it's a function, because JavaScript :). also there can be stateless components, those are definitely functions.
actually PropTypes.node might not be needed. I am going to check.


const RouteApp = nextProps.app;

this._appInstance = new RouteApp({
Copy link

Choose a reason for hiding this comment

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

Couldn't we get the instance instead of creating a new object ?

Copy link
Member Author

Choose a reason for hiding this comment

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

we can't get because the instance does not live anywhere. these are App classes that are passed to <Route app={App} /> components.

since they are not registered to the Root app, like how we do it for Apps targeting Regions, they can't be obtained later from somewhere else. these Apps' lifecycle are contained by the <Route>.

const matched = this.props.computedMatch || this.state.matched || null;

return ComponentToRender !== null && matched !== null
? <ComponentToRender match={matched} />
Copy link

Choose a reason for hiding this comment

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

just a matter of style but maybe a if here could be more readable

Copy link
Contributor

Choose a reason for hiding this comment

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

both look fine to me. I wouldn't change it unless many people find it unreadable.

Copy link
Contributor

Choose a reason for hiding this comment

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

I also think that both provide the same level of readability, therefore would keep it the way it is.

Copy link

Choose a reason for hiding this comment

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

Yeah that's why I said a matter of style xD


React.Children.forEach(this.props.children, (element) => {
if (child !== null) {
return;
Copy link

Choose a reason for hiding this comment

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

Shouldn't we explicitly return null here?

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 so. Here it's works as a continue statement for .forEach loop.

Copy link

Choose a reason for hiding this comment

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

oh yeah I didn't see it sorry

export default function matchFromHistory(pattern, history, options = {}) {
if (!pattern) {
return {
url: history.location.pathname,
Copy link

Choose a reason for hiding this comment

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

Wouldn't it be better to check if history.location exists in the if?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think history.location will always exist. At least it was always the case in unit tests with MemoryRouterService even before pushing any URLs in it. The idea here is to return any match if there's no pattern to match. @fahad19, am I correct?

Copy link
Member Author

Choose a reason for hiding this comment

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

yep.


const matched = re.exec(history.location.pathname);

if (!matched) {
Copy link

Choose a reason for hiding this comment

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

I would check as well Array.isArray(matched)

<div>
<Route path="/" component={HomePage} exact />
<Route path="/about" component={AboutPage} />
</div>
Copy link
Contributor

Choose a reason for hiding this comment

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

Just to make sure I understand: so this piece of code won't render any actual content, it's just for registering routes to components? Or will this actually display something?

Copy link
Contributor

Choose a reason for hiding this comment

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

it won't display until window url match the path
Really simplified version of this would be a function:

function Route({path, component}){
 if (window.location.path === path) {
  return <component match={someDataAboutCurrentRouteLikeParams} />
 }
 return null;
}

Copy link
Member Author

Choose a reason for hiding this comment

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

it will render, once the current location in the browser matches to any of the paths. if there is a match, then the assigned component (HomePage or AboutPage) will render accodingly.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah ok, I think I understand, so the difference between this and using <Switch> is that this would simply render all the components which match, not just the first match, right?

Copy link
Member Author

Choose a reason for hiding this comment

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

exactly.

<Switch> would always render the first matching child, and stop there. If nothing else matches, then the last child (if it happens to have no path prop) will be rendered.

Much like switch in the language, with default as the last case.

handleClick = (e) => {
e.preventDefault();

// TODO avoid pushing route if it's current URL
Copy link
Contributor

Choose a reason for hiding this comment

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

How big is this task, can we do it now? 😺

Copy link
Member Author

Choose a reason for hiding this comment

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

@viacheslaff: could you kindly take a look at this?

Copy link
Contributor

Choose a reason for hiding this comment

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

I've added this TODO, it's the history library that issues a warning whenever the same route is pushed. there's a bit of discussion in here about whether it should warn or stay silent: remix-run/react-router#4467. but I guess <Link> shouldn't push the url if it's path exactly matches current url

linkProps.href = to;

return (
<a {...linkProps}>
Copy link
Contributor

Choose a reason for hiding this comment

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

wow, this is cool, haven't seen the spread being used like this before

1. `pattern` (`String`): Pattern to match against
1. `options` (`Object`)
* `options.exact` (`Boolean`): Matches pattern exactly, defaults to `false`.
* `options.cache` (`Boolean`): Cache matches (TODO)
Copy link
Contributor

Choose a reason for hiding this comment

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

By TODO you mean this is not implemented yet? Shouldn't we just remove this from the readme?

Copy link
Member Author

Choose a reason for hiding this comment

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

a minimal amount of caching should be supported. repeated route matching can be costly on component re-renders. I will take care of it.


- `BrowserRouterService`: uses modern HTML5 History API
- `HashRouterService`: for legacy browsers
- `MemoryRouterService`: useful for tests
Copy link
Contributor

Choose a reason for hiding this comment

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

Which one should an average app developer use?
Isn't there a way to automatically figure out which one is the best choice with feature detection or something?

Copy link
Member Author

Choose a reason for hiding this comment

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

it's a bit difficult to decide which one should be used by default by anyone. if your application is backed by a server, then BrowserRouterService makes sense.

BrowserRouterService would keep URLs in your browser like /about/page/here.

But HashRouterService can work anywhere, as long as the target page is loaded, and URLs would look like this in the browser /index.html#/about/page/here.

And MemoryRouterService has nothing to do with the browser's API. it just imitates the behaviour of the other services and very useful in unit testing your app.

We want the developers to pick a service of their own needs when constructing the App, otherwise we end up having too much code in our bundles, that's never executed.

The developer needs to make a conscious decision about it. If you want feature detection, you have to do it yourself when returning the instance of the right service when your router provider is computed.

Choose a reason for hiding this comment

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

Might make sense to copy this over into the docs ;)

Copy link
Member Author

Choose a reason for hiding this comment

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

@jvantillo: good idea. I have copied the text, and will send a PR about how to choose the right router service in your app soon. but really want to release it first :D

also plan to revamp the homepage with more content to deliver a better message to developers. a brainstorm meeting invitation is coming soon.

Copy link
Contributor

@markvincze markvincze left a comment

Choose a reason for hiding this comment

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

Looks awesome, a huge amount of work, and really nice, polished docs and sample! 👍

"react-test-renderer": "^15.5.4"
"react-test-renderer": "^15.5.4",
"frint": "^2.0.1",
"frint-test-utils": "^2.0.1"
Copy link
Contributor

Choose a reason for hiding this comment

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

frint and frint-test-utils are duplicates. one pair should be removed

@@ -38,7 +38,9 @@
"prop-types": "^15.5.10",
"react": "^15.5.4",
"react-dom": "^15.5.4",
"react-test-renderer": "^15.5.4"
"react-test-renderer": "^15.5.4",
Copy link
Contributor

Choose a reason for hiding this comment

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

because enzyme, prop-types, react, react-dom , react-test-renderer are now in root package.json, they can be removed this package.json

linkProps.className += ` ${activeClassName}`;
}

if (typeof type === 'undefined') {
Copy link
Contributor

Choose a reason for hiding this comment

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

type prop is meant to contain type property for a button element. if somebody passes a type as a prop, then a <button> gets rendered, otherwise an <a>. I'm not even sure what should happen when somebody passes null there and what the developer expects in that case.

path: PropTypes.string,
exact: PropTypes.bool,
computedMatch: PropTypes.object,
component: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
Copy link
Contributor

Choose a reason for hiding this comment

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

here we expect a definition of component, a class, not an instance (Component, not <Component />). that's why PropTypes.element doesn't fit here. so it's a function, because JavaScript :). also there can be stateless components, those are definitely functions.
actually PropTypes.node might not be needed. I am going to check.

path: PropTypes.string,
exact: PropTypes.bool,
computedMatch: PropTypes.object,
component: PropTypes.func,
Copy link
Contributor

Choose a reason for hiding this comment

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

@JackTheRipper removed the PropTypes.node, because PropTypes.func is sufficient

@coveralls
Copy link

Coverage Status

Coverage increased (+0.1%) to 97.519% when pulling e63722b on router into 927667e on master.

@coveralls
Copy link

Coverage Status

Coverage increased (+0.1%) to 97.498% when pulling ffd0305 on router into 927667e on master.

* frint-router: caching.

* frint-router: coverage.

* frint-router: caching docs.

* examples: require packages from local node_modules.
@coveralls
Copy link

Coverage Status

Coverage increased (+0.1%) to 97.507% when pulling c76bbe1 on router into 927667e on master.

@coveralls
Copy link

Coverage Status

Coverage increased (+0.1%) to 97.507% when pulling 9dab59c on router into 927667e on master.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants