-
-
Notifications
You must be signed in to change notification settings - Fork 10.6k
[WIP] manage location through MatchProvider #4067
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
Conversation
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.
Looks pretty good, just a few things.
Also wondering if we should add in LocationSubscriber
to the miss component, as mentioned here
Also a case we still need to consider:
- When a miss component has
<Match>
's (or<Miss>
's) inside of it, currently they never update. Not sure how common this case is, but the solution is just to change<Miss>
to render in a similar way toMatch
- wrapped by a<LocationSubscriber>
and a<MatchContext>
element.
done() | ||
}) | ||
}) | ||
}) | ||
}) | ||
|
||
describe('FAILING MISS TESTS', () => { |
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.
Rename test to something descriptive now it's passing
@@ -5,7 +5,8 @@ import { | |||
|
|||
class MatchProvider extends React.Component { | |||
static childContextTypes = { | |||
match: matchContextType.isRequired | |||
match: matchContextType.isRequired, |
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.
Maybe rename this to provider
now it's dealing with misses and matches? Also makes it clear where this context originates from in child components.
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'll hold off on changing this, but I agree that match
is ambiguous and a different name might be preferable.
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.
Is there a reason for holding off? I feel like the Miss
component accessing the match
context and calling addMiss
on it could be confusing.
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.
There wasn't a compelling reason. I just pushed a change to rename match
to provider
. It can always be renamed again if something else is preferable.
}) | ||
|
||
const { serverRouter, match } = this.context | ||
if ( serverRouter && match ) { |
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.
Small thing - the convention seems to be no leading space in if
parentheses
@alisd23 With this, the As far as testing for a |
@@ -17,34 +18,35 @@ class MatchProvider extends React.Component { | |||
// **IMPORTANT** we must mutate matches, never reassign, in order for | |||
// server rendering to work w/ the two-pass render approach for Miss | |||
this.matches = [] |
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.
This actually breaks server rendering in its current form because that only looks at the number of elements in the matches
array of a <MatchProvider>
. The current test for that ('renders misses on second pass with server render context result'
) just doesn't fail because there are no <Match>
components to be added to the matches
array. I'll update that to be breaking so that that issue has to be addressed.
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.
It looks like server-rendering does work at the moment, that test is correct. The matches
in the current version is an array of objects for matching <Match>
's, whereas you're making it an array of functions, 1 from <Match>
component?
Did you mean this now breaks with the changes in this PR, or that it was broken originally?
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.
If you add the 'doesn't render misses on first pass'
test to the current v4
branch, it should fail.
If you look at the render
function of <Miss>
, it determines if it should render by calling the missedAtIndex
function. The current missedAtIndex
code just checks if the current item (determined by serverRouterIndex
) has matches in the matchesByIdentity
array and if there is a <Miss>
component for it.
The second check will always be true because the <Miss>
will have registered itself. The first check will be true (aka there are no matches) if no <Match>
components match prior to the <Miss>
component rendering. If there is <Match>
that matches the current location, but it is rendered after a <Miss>
, that <Miss>
will be rendered on the first pass.
d27c73a
to
79d1143
Compare
I had to pretty heavily modify the
Some additional tests might be needed to verify that this is working correctly. |
}) - 1 | ||
// on the second pass, index is determined by render order | ||
const registerMatchProvider = () => { | ||
return flushed ? flushedIndex++ : ( |
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.
Am I correct here in believing that the order in which <MatchProvider>
s will be rendered is always the same for the first and second pass?
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.
Because registerMatchContext
is called in componentWillMount
, and (I think) componentWillMount
is called in sequential order in the children - it is determinstic.
But also: what is the reason for flushedIndex
? Doesn't seem to be used anywhere apart from the incrementing.
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.
On the first pass, each <MatchProvider>
adds an item to the matchContexts
arrays. This is done through the registerMatchProvider
function, which returns the index of the added item. That index is stored by the <MatchProvider>
as serverRouterIndex
. While the first pass is rendering, it will use the index to manipulate the item in order to indicate if it a) has any matches and b) has any <Miss>
components.
On the second pass, we want to reference the items in the array that were created during the first pass. For each <MatchProvider>
, we receive the index for the correct item in the matchContexts
array via the call to registerMatchProvider
. This is done using flushedIndex
variable, simply incrementing the index returned on each call (that's why the render order is important).
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.
Ok I see, so you're just replacing the array add behaviour (which returns an incrementing index each time its called) with an incrementing integer variable. Makes sense.
] | ||
|
||
const setRedirect = flushed ? k : (location) => { |
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'm not entirely sure what the idea with these ternary statements are. They execute when createServerRenderContext
is called, at which point flushed === false
, so they are only ever the second option.
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 think it was probably there because the second render pass might still trigger that setRedirect
function, so it is actually possible to trigger k
(although you're unlikely to see a chain of redirects), and I agree you'd never use that redirect
value again, so modifying doesn't actually affect anything.
@pshrmn Yeah actually I agree a Match inside a Miss can be done in other ways like using parent components. That makes the |
02c8dde
to
3ab8157
Compare
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'm happy with this! Best have someone else review this properly as well though, as it's quite a big change.
@timdorr want to take a look at this? |
I will, but after this weekend. Crazy busy and I want to give your 3.0 PR some attention so we can ship that version. |
Sure, no pressure 👍 |
Please see 227a2ed before doing anymore work in here. |
Basically, we can't use Yes, we can do it today and it "works" with componentWillMount, but I've talked to @sebmarkbage about this at length and writing code that is order dependent on In other words, we have to do a two-pass server render for exceptional cases (like 404s), and we just have to deal with not having the same checksum for now. Also see #3877 (comment) That said, I'm confident we'll eventually figure out how to create an API like |
@ryanflorence which |
The only thing that is order dependent is when If there is, then The only reason that I can think of for non-deterministic render order would be if someone is randomly shuffling their components. [0] https://github.com/pshrmn/react-router/blob/matchprovider-location/modules/MatchProvider.js#L60-L61 |
5b4b96f
to
b415d41
Compare
The <MatchProvider> maintains references to each of its <Match> and <Miss> child components through subscriptions. Each time that the location changes, the <MatchProvider> will update its <Match> components through their subscribed functions. Each update function returns a boolean indicating if the <Match> matches the current pattern. Once the <Match>es have updated, the <MatchProvider> will update any <Miss> children through their subscription functions, informing them if there were any matches for the current location.
b415d41
to
ae92492
Compare
Thankfully I think randomly ordering components would be a very rare case, and it can be solved by doing the randomization on the actual data, before both renders. The
The logic in |
The arguments to the constructor are class Match extends React.Component {
constructor(props, context) {
super(props, context)
this.state = {
match: this.matchCurrent(props.location || context.location)
}
}
componentWillMount() {
const { match } = this.state
const { serverRouter, provider } = this.context
if (serverRouter && provider) {
// ...
}
}
}
That The randomization was the only instance I could think of which could break knowing the render order, but I'd be hard pressed to come up with a legitimate reason to do that. Since the reconciliation between server and client rendering fails if the server returns second pass code, I'm not sure that its necessary to address anyways. |
@pshrmn Oh yeah - forgot about that, my brain failed me there... |
this.registerMatch() | ||
this.update = (location) => { | ||
const match = this.matchCurrent(location) | ||
this.setState({ |
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.
This could be optimized to check if the new match matches the current this.state.match
and not calling this.setState
if it does.
what's the motivation here?
Now that we're using Might be able to go the other way and use react broadcast for |
(Sorry, this is a bit long) The original intent was that the hierarchy of RR boils down to:
Current ModelA The only way that a Normally, this is not an issue because the
When a component that calls
Now, a duct tape fix would be to modify MatchProvider is location awareNote: while writing this I realized that the When the
The "magic" behind this is that
For a normal update cycle (no In summaryBy keeping the location in This should simplify the logic of RR because location changes flow in the same direction as the hierarchy. @ryanflorence If you decide to go this route, the |
@pshrmn Just to remind me (completely forgotten how this all works now), this will still require 2 render passes to render a miss? Seeing is this is quite a major and general change to the MatchProvider logic, maybe worth looking at the 2 pass Also conflicts 😄 |
@alisd23 From sebmarkbage's comment facebook/react#7671 (comment)
With Fibers we cannot rely on There really is no way to determine if a A non-React example would be if we want to walk through an array and call a
Now, it is possible to have a
It's a PITA because you're basically duplicating your app and React just so that the first pass on the client matches the second pass output of React on the server.
|
Oh so it's With the |
While I think that this approach could work, it gets a bit complicated when dealing with managing async components and it might be easier to handle that with calling Either way, I'm going to close this. |
@pshrmn @ryanflorence do you guys think using some kind of |
That's what |
@timdorr I get that but it's still and extra thing we have to do manually. if |
*would not be necessary |
Subscriptions do not work well in a decentralized system because of React's https://medium.com/@pshrmn/ditching-subscriptions-in-react-router-6496c71ce4ec I wrote this article a while ago, and while it is probably a bit longer than it needs to be, I think that it pretty thoroughly covers why React Router no longer does subscriptions (and why that isn't a bad thing!) |
I understand it's decentralized, but wouldn't it be pretty easy to make every component that puts a |
Wait no, I can't imagine a case in which that would happen, because a new instance of a parent component and its observable location instance wouldn't get created without the descendant's So far I've never encountered a case where I wanted to pass down dynamic values via context that I couldn't solve by putting some observable-type thing on context. |
Now I realize, even if an observable location is on each Route's child context, handling location changes in user-written components (the kind that currently take the current |
This is a way to manage location changes that is resistant to
shouldComponentUpdate
.The main change is that
<MatchProvider>
components are kept updated about the current location. All<Match>
and<Miss>
children of a<MatchProvider>
provide it with update functions. The<Match>
update functions return a boolean indicating whether they match against the current location. The<Miss>
update function is the same as the current one.<Match>
maintains amatch
state, which is the result ofmatchPattern
and is set whenever the component updates naturally (throughcomponentWillReceiveProps
) or when itsupdate
function is called.This passes the failing miss test that @alisd23 wrote in #4047, but may need additional tests to verify its correctness.
Related issue: #4035