-
Notifications
You must be signed in to change notification settings - Fork 46
handle new SwitchRouter history back behaviors #31
handle new SwitchRouter history back behaviors #31
Conversation
cc @satya164 @ericvicenti @brentvatne @satya164 your RFC did not set the history option as default backbehavior, should we do this? I've seen this in your comment here react-navigation/rfcs#2 (comment) |
thank you! will review this soon |
src/routers/SwitchRouter.js
Outdated
const resetOnBlur = config.hasOwnProperty('resetOnBlur') | ||
? config.resetOnBlur | ||
: true; | ||
const initialRouteIndex = order.indexOf(initialRouteName); | ||
|
||
const routeIndexHistory = [initialRouteIndex]; |
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 like this is state contained in the router. Routers are currently entirely stateless, in order to make navigation actions predictable and debuggable. The rest of the stuff out in this context is router configuration. (In theory, stateless routers could enable dynamic routers, where the whole router gets swapped out, although we don't really handle that now.)
So I think we should make sure that anything "history" related lives in navigation state, even if it means we need to add an extra key like "routeIndexHistory" for the switch router in this mode.
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.
But also, I think maybe the route history could actually be saved in state.routes, so that the navigation state resembles the stack nav state.
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. As this state is exposed to users I'll let you choose the key name.
Also I choose the history to have min length = 1 but I could have also made it min length 0,do you have a preference?
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.
Personally I prefer using route.key
over index, which would semantically preserve order if the routes changed for some reason. I think routeKeyHistory
is a sensible name. Lets make sure to only assign that property when this router mode is used.
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.
agree with @ericvicenti's comments here :)
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.
Hi, I've made the change to add a routeKeyHistory
to the state.
As far as I understand, for a switch router the key === name right? as it does not make any sense to navigate to the same screen under different names?
Also, adding this history complicates slightly the code, as there are many places where the next state can be returned. I'm not totally sure to have covered all the edge cases but at least my test is passing. Please tell me what you think
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.
Thank you for working on this! I'm excited to get you ramped up on core
src/routers/SwitchRouter.js
Outdated
const resetOnBlur = config.hasOwnProperty('resetOnBlur') | ||
? config.resetOnBlur | ||
: true; | ||
const initialRouteIndex = order.indexOf(initialRouteName); | ||
|
||
const routeIndexHistory = [initialRouteIndex]; |
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.
But also, I think maybe the route history could actually be saved in state.routes, so that the navigation state resembles the stack nav state.
src/routers/SwitchRouter.js
Outdated
@@ -177,8 +196,16 @@ export default (routeConfigs, config = {}) => { | |||
const isBackEligible = | |||
action.key == null || action.key === activeChildLastState.key; | |||
if (action.type === NavigationActions.BACK) { | |||
if (isBackEligible && shouldBackNavigateToInitialRoute) { | |||
activeChildIndex = initialRouteIndex; | |||
if (isBackEligible && backBehavior !== 'none') { |
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.
these ifs can be flattened, and IMO it is easier to read that way. Redundant else return state
can be eliminated.
// A simple helper that makes it easier to write basic routing tests | ||
// As we generally want to apply one action after the other, | ||
// it's often convenient to manipulate a structure that keeps the router state | ||
class ExampleRouterHelper { |
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.
As a general rule I tend to avoid utilities or helpers in my tests, because they make it harder to see what is actually being tested. The rest of the tests in this library may be verbose, but they follow this philosophy.
For example, how often do the new tests check against the initial state? Only once. What if you pass a navigate action in with a null state? The helper makes it impossible to test that case.
That being said, this helper isn't the worst example, and the philosophy is just my opinion. So I'll leave it to Brent and your judgement on this one.
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 see. I somehow agree with your philosophy but also I think the helper helps avoiding typos in tests. For example when writing tests with no helper I first made a typo where I passed state2 instead of state.
I'll let you and Brent choose and I'm OK to revert, but we should rather keep tests consistant with each other's
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 agree we should be consistent, but we could slowly migrate other tests if the best practice changes. @brentvatne do you have a preference?
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 agree with @slorber that it can be a bit tedious to make sure you're threading through the correct state variables. We should pull a class like this out to a helper file, call it something like RouterTestHelper
, and have it accept a param for how it should initialize the router. Let's do that in a follow-up PR and change other tests to use it at the same time, and leave this PR with the helper as it is for now.
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'll try to do another PR. I'll do one for SwitchRouter first and then StackRouter. StackRouter has more complicated tests so it will mostly apply to tests of getStateForAction
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.
Sounds good!
(Also I can't help but humbly mention my preference for functions+return objects+closures over ES classes.. const { applyAction, getState } = getRouterTestHelper(router);
)
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.
@ericvicenti - i'd be curious to see what that looks like next to the class version
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 can give a try in another PR and you compare. I'm fine with both as long as the tests are easy to read
I updated the example project to SDK32 and added a good way to do test @react-navigation/core changes in it: https://github.com/react-navigation/react-navigation-core#development-workflow - hope that helps! If you rebase this PR on master you'll be able to use it.
I think the default should be the current behavior, |
const initialRouteIndex = order.indexOf(initialRouteName); | ||
if (initialRouteIndex === -1) { |
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.
moved this check closer to initialRouteIndex, seems appropriate
I've made the changes required to put the history to navigation state. Not sure exactly what I'm doing so take care reviewing this :D Will try to check to add an example soon. Agree we can keep for now the |
type: NavigationActions.NAVIGATE, | ||
routeName: 'C', | ||
}) | ||
).toMatchObject({ index: 2, routeKeyHistory: ['B', 'A', 'C'] }); |
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.
just a dev note: here the object is matched "partially" but the array is matched "fully". Not really explicit from jest doc though...
If the state contains ['B', 'A', 'C', 'D']
it will effectively fail because of presence of D
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.
Makes sense that its a shallow 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.
Looking great!
src/routers/SwitchRouter.js
Outdated
const routeKey = | ||
state.routeKeyHistory[state.routeKeyHistory.length - 2]; | ||
activeChildIndex = order.indexOf(routeKey); | ||
} else { |
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 like you can flatten this out again 😛
// A simple helper that makes it easier to write basic routing tests | ||
// As we generally want to apply one action after the other, | ||
// it's often convenient to manipulate a structure that keeps the router state | ||
class ExampleRouterHelper { |
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.
Sounds good!
(Also I can't help but humbly mention my preference for functions+return objects+closures over ES classes.. const { applyAction, getState } = getRouterTestHelper(router);
)
type: NavigationActions.NAVIGATE, | ||
routeName: 'C', | ||
}) | ||
).toMatchObject({ index: 2, routeKeyHistory: ['B', 'A', 'C'] }); |
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.
Makes sense that its a shallow 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.
just a couple of small comments, thanks for doing this :D
src/routers/SwitchRouter.js
Outdated
@@ -226,7 +263,7 @@ export default (routeConfigs, config = {}) => { | |||
routes, | |||
index: activeChildIndex, | |||
}; | |||
return getNextState(prevState, nextState); | |||
return updateRouteKeyHistory(getNextState(prevState, nextState)); |
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.
can we move updateRouteKeyHistory
into getNextState
? afraid of it being too easy to accidentally not update the route key history in the future
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.
done
src/routers/SwitchRouter.js
Outdated
...state, | ||
index: activeChildIndex, | ||
}); | ||
return updateRouteKeyHistory( |
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.
same as above re: moving to getNextState
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.
done
// A simple helper that makes it easier to write basic routing tests | ||
// As we generally want to apply one action after the other, | ||
// it's often convenient to manipulate a structure that keeps the router state | ||
class ExampleRouterHelper { |
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.
@ericvicenti - i'd be curious to see what that looks like next to the class version
src/routers/SwitchRouter.js
Outdated
nextRouteKeyHistory = nextRouteKeyHistory.filter(k => k !== keyToAdd); // Deduplicate | ||
nextRouteKeyHistory.push(keyToAdd); | ||
} | ||
if (action.type === NavigationActions.BACK) { |
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.
else if
function getNextState(prevState, possibleNextState) { | ||
if (!prevState) { | ||
return possibleNextState; | ||
function getNextState(action, prevState, possibleNextState) { |
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.
@brentvatne I added action to the params list there as it's needed for updateNextStateHistory.
Didn't want to move getNextState inside getStateForActions. Don't really like to have a lot of deeply nested closures whose captured variables are not very "visible", keeping the function outside makes it clearer which are the variable dependencies.
Also put updateNextStateHistory inside getNextState as it's the only place where it's used
} | ||
|
||
return nextState; | ||
return updateNextStateHistory(nextState); |
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 simplified a bit the getNextState original code, it's clearer to have a single return path imho
nice work @slorber :) |
@slorber - want to open a pr on https://github.com/react-navigation/react-navigation.github.io with info on this change? :) |
Thanks I'll open some more PR with docs and examples next week |
Hi,
This is a proposal implementation for new SwitchRouter backBehaviors documented in RFC:
https://github.com/react-navigation/rfcs/blob/master/text/0008-back-behaviour-for-tabs.md
react-navigation/rfcs#2
I didn't test it in a real app but the tests are passing and seems ok regarding the RFC, is there any easy setup to do so?
Also note I created a
ExampleRouterHelper
which makes tests easier to read, by creating a "stateful router", because most of the tests are generally executing one action after the other. If you like this better I can rewrite all the tests this way in another PR (or this one, your choice), or revert to the previous way of testing which involves creating many local state variables. I can also try to apply the same technique when appropriate on other test files by creating a more generic helper.