Skip to content
This repository has been archived by the owner on Feb 25, 2020. It is now read-only.

handle new SwitchRouter history back behaviors #31

Merged
merged 8 commits into from
Feb 9, 2019

Conversation

slorber
Copy link
Member

@slorber slorber commented Jan 31, 2019

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.

@slorber
Copy link
Member Author

slorber commented Jan 31, 2019

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)

@brentvatne brentvatne self-requested a review February 3, 2019 19:08
@brentvatne
Copy link
Member

thank you! will review this soon

const resetOnBlur = config.hasOwnProperty('resetOnBlur')
? config.resetOnBlur
: true;
const initialRouteIndex = order.indexOf(initialRouteName);

const routeIndexHistory = [initialRouteIndex];
Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Member Author

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?

Copy link
Contributor

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.

Copy link
Member

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 :)

Copy link
Member Author

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

Copy link
Contributor

@ericvicenti ericvicenti left a 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

const resetOnBlur = config.hasOwnProperty('resetOnBlur')
? config.resetOnBlur
: true;
const initialRouteIndex = order.indexOf(initialRouteName);

const routeIndexHistory = [initialRouteIndex];
Copy link
Contributor

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.

@@ -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') {
Copy link
Contributor

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 {
Copy link
Contributor

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.

Copy link
Member Author

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

Copy link
Contributor

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?

Copy link
Member

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.

Copy link
Member Author

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

Copy link
Contributor

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); )

Copy link
Member

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

Copy link
Member Author

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

@brentvatne
Copy link
Member

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?

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.

@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)

I think the default should be the current behavior, initialRoute. If we decide at a later date that we want to change that we'll need to do a major version bump of react-navigation

const initialRouteIndex = order.indexOf(initialRouteName);
if (initialRouteIndex === -1) {
Copy link
Member Author

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

@slorber
Copy link
Member Author

slorber commented Feb 5, 2019

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 initialRoute, and eventually discuss switching to "history" in 4.0

type: NavigationActions.NAVIGATE,
routeName: 'C',
})
).toMatchObject({ index: 2, routeKeyHistory: ['B', 'A', 'C'] });
Copy link
Member Author

@slorber slorber Feb 5, 2019

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

Copy link
Contributor

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 👌

Copy link
Contributor

@ericvicenti ericvicenti left a comment

Choose a reason for hiding this comment

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

Looking great!

const routeKey =
state.routeKeyHistory[state.routeKeyHistory.length - 2];
activeChildIndex = order.indexOf(routeKey);
} else {
Copy link
Contributor

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 {
Copy link
Contributor

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'] });
Copy link
Contributor

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 👌

Copy link
Member

@brentvatne brentvatne left a 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

@@ -226,7 +263,7 @@ export default (routeConfigs, config = {}) => {
routes,
index: activeChildIndex,
};
return getNextState(prevState, nextState);
return updateRouteKeyHistory(getNextState(prevState, nextState));
Copy link
Member

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

Copy link
Member Author

Choose a reason for hiding this comment

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

done

...state,
index: activeChildIndex,
});
return updateRouteKeyHistory(
Copy link
Member

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

Copy link
Member Author

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 {
Copy link
Member

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

nextRouteKeyHistory = nextRouteKeyHistory.filter(k => k !== keyToAdd); // Deduplicate
nextRouteKeyHistory.push(keyToAdd);
}
if (action.type === NavigationActions.BACK) {
Copy link
Member

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) {
Copy link
Member Author

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);
Copy link
Member Author

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

@brentvatne
Copy link
Member

nice work @slorber :)

@brentvatne brentvatne merged commit 8e0041a into react-navigation:master Feb 9, 2019
@brentvatne
Copy link
Member

@slorber - want to open a pr on https://github.com/react-navigation/react-navigation.github.io with info on this change? :)

@slorber
Copy link
Member Author

slorber commented Feb 9, 2019

Thanks

I'll open some more PR with docs and examples next week

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

Successfully merging this pull request may close these issues.

3 participants