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

Fix duplicate workspaces being created on every sign in #10122

Merged
merged 12 commits into from
Aug 2, 2022
Merged
3 changes: 0 additions & 3 deletions src/ONYXKEYS.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,6 @@ export default {
// Note: These are Persisted Requests - not all requests in the main queue as the key name might lead one to believe
PERSISTED_REQUESTS: 'networkRequestQueue',

// What the active route is for our navigator. Global route that determines what views to display.
CURRENT_URL: 'currentURL',

// Stores current date
CURRENT_DATE: 'currentDate',

Expand Down
10 changes: 9 additions & 1 deletion src/libs/Navigation/AppNavigator/AuthScreens.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Onyx, {withOnyx} from 'react-native-onyx';
import moment from 'moment';
import _ from 'underscore';
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
import * as StyleUtils from '../../../styles/StyleUtils';
import withWindowDimensions, {windowDimensionsPropTypes} from '../../../components/withWindowDimensions';
import CONST from '../../../CONST';
Expand Down Expand Up @@ -86,6 +87,9 @@ const modalScreenListeners = {

const propTypes = {
...windowDimensionsPropTypes,

/** The current path as reported by the NavigationContainer */
currentPath: PropTypes.string.isRequired,
};

class AuthScreens extends React.Component {
Expand Down Expand Up @@ -115,7 +119,6 @@ class AuthScreens extends React.Component {
App.openApp(this.props.allPolicies);

App.fixAccountAndReloadData();
App.setUpPoliciesAndNavigate(this.props.session);
Timing.end(CONST.TIMING.HOMEPAGE_INITIAL_RENDER);

const searchShortcutConfig = CONST.KEYBOARD_SHORTCUTS.SEARCH;
Expand All @@ -133,6 +136,11 @@ class AuthScreens extends React.Component {
}

shouldComponentUpdate(nextProps) {
// we perform this check here instead of componentDidUpdate to skip an unnecessary re-render
if (this.props.currentPath !== nextProps.currentPath) {
App.setUpPoliciesAndNavigate(nextProps.session, nextProps.currentPath);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Ah haha. My immediate thought was that it would be better to do this in componentDidUpdate() vs shouldComponentUpdate(). As latter is used to tell the component whether it should re-render and not really for side effects (though I guess we are doing similar stuff elsewhere :oh-nothing:).

The code on line 143 is preventing the AuthScreens from updating when the currentPath changes (which solves my concern shared in the original issue about unnecessary re-rendering). But also explains why we are not using componentDidUpdate() because it will only update if the isSmallScreenWidth props change. 😄

Another option would be to allow the component to update when the currentPath changes and then we could use componentDidUpdate(), but that would trigger a re-render.

If we still prefer this logic to be here and not in the NavigationRoot I'd maybe add a comment or two to explain why we are doing this and why it needs to happen in shouldComponentUpdate()

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah doing this here in shouldComponentUpdate() is a little micro-optimization. I'll add a comment to make that clear 👍🏽

Copy link
Contributor

Choose a reason for hiding this comment

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

So to make sure that I understand this correctly, App.setUpPoliciesAndNavigate will be called once the NavigationRoot passes down the currentPath, which ensures that it only runs when the navigation is ready and the user is signed in. If that's correct then it looks good to me.

I think that would make this redundant, but it's probably wise to leave the check just in case.

App/src/libs/actions/App.js

Lines 176 to 178 in 0c2c2f6

if (!session || !currentPath) {
return;
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually wouldn't we be calling this any time the path changes? 🤔 and isn't that like every state change for the navigator? We just want it to run once though don't we?

Copy link
Contributor

Choose a reason for hiding this comment

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

I just came across this code today while investigating #10271 and I think it's a bad idea to put this logic inside of shouldComponentUpdate(). It's an anti-pattern (not what the purpose of the lifecycle method is for) and adds a side-effect in a place that's not really appropriate.

It also leads to this:

Actually wouldn't we be calling this any time the path changes? 🤔 and isn't that like every state change for the navigator? We just want it to run once though don't we?

Which I think is a bad idea and not what is wanted.

Copy link
Contributor

Choose a reason for hiding this comment

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

Do you mean this proposal? Not sure if you can see it or not. I can see it from the conversation page of the PR.

image

I was afraid someone was gonna ask me for a suggestion on how to fix it 😓

Honestly, I'm a bit lost at everything that was done in this PR and I don't really understand how it fixes the original problem. I am going back now to read the GH closer as I see you struggled with understanding this in the beginning too. I'll see if I can follow along there.

Copy link
Contributor

Choose a reason for hiding this comment

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

OK, I think I followed some of that. It looks like at some point in the conversation everyone agreed that window.location.href would work the best and be the most simple, but then, it was never attempted?

I think we should go that route. I agree with you that on native platforms, it can be a no-op and that's fine.

// index.js
function getCurrentUrl() {
    return window.location.href;
}

// index.native.js
function getCurrentUrl() {
    return '';
}

I wouldn't mind seeing how simple that would make all of this. Would it mean that all the changes in this PR can be reverted? I think that would be great if it's possible.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh nevermind my old proposal is here in the issue :D https://github.com/Expensify/Expensify/issues/217505#issuecomment-1196040735

Copy link
Contributor

Choose a reason for hiding this comment

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

@arosiclair as a matter of cleanup, could you look into switching this to window.location.href and see if that could simplify things and remove this side-effect?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah we can take the window.location.href approach since this can be messy. It's be a pretty simple change so I'll submit another PR and add you guys as reviewers


return nextProps.isSmallScreenWidth !== this.props.isSmallScreenWidth;
}

Expand Down
5 changes: 4 additions & 1 deletion src/libs/Navigation/AppNavigator/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@ import AuthScreens from './AuthScreens';
const propTypes = {
/** If we have an authToken this is true */
authenticated: PropTypes.bool.isRequired,

/** The current path as reported by the NavigationContainer */
currentPath: PropTypes.string.isRequired,
};

const AppNavigator = props => (
props.authenticated
? (

// These are the protected screens and only accessible when an authToken is present
<AuthScreens />
<AuthScreens currentPath={props.currentPath} />
)
: (
<PublicScreens />
Expand Down
16 changes: 10 additions & 6 deletions src/libs/Navigation/NavigationRoot.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {getPathFromState, NavigationContainer, DefaultTheme} from '@react-naviga
import * as Navigation from './Navigation';
import linkingConfig from './linkingConfig';
import AppNavigator from './AppNavigator';
import * as App from '../actions/App';
import FullScreenLoadingIndicator from '../../components/FullscreenLoadingIndicator';
import Log from '../Log';
import colors from '../../styles/colors';
Expand All @@ -31,6 +30,10 @@ class NavigationRoot extends Component {
constructor(props) {
super(props);

this.state = {
currentPath: '',
};

this.parseAndStoreRoute = this.parseAndStoreRoute.bind(this);
}

Expand All @@ -43,15 +46,16 @@ class NavigationRoot extends Component {
return;
}

const path = getPathFromState(state, linkingConfig.config);
const currentPath = getPathFromState(state, linkingConfig.config);

// Don't log the route transitions from OldDot because they contain authTokens
if (path.includes('/transition')) {
if (currentPath.includes('/transition')) {
Log.info('Navigating from transition link from OldDot using short lived authToken');
} else {
Log.info('Navigating to route', false, {path});
Log.info('Navigating to route', false, {path: currentPath});
}
App.setCurrentURL(path);
marcaaron marked this conversation as resolved.
Show resolved Hide resolved

this.setState({currentPath});
Copy link
Contributor

Choose a reason for hiding this comment

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

NAB, I still feel icky about triggering a re-render of the NavigationContainer - but can't really back up this concern with any facts or evidence about why we should not do it so not gonna block on it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I see what you mean but for the record, child components only re-render if their props change. So it's safe to re-render the root component (or any super high ancestor) of an app so long as it doesn't cascade a massive change of props down the whole app.

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 it doesn't matter much in this case because we are preventing re-renders in AuthScreens 😄

}

render() {
Expand All @@ -72,7 +76,7 @@ class NavigationRoot extends Component {
enabled: false,
}}
>
<AppNavigator authenticated={this.props.authenticated} />
<AppNavigator authenticated={this.props.authenticated} currentPath={this.state.currentPath} />
</NavigationContainer>
);
}
Expand Down
72 changes: 32 additions & 40 deletions src/libs/actions/App.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {AppState, Linking} from 'react-native';
import {AppState} from 'react-native';
import Onyx from 'react-native-onyx';
import lodashGet from 'lodash/get';
import Str from 'expensify-common/lib/str';
Expand Down Expand Up @@ -45,13 +45,6 @@ Onyx.connect({
},
});

/**
* @param {String} url
*/
function setCurrentURL(url) {
Onyx.set(ONYXKEYS.CURRENT_URL, url);
}

/**
* @param {String} locale
*/
Expand Down Expand Up @@ -156,13 +149,10 @@ function fixAccountAndReloadData() {
}

/**
* This action runs every time the AuthScreens are mounted. The navigator may
* not be ready yet, and therefore we need to wait before navigating within this
* action and any actions this method calls.
* This action runs when the Navigator is ready and the current route changes
*
* currentPath should be the path as reported by the NavigationContainer
*
* getInitialURL allows us to access params from the transition link more easily
* than trying to extract them from the navigation state.

* The transition link contains an exitTo param that contains the route to
* navigate to after the user is signed in. A user can transition from OldDot
* with a different account than the one they are currently signed in with, so
Expand All @@ -177,33 +167,36 @@ function fixAccountAndReloadData() {
* pass it in as a parameter. withOnyx guarantees that the value has been read
* from Onyx because it will not render the AuthScreens until that point.
* @param {Object} session
* @param {string} currentPath
*/
function setUpPoliciesAndNavigate(session) {
Linking.getInitialURL()
.then((url) => {
if (!url) {
return;
}
const path = new URL(url).pathname;
const params = new URLSearchParams(url);
const exitTo = params.get('exitTo');
const isLoggingInAsNewUser = SessionUtils.isLoggingInAsNewUser(url, session.email);
const shouldCreateFreePolicy = !isLoggingInAsNewUser
&& Str.startsWith(path, Str.normalizeUrl(ROUTES.TRANSITION_FROM_OLD_DOT))
function setUpPoliciesAndNavigate(session, currentPath) {
neil-marcellini marked this conversation as resolved.
Show resolved Hide resolved
if (!session || !currentPath || !currentPath.includes('exitTo')) {
return;
}

let exitTo;
try {
neil-marcellini marked this conversation as resolved.
Show resolved Hide resolved
const url = new URL(currentPath, CONST.NEW_EXPENSIFY_URL);
exitTo = url.searchParams.get('exitTo');
} catch (error) {
// URLSearchParams is unsupported on iOS so we catch th error and
// silence it here since this is primarily a Web flow
return;
}

const isLoggingInAsNewUser = SessionUtils.isLoggingInAsNewUser(currentPath, session.email);
const shouldCreateFreePolicy = !isLoggingInAsNewUser
&& Str.startsWith(currentPath, Str.normalizeUrl(ROUTES.TRANSITION_FROM_OLD_DOT))
&& exitTo === ROUTES.WORKSPACE_NEW;
if (shouldCreateFreePolicy) {
Policy.createAndGetPolicyList();
return;
}
if (!isLoggingInAsNewUser && exitTo) {
Navigation.isNavigationReady()
.then(() => {
// We must call dismissModal() to remove the /transition route from history
Navigation.dismissModal();
Navigation.navigate(exitTo);
});
}
});
if (shouldCreateFreePolicy) {
Policy.createAndGetPolicyList();
return;
}
if (!isLoggingInAsNewUser && exitTo) {
// We must call dismissModal() to remove the /transition route from history
Navigation.dismissModal();
Navigation.navigate(exitTo);
}
}

// When the app reconnects from being offline, fetch all initialization data
Expand All @@ -213,7 +206,6 @@ NetworkConnection.onReconnect(() => {
});

export {
setCurrentURL,
setLocale,
setSidebarLoaded,
getAppData,
Expand Down