-
Notifications
You must be signed in to change notification settings - Fork 2k
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
base: trunk
Are you sure you want to change the base?
Conversation
Link to Calypso live: https://calypso.live?image=registry.a8c.com/calypso/app:build-17844 |
Here is how your PR affects size of JS and CSS bundles shipped to the user's browser: App Entrypoints (~28 bytes added 📈 [gzipped])
Common code that is always downloaded and parsed every time the app is loaded, no matter which route is used. Sections (~70 bytes added 📈 [gzipped])
Sections contain code specific for a given set of routes. Is downloaded and parsed only when a particular route is navigated to. Async-loaded Components (~36 bytes added 📈 [gzipped])
React components that are loaded lazily, when a certain part of UI is displayed for the first time. Legend What is parsed and gzip size?Parsed Size: Uncompressed size of the JS and CSS files. This much code needs to be parsed and stored in memory. Generated by performance advisor bot at iscalypsofastyet.com. |
export type Query = { | ||
[ key: string ]: string | number | undefined; | ||
_timestamp?: number; | ||
}; |
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.
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
.
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.
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 Query = { | ||
[ key: string ]: string | number | undefined; | ||
_timestamp?: number; | ||
}; |
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.
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.
client/state/route/types.ts
Outdated
|
||
export type RouteSetActionCreator = ( | ||
path: RouteSetAction[ 'path' ], | ||
query: RouteSetAction[ 'query' ] |
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.
The types here are Path
and Query
, there's no need to specify them in a verbose way like this.
client/state/route/query/reducer.ts
Outdated
const currentReducer = ( state: QueryState[ 'current' ], query: Query ) => | ||
! isEqualQuery( state, query ) ? timestamped( query ) : state; | ||
|
||
const initialState = { |
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.
const initialState: QueryState = {
initial: false,
...
}
is better. It avoids the need to write as const
.
client/state/route/types.ts
Outdated
previous: Query | false; | ||
}; | ||
|
||
export type Path = string; |
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.
Type aliases like this are not very useful in TypeScript, where the type checking is structural rather than nominal. Simply using string
everywhere is OK.
|
||
export type RouteState = { | ||
path: PathState; | ||
query: QueryState; |
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 is also a lastNonEditorRoute: string
field in RouteState
*/ | ||
export function getCurrentQueryArguments( | ||
state: IAppState | ||
): IAppState[ 'route' ][ 'query' ][ 'current' ] | null { |
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.
The return type is Query
, so why not use it instead?
/** | ||
* Internal dependencies | ||
*/ | ||
import { ROUTE_CLEAR_LAST_NON_EDITOR, ROUTE_SET } from 'calypso/state/action-types'; |
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 PR adds a reducer.ts
file, but doesn't delete the old reducer.js
one.
client/state/route/types.ts
Outdated
path: Path; | ||
query: Query; | ||
type: typeof ROUTE_SET; | ||
}; |
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 that diligently declaring strict types for the Route*
actions is not necessary. The types are not actually used anywhere.
The setRoute
action creator (like any other action creator) returns an opaque action object whose internal structure is not interesting. The action is only dispatched by passing it to dispatch
and its fields could as well be private.
It's only the reducer that actually reads them, but our reducer is declared as accepting AnyAction
and doesn't use the specific types.
Because of that, the actions creators could simply return the Action
type from redux
.
2745aab
to
9faa750
Compare
getCurrentQueryArguments was used, accessing email_address, without checking if it was null
Removed unused types
@@ -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 ); |
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 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.
* Kept for backwards compatibility in case any null checks. | ||
* Investigate and refactor. | ||
*/ | ||
export function getCurrentQueryArguments( state: IAppState ): Query | false | null { |
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.
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.
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 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.
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.
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.
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.
That's perfect. Thanks! We'll take this path.
Changes proposed in this Pull Request
Adds types to the route path and query state actions/reducers/selectors.
I guess more typing can't really hurt. This will also help our work in #55313 with better types coming from the underlying path & query selectors. Hopefully, put some of the groundwork here.
Testing instructions
Related to #55313, #57109