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

State: Type route path & query #55315

Open
wants to merge 3 commits into
base: trunk
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions client/blocks/login/login-form.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -687,8 +687,8 @@ export default connect(
socialAccountLinkService: getSocialAccountLinkService( state ),
userEmail:
props.userEmail ||
getInitialQueryArguments( state ).email_address ||
getCurrentQueryArguments( state ).email_address,
getInitialQueryArguments( state )?.email_address ||
getCurrentQueryArguments( state )?.email_address,
wccomFrom: get( getCurrentQueryArguments( state ), 'wccom-from' ),
currentQuery: getCurrentQueryArguments( state ),
};
Expand Down
4 changes: 3 additions & 1 deletion client/data/home/use-home-layout-query-params.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import config from '@automattic/calypso-config';
import { get } from 'lodash';
import { useSelector } from 'react-redux';
import { getCurrentQueryArguments } from 'calypso/state/selectors/get-current-query-arguments';

Expand All @@ -8,7 +9,8 @@ export interface HomeLayoutQueryParams {
}

export function useHomeLayoutQueryParams(): HomeLayoutQueryParams {
const { dev, view } = useSelector( getCurrentQueryArguments ) as any;
const dev = get( useSelector( getCurrentQueryArguments ), 'dev', undefined );
const view = get( useSelector( getCurrentQueryArguments ), 'view', undefined );
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't look like an improvement. Just const { dev, view } = useSelector( ... ) should work. If it doesn't, there's something wrong with the types and they should be fixed. Calling the selector twice and introducing a new usage of Lodash is not good.


return {
dev: dev === 'true' || ( ! dev && config.isEnabled( 'home/layout-dev' ) ) || undefined,
Expand Down
4 changes: 2 additions & 2 deletions client/login/magic-login/request-login-email-form.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,8 @@ const mapState = ( state ) => {
emailRequested: getMagicLoginRequestedEmailSuccessfully( state ),
userEmail:
getLastCheckedUsernameOrEmail( state ) ||
getCurrentQueryArguments( state ).email_address ||
getInitialQueryArguments( state ).email_address,
getCurrentQueryArguments( state )?.email_address ||
getInitialQueryArguments( state )?.email_address,
};
};

Expand Down
4 changes: 2 additions & 2 deletions client/login/wp-login/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -313,8 +313,8 @@ export default connect(
oauth2Client: getCurrentOAuth2Client( state ),
isLoginView: ! props.twoFactorAuthType && ! props.socialConnect,
emailQueryParam:
getCurrentQueryArguments( state ).email_address ||
getInitialQueryArguments( state ).email_address,
getCurrentQueryArguments( state )?.email_address ||
getInitialQueryArguments( state )?.email_address,
} ),
{
recordPageView: withEnhancers( recordPageView, [ enhanceWithSiteType ] ),
Expand Down
27 changes: 0 additions & 27 deletions client/state/route/actions.js

This file was deleted.

27 changes: 27 additions & 0 deletions client/state/route/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/* eslint-disable jsdoc/require-param */
/**
* Internal dependencies
*/
import 'calypso/state/ui/init';
import { ROUTE_SET, ROUTE_CLEAR_LAST_NON_EDITOR } from 'calypso/state/action-types';
import { RouteClearActionCreator, RouteSetActionCreator } from 'calypso/state/route/types';

/**
* Returns an action object signalling that the current route is to be changed
*/
export const setRoute: RouteSetActionCreator = ( path, query = {} ) => {
return {
type: ROUTE_SET,
path,
query,
};
};

/**
* Action to forget what the last non-editor route was.
*/
export const clearLastNonEditorRoute: RouteClearActionCreator = () => {
return {
type: ROUTE_CLEAR_LAST_NON_EDITOR,
};
};
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
/**
* External dependencies
*/
import { Reducer, AnyAction } from 'redux';
/**
* Internal dependencies
*/
import { ROUTE_CLEAR_LAST_NON_EDITOR, ROUTE_SET } from 'calypso/state/action-types';
Copy link
Member

Choose a reason for hiding this comment

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

This PR adds a reducer.ts file, but doesn't delete the old reducer.js one.


/**
Expand All @@ -8,7 +15,7 @@ import { ROUTE_CLEAR_LAST_NON_EDITOR, ROUTE_SET } from 'calypso/state/action-typ
*/
const editorPattern = /^\/(block-editor|page[^s]|post[^s])/;

export const lastNonEditorRouteReducer = ( state = '', action ) => {
export const lastNonEditorRouteReducer: Reducer< string, AnyAction > = ( state = '', action ) => {
const { path, type } = action;
switch ( type ) {
case ROUTE_SET:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
/**
* External dependencies
*/
import { Reducer, AnyAction } from 'redux';
/**
* Internal dependencies
*/
import { ROUTE_SET } from 'calypso/state/action-types';
import { PathState } from 'calypso/state/route/types';

const initialState = {
initial: '',
current: '',
previous: '',
};

export const pathReducer = ( state = initialState, action ) => {
export const pathReducer: Reducer< PathState, AnyAction > = ( state = initialState, action ) => {
const { path, type } = action;
switch ( type ) {
case ROUTE_SET:
Expand Down
34 changes: 0 additions & 34 deletions client/state/route/query/reducer.js

This file was deleted.

45 changes: 45 additions & 0 deletions client/state/route/query/reducer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* External dependencies
*/
import { isEqual, omit } from 'lodash';
import { Reducer, AnyAction } from 'redux';
/**
* Internal dependencies
*/
import { ROUTE_SET } from 'calypso/state/action-types';
import { Query, QueryState } from 'calypso/state/route/types';

const timestamped = ( query: Query ): Query => ( {
...query,
_timestamp: Date.now(),
} );

const isEqualQuery = ( state: QueryState[ keyof QueryState ], query: Query ) =>
isEqual( omit( state, '_timestamp' ), omit( query, '_timestamp' ) );

const initialReducer = ( state: QueryState[ 'initial' ], query: Query ) =>
state === false ? timestamped( query ) : state;

const currentReducer = ( state: QueryState[ 'current' ], query: Query ) =>
! isEqualQuery( state, query ) ? timestamped( query ) : state;

const initialState: QueryState = {
initial: false,
current: false,
previous: false,
};

export const queryReducer: Reducer< QueryState, AnyAction > = ( state = initialState, action ) => {
const { query, type } = action;
switch ( type ) {
case ROUTE_SET:
return {
initial: initialReducer( state.initial, query ),
current: currentReducer( state.current, query ),
previous: state.current === false ? false : state.current,
};
}
return state;
};

export default queryReducer;
31 changes: 31 additions & 0 deletions client/state/route/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* External dependencies
*/
import { Action } from 'redux';

export type Query = {
[ key: string ]: string | number | undefined;
_timestamp?: number;
};
Comment on lines +6 to +9
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not fun of this type. Hopefully, there's a way to express it differently. _timestamp is the only property of type number that may also be undefined - everything else is a string.

Copy link
Member

Choose a reason for hiding this comment

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

The _timestamp shouldn't really be here on the query, but rather on a separate property of the state.route object. There would be { path, query, timestamp } properties there.

The _timestamp field is currently used only by the Guided Tours code and nowhere else. The meaning is "the time at which the last (initial, previous) route navigation happened".

Adding types reveals such hacks, like slapping non-query data on the query object, and nudges us toward fixing them.


export type QueryState = {
initial: Query | false;
current: Query | false;
previous: Query | false;
};

export type PathState = {
initial: string;
current: string;
previous: string;
};

export type RouteState = {
lastNonEditorRoute: string;
path: PathState;
query: QueryState;
Copy link
Member

Choose a reason for hiding this comment

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

There is also a lastNonEditorRoute: string field in RouteState

};

export type RouteClearActionCreator = () => Action;

export type RouteSetActionCreator = ( path: string, query: Query ) => Action;
13 changes: 0 additions & 13 deletions client/state/selectors/get-current-query-arguments.js

This file was deleted.

25 changes: 25 additions & 0 deletions client/state/selectors/get-current-query-arguments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/* eslint-disable jsdoc/require-param */

/**
* Internal dependencies
*/
import 'calypso/state/route/init';

/**
* Type dependencies
*/
import type { Query } from 'calypso/state/route/types';
import type { IAppState } from 'calypso/state/types';

/**
* Gets the last query parameters set by a ROUTE_SET action
*
* TODO: We would be better off not conditioning with null here.
* Kept for backwards compatibility in case any null checks.
* Investigate and refactor.
*/
export function getCurrentQueryArguments( state: IAppState ): Query | false | null {
Copy link
Member

Choose a reason for hiding this comment

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

The return type should be just Query, and the selector should return an empty {} object if the current URL doesn't have a query string. That's the API most convenient for the caller.

Copy link
Contributor Author

@chriskmnds chriskmnds Nov 1, 2021

Choose a reason for hiding this comment

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

I feel the selector could just return whatever state.route.query.current points to. 🤔 No need to coalesce null in the mix as we do here either (as also done previously). So either undefined or state.route.query.current -> ultimately, just return state?.route?.query?.current;.

This makes sense to me in the following way:

  • the selector becomes a simple/dumb reflection of the state tree
  • the default {} (or whatever else we choose) becomes the responsibility of the reducer
  • undefined for something that has not been defined yet may carry meaning of its own, and a raw/base selector can reflect that

Of course, trade-offs e.g. a derived state selector will ultimately provide some sort of default, which they'd be responsible for defining anyhow. So the alternative (what you mention I think) might just be better e.g. selectors always defining and returning the default values.

Copy link
Member

@jsnajdr jsnajdr Nov 1, 2021

Choose a reason for hiding this comment

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

Yes, I think the false value for the state doesn't need to be there, in neither of initial, current and previous. It can be an empty object instead:

const EMPTY_QUERY = {};

const initialState = {
  initial: EMPTY_QUERY,
  current: EMPTY_QUERY,
  previous: EMPTY_QUERY,
};

And the reducer for initial can do a check like:

initial = state.initial === EMPTY_QUERY ? action.query : state.initial

to set the initial state only on the very first ROUTE_SET. Even if action.query will be an empty object, it will be a different empty object than the initial state.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's perfect. Thanks! We'll take this path.

return state?.route?.query?.current || null;
}

export default getCurrentQueryArguments;
13 changes: 0 additions & 13 deletions client/state/selectors/get-current-route.js

This file was deleted.

25 changes: 25 additions & 0 deletions client/state/selectors/get-current-route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/* eslint-disable jsdoc/require-param */
/**
* Internal dependencies
*/
import 'calypso/state/route/init';

/**
* Type dependencies
*/
import type { IAppState } from 'calypso/state/types';

/**
* Gets the last route set by a ROUTE_SET action
*
* TODO: We would be better off not conditioning with null here.
* Kept for backwards compatibility in case any null checks.
* Investigate and refactor.
*/
export function getCurrentRoute(
state: IAppState
): IAppState[ 'route' ][ 'path' ][ 'current' ] | null {
return state?.route?.path?.current || null;
}

export default getCurrentRoute;
2 changes: 2 additions & 0 deletions client/state/types.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { DefaultRootState } from 'react-redux';
import { IMarketplaceState } from 'calypso/state/marketplace/types';
import { IPluginsState } from 'calypso/state/plugins/reducer';
import { RouteState } from 'calypso/state/route/types';

/**
* This global app state is currently incomplete and should be completed as each state slice becomes typed
* */
export interface IAppState extends DefaultRootState {
plugins: IPluginsState;
marketplace: IMarketplaceState;
route: RouteState;
}