-
-
Notifications
You must be signed in to change notification settings - Fork 10.6k
New top level API #1012
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
Comments
Something to consider:
|
That's the direction I've been headed too. Almost identical ... pretty much exactly identical. I'll post my code after I think through a few more bits, but regarding transitions, this is something I've been thinking about. Instead of tracking active components and passing them around to transition hooks and people having to figure out how to get their flux/flummox/fluxxor/freekz into transition hooks, we could maybe have users "block" transitions before they even happen, and then unblock them later. For example, instead of waiting for a transition from a half-filled out form (literally the only use case for var Signup = Router.createHandler(class extends React.Component {
handleInputChange (event) {
if (formIsNotEmpty(this.getDOMNode()))
this.blockTransitions((allowTransition) => {
if (prompt('Unsaved changes, click okay to leave page'))
allowTransition();
});
else
this.unblockTransitions();
},
render () {
return (
<form>
<input name="address" onChange={this.handleInputChange.bind(this)}/>
</form>
);
}
}); |
We don't really need |
I hope you're ready for this: // can still do this for most apps and people who like it
/*
var routes = (
<Route handler={App}>
<Route path="/signup" handler={Signup}>
<Route path="/courses" handler={Courses}/>
<Route path="/course/:course_id" handler={Course}>
<Route path="assignments" handler={Assignments}/>
<Route path="assignments/:assignment" handler={Assignment}/>
<Route path="/grades/:course_id" handler={Grades}/>
</Route>
</Route>
);
*/
// but it all becomes this somewhere
////////////////////////////////////////////////////////////////////////////////
// Routes
var SignupRoute = {
path: '/signup',
handlers: {
main: require('./Signup'),
sidebar: require('./Signup/Sidebar')
}
};
var CoursesRoute = {
path: '/courses',
handlers: {
main: require('./Courses'),
sidebar: require('./Courses/Sidebar')
},
// because paths will be gradually matched, need to provide hints because
// the GradesRoute won't be loaded yet so the matcher won't know that
// `/grades/econ-101` is a descendant route of `/courses`. Could preprocess
// this in apps that can't have the whole route config at boot)
descendentPathMatches: ['/grades/:course_id']
};
var CourseRoute = {
path: '/course/:course_id',
handlers: {
// handlers can be functions that receive a callback, working seemlessly
// with webpack lazy bundles
main: require('bundle!lazy?./Course'),
sidebar: require('bundle!lazy?./Course/Sidebar')
},
// bummer it has to go all the way up the tree
descendentPathMatches: ['/grades/:course_id']
}
var AssignmentsRoute = {
path: '/course/:course_id/assignments',
handlers: {
main: require('bundle!lazy?./Course/Assignments'),
toolbar: require('bundle!lazy?./Course/Assignments/Toolbar')
}
};
var AssignmentRoute = {
path: '/course/:course_id/assignments/:assignment_id',
handlers: {
main: require('bundle!lazy?./Course/Assignments/Assignment'),
toolbar: require('bundle!lazy?./Course/Assignments/Assignment/Toolbar')
}
};
var Grades = {
path: '/grades/:course_id',
handlers: {
main: require('bundle!lazy?./Course/Grades'),
toolbar: require('bundle!lazy?./Course/Grades/Toolbar')
}
};
////////////////////////////////////////////////////////////////////////////////
// Components
var App = Router.createRootHandler(class extends React.Component {
static routes = [
SignupRoute,
CoursesRoute,
CourseRoute
];
render () {
return (
<div>
<h1>App</h1>
<Outlet for="sidebar"/>
<Outlet/>
</div>
);
}
});
class SignupSidebar extends React.Component {
render () {
return (
<div _dangerouslySetInnerHTML={{html: STATIC_CONTENT.SIGNUP_STUFF}}/>;
);
}
}
var Signup = Router.createHandler(class extends React.Component {
handleInputChange (event) {
if (formIsNotEmpty(this.getDOMNode()))
this.blockTransitions((allowTransition) => {
if (prompt('Unsaved changes, click okay to leave page'))
allowTransition();
});
else
this.unblockTransitions();
}
handleSubmit (event) {
createUser(getFormValues(event.target)).then(() => {
// transition to route definitions
this.transitionTo(CoursesRoute);
});
}
render () {
return (
<form>
<input name="name" onChange={this.handleInputChange.bind(this)}/>
<input name="email" onChange={this.handleInputChange.bind(this)}/>
<button type="submit" onSubmit={this.handleSubmit.bind(this)}>sign up</button>
</form>
);
}
});
class Courses extends React.Component {}
class CourseSidebar extends React.Component {
render () {
return (
<nav>
<ul>
<li><Link to={AssignmentsRoute}>Assignments</Link></li>
<li><Link to={GradesRoute}>Grades</Link></li>
</ul>
</nav>
);
}
}
var Course = Router.createHandler(class extends React.Component {
// the "main" handler defines the nested routes it responds
// to. We would gradually match paths so that the route config
// can be loaded lazily along with the components
static routes = [
AssignmentsRoute,
AssignmentRoute,
GradesRoute,
];
render () {
return (
<div>
{/*
we inject params/query/routeBranch/router
assists with getting started quickly (not having to learn about
context or passing the router state down the hierarchy) and also
assists with docs/commenting on issues because we have a base
assumption about how people can access this stuff.
*/}
<h2>{this.props.params.course_id}</h2>
<Outlet for="toolbar"/>
<Outlet/>
</div>
);
}
});
class Assignments extends React.Component {}
class AssignmentsToolbar extends React.Component {}
class Assignment extends React.Component {}
class AssignmentToolbar extends React.Component {}
class Grades extends React.Component {}
class GradesToolbar extends React.Component {}
////////////////////////////////////////////////////////////////////////////////
// running the app
// lets say we land at /course/econ-101/assignment/234
App.run(HistoryLocation, (state) => {
// still use App.run and not just the location so that we can provide the
// matched branch of routes in here for data fetching and also so that
// the router can wait for any async component loading to happen before calling
// back in here
state.branch; // [{ route: CourseRoute, handlers: { main: ..., sidebar ...}, ...];
React.render(<App/>, document.body);
}); |
We don't have the route branch in the location subscription in the example that I gave, only the new location. Of course, routers will still need a HashLocation.addChangeListener(function (location) {
var match = MyRouter.match(location);
// Here you could introspect the routes, but you'll end up doing the same
// matching work in the call to render unless we possibly have another
// prop we can use for the pre-computed match object.
React.render(<MyRouter match={match}/>, document.body);
}); Ultimately it seems this thread is about doing a lot less stuff than we were trying to do previously, which I think is a positive direction to take. We can delegate all async behavior/prop loading to something else. |
@mjackson I don't want users to have to match the location and then pass it in, I'd rather handle that for them. |
@ryanflorence The |
In other words, I don't like magically putting state onto the |
I'm after:
|
That's exactly what I tried except I was missing I like @mjackson's explicit approach with change handler on location over class RouterEntryPoint extends Component {
componentDidMount() {
HashLocation.addChangeListener(this.handleLocationChange);
}
componentWillUnmount() {
HashLocation.removeChangeListener(this.handleLocationChange);
}
handleLocationChange(location) {
this.setState({ match: MyRouter.match(location) });
}
render() {
const { match } = this.state;
return <MyRouter match={match} />
}
} |
How do you feel about passing var Course = Router.createHandler(class extends React.Component {
static routes = [
AssignmentsRoute,
AssignmentRoute,
GradesRoute,
];
render () {
// Grab outlets from a prop
let { Toolbar, Main } = this.props.outlets;
// Parent can show a spinner while async outlet is loading!
if (!Main) {
Main = Spinner;
}
return (
<div>
<h2>{this.props.params.course_id}</h2>
<Toolbar />
<Main />
</div>
);
}
}); I see several benefits:
|
I like where this is going, especially
I'm not sure yet about
As for using I've also recently been thinking about ways to "relax" react-router to enable more use cases and better library integration. Here's what I had in mind:
|
Also, there are some use-cases we've all talked about before that we should make sure we can handle:
|
👍 I had a version like that too, but didn't consider the value the introspection. |
I like the idea of not doing HashLocation.addChangeListener(function (location) {
var branch = MyRouter.match(location); // <-- perfect, now people can do their own data loading
// but how do we, the Router, gather up the async components before rendering here?
React.render(<MyRouter match={match}/>, document.body);
// I don't want users to have to do boilerplate like this:
loadHandlers(MyComponent, location).then(() => {
React.render(<MyRouter match={match}/>, document.body);
});
}); That's why I think we still need |
// we can easily provide something like this
Router.run = (Root, Location, cb) => {
Location.listen((location) => {
var branch = Root.match(location);
resolveComponents(branch).then(() => {
cb(location, branch);
});
});
};
// to allow for this as the minimal boilerplate and what we do for examples/guides/etc.
Router.run(App, HistoryLocation, (location) => {
React.render(<App location={location} />, document.body);
});
// but this is still public API for other use cases
HistoryLocation.listen((location) => {
React.render(<App location={location} />, document.body);
}); |
Sorry, one more thing, ANIMATIONS.
|
Just a heads up, I added a |
The original transition from hook was an instance method, I always liked it better. I think we went with a static for parity with HistoryLocation.listen((location) => {
var branch = App.match(location);
runTransitionToHooks(branch, () => {
React.render(<App location={location}/>, document.body);
});
}); |
A synchronous I like the recent snippets with var transitions = createTransitionManager(..);
HistoryLocation.listen((location) => {
var branch = App.match(location);
var transition = transitions.start();
resolveComponents(branch, () => {
// in case it was superseded by transition.start()
if (!transition.isActive()) return;
runTransitionHooks(branch, transition, () => {
// apply redirects etc; return .isActive() for convenience
if (!transition.finish()) return;
React.render(<App location={location}/>, document.body);
});
});
}); (I'm not super fond of the API, but I think something like this is necessary to manage asynchronous flows) |
Agree, we try to do too much & fail there. |
@taurose Interesting. I hadn't seen your comment on #952, but it makes a lot of sense. Thanks for the link. It seems like both of the early var manager = createTransitionManager(...);
HistoryLocation.listen(function (location) {
var match = App.match(location);
manager.resolveComponents(match, function () {
manager.transitionTo(match, function () {
React.render(<App location={location}/>, document.body);
});
});
}); |
Why do we pass |
@gaearon There is some work involved in calculating the |
Yep, but Sure, if user wants to traverse routes themselves, they want to call |
Just throwing in something that has been bugging me about transitions.
All of these are solved by throwing away willTransitionTo(router, retry) {
// transition.abort(); <--- meh
if (router.canGoBack()) {
router.goBack();
} else {
router.replaceWith('/some-other-page');
}
} Please correct me if I'm missing something obvious here. This also makes more sense on server than catching redirects and aborts in |
I guess we could put that check into
Not entirely disagreeing, but, on the other hand, it
As I said before, my concern is that if we removed But if we do leave it in, we should do something about // client
var manager = createTransitionManager(route, history);
history.listen(function(location) {
var transition = manager.startTransition();
somethingAsync(transition, function() {
if(manager.isActive(transition)) {
// ok
React.render(..);
} else if(transition.isAborted()){
// aborted by user; e.g. keep URL consistent
history.replaceWith(lastLocation.path);
// alternatively do nothing
} else {
// superseded/redirected
// nothing to do
}
});
});
// server: just a single transition
var transition = createTransition(route);
somethingAsync(transition, () => {
if(transition.isRedirected()){
res.redirect(transition.getRedirectPath());
} else if(transition.isAborted()){
res.sendStatus(500);
logError(transition.getAbortReason());
} else {
// ok
res.send(React.renderToString(..));
}
}); Edit: changed code snippet to process transition result manually instead of using handlers. |
Much of the information here is stale, so let's close. Thanks for the discussion tho everyone :) |
We had a great discussion on gitter this morning that helped to consolidate a few ideas around what we need from the router API going forward. In the shower, I thought about what everyone said and tried to put it all together. Here's what I propose:
Benefits of this approach over our current API:
<Link>
onChange
is much more explicit. Quite a few people have been asking about thisstate
API, so people don't rely on itHistoryLocation
will stillrequire('RefreshLocation')
so it can do the auto-fallback thingDrawbacks:
routes
prop looks likeObviously there are still some holes, like what the actual route definition looks like and how you add transition hooks, etc. But for a top-level API I really like this approach.
/cc @ryanflorence @gaearon @taurose
Update: Added more
propTypes
.Update: Added
shouldComponentTransition
lifecycle method.Update: Renamed
shouldComponentTransition
=>shouldComponentUnmount
.The text was updated successfully, but these errors were encountered: