Refactor "Use your domain" to functional component#101854
Conversation
Jetpack Cloud live (direct link)
Automattic for Agencies live (direct link)
|
|
Here is how your PR affects size of JS and CSS bundles shipped to the user's browser: Sections (~205 bytes removed 📉 [gzipped]) DetailsSections contain code specific for a given set of routes. Is downloaded and parsed only when a particular route is navigated to. 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. |
|
This PR modifies the release build for the following Calypso Apps: For info about this notification, see here: PCYsg-OT6-p2
To test WordPress.com changes, run |
e63f690 to
3fd2fd8
Compare
| static propTypes = { | ||
| analyticsSection: PropTypes.string.isRequired, | ||
| basePath: PropTypes.string, | ||
| cart: PropTypes.object, | ||
| domainsWithPlansOnly: PropTypes.bool, | ||
| primaryWithPlansOnly: PropTypes.bool, | ||
| goBack: PropTypes.func, | ||
| initialQuery: PropTypes.string, | ||
| isSignupStep: PropTypes.bool, | ||
| mapDomainUrl: PropTypes.string, | ||
| transferDomainUrl: PropTypes.string, | ||
| onRegisterDomain: PropTypes.func, | ||
| onTransferDomain: PropTypes.func, | ||
| onSave: PropTypes.func, | ||
| selectedSite: PropTypes.oneOfType( [ PropTypes.object, PropTypes.bool ] ), | ||
| forcePrecheck: PropTypes.bool, | ||
| }; |
There was a problem hiding this comment.
I was able to remove a lot of props compared to the previous implementation:
- some of them came from
connecting the component to the Redux Global state, and were replaced by hooks; - some of them are actually not currently used, and I guess were not cleaned up over time
There was a problem hiding this comment.
In the Git history I see big refactorings (#25573 is an example) where large chunks of code were moved around between use-your-domain-step, register-domain-step, transfer-domain-step etc. That explains how many props and state fields got left behind unused.
| getDefaultState() { | ||
| return { | ||
| authCodeValid: null, | ||
| domain: null, | ||
| domainsWithPlansOnly: false, | ||
| inboundTransferStatus: {}, | ||
| precheck: get( this.props, 'forcePrecheck', false ), | ||
| searchQuery: this.props.initialQuery || '', | ||
| submittingAuthCodeCheck: false, | ||
| submittingAvailability: false, | ||
| submittingWhois: get( this.props, 'forcePrecheck', false ), | ||
| supportsPrivacy: false, | ||
| }; | ||
| } |
There was a problem hiding this comment.
Most of the old "state" wasn't actually used — in fact, setState is never called in this component. The only state usage that I kept in the new version is for storing the initial value of a prop.
| domainsWithPlansOnlyButNoPlan | ||
| ) { | ||
| transferSalePriceText = translate( 'Sale price is %(cost)s', { | ||
| args: { cost: domainProductSalePrice! }, |
There was a problem hiding this comment.
Added a ! because isEmpty is not acting as a type guard for domainProductSalePrice
There was a problem hiding this comment.
You can remove the isEmpty check (it's lodash, so it's unwanted anyway) and replace it with domainProductSalePrice !== null.
| // @ts-expect-error despite the TS error, formatCurrency works with a | ||
| // `null` currencyCode and uses a fallback currency. | ||
| mappingPriceText = formatCurrency( price, currencyCode ); |
There was a problem hiding this comment.
I didn't want to work on formatCurrency to prevent scope bloat, but I get the sense, from the number of warnings that I see in the browser console, that formatCurrency is often invoked with a null or undefined currency code. I wonder if we should change it so that it handles null or undefined internally
There was a problem hiding this comment.
This TS error can be fixed by changing the if ( price ) condition to if ( price && currencyCode ). The currencyCode Redux state is initialized when the products list is finished fetching (triggered by QueryProducts).
There was a problem hiding this comment.
I wonder if we should change it so that it handles null or undefined internally
I think this might be hard to do. How do you format a price if you don't know the currency? Better to not render a price at all if you don't know how but what to render in its place? That's very context specific and probably should be in the hands of the caller.
This TS error can be fixed by changing the if ( price ) condition to if ( price && currencyCode )
I think this is the right solution.
There was a problem hiding this comment.
Thank you both for the advice — I'll go ahead and implement the suggested solution, even if it introduces a runtime change.
Better to not render a price at all if you don't know how but what to render in its place?
That's a good point, but one that I see ignored repeatedly across the codebase (judging by the amount of browser console warnings) 😞
I guess that the fact that formatCurrency still picks a default currency also doesn't help.
There was a problem hiding this comment.
One thing that could improve the formatCurrency situation is fixing the getDomainPrice function: it should return null if the currencyCode is not valid.
There are just a handful of usages, and the function already returns null when there is no valid "price" value, so hopefully this would be an easy refactoring.
| className="use-your-domain-step__option-button" | ||
| primary={ isPrimary } | ||
| onClick={ onClick } | ||
| busy={ submitting } |
There was a problem hiding this comment.
I removed the busy prop since, in the current implementation, it was always computing as false
|
|
||
| export default connect( | ||
| ( state ) => ( { | ||
| currentUser: getCurrentUser( state ), |
There was a problem hiding this comment.
Removed in the new version as it was not used in code
| productsList: getProductsList( state ), | ||
| } ), | ||
| { | ||
| errorNotice, |
There was a problem hiding this comment.
Also removed as I couldn't see any error notice being dispatched in the code
3fd2fd8 to
f08ef0f
Compare
| recordTransferButtonClickInUseYourDomain, | ||
| recordMappingButtonClickInUseYourDomain, |
There was a problem hiding this comment.
Moved to inline dispatch calls
| recordTransferButtonClickInUseYourDomain, | ||
| recordMappingButtonClickInUseYourDomain, | ||
| } | ||
| )( withCartKey( withShoppingCart( localize( UseYourDomainStep ) ) ) ); |
There was a problem hiding this comment.
localize=>useTranslate()withShoppingCart=>useShoppingCart()withCartKey=>useCartKey()
| { this.renderSelectTransfer() } | ||
| { this.renderSelectMapping() } |
There was a problem hiding this comment.
Refactored these two render functions (and the underlying renderOptionContent) to an internal UseYourDomainStepContent component
| ! isEmpty( domainProductSalePrice ) && | ||
| ! isNextDomainFree( cart ) && | ||
| ! isDomainBundledWithPlan( cart, searchQuery ) && |
There was a problem hiding this comment.
Inverted the boolean check since we can't early return in the render function.
| if ( domainProductPrice ) { | ||
| if ( | ||
| isEmpty( domainProductSalePrice ) || | ||
| isNextDomainFree( cart ) || | ||
| isDomainBundledWithPlan( cart, searchQuery ) || | ||
| domainsWithPlansOnlyButNoPlan | ||
| ) { | ||
| return; | ||
| } | ||
|
|
||
| return translate( 'Sale price is %(cost)s', { args: { cost: domainProductSalePrice } } ); | ||
| }; | ||
|
|
||
| getTransferPriceText = () => { | ||
| const { | ||
| cart, | ||
| currencyCode, | ||
| translate, | ||
| domainsWithPlansOnly, | ||
| isSignupStep, | ||
| productsList, | ||
| selectedSite, | ||
| } = this.props; | ||
| const { searchQuery } = this.state; | ||
| const productSlug = getDomainProductSlug( searchQuery ); | ||
| const domainsWithPlansOnlyButNoPlan = | ||
| domainsWithPlansOnly && ( ( selectedSite && ! isPlan( selectedSite.plan ) ) || isSignupStep ); | ||
|
|
||
| const domainProductPrice = getDomainPrice( productSlug, productsList, currencyCode ); | ||
|
|
||
| if ( | ||
| domainProductPrice && | ||
| ( isNextDomainFree( cart ) || | ||
| isDomainBundledWithPlan( cart, searchQuery ) || | ||
| domainsWithPlansOnlyButNoPlan || | ||
| getDomainTransferSalePrice( productSlug, productsList, currencyCode ) ) | ||
| domainsWithPlansOnlyButNoPlan || | ||
| getDomainTransferSalePrice( productSlug, productsList, currencyCode ) | ||
| ) { | ||
| return translate( 'Renews at %(cost)s', { args: { cost: domainProductPrice } } ); | ||
| } | ||
|
|
||
| if ( domainProductPrice ) { | ||
| return translate( '%(cost)s per year', { args: { cost: domainProductPrice } } ); | ||
| } | ||
| }; | ||
|
|
||
| getMappingPriceText = () => { | ||
| const { | ||
| cart, | ||
| currencyCode, | ||
| domainsWithPlansOnly, | ||
| primaryWithPlansOnly, | ||
| productsList, | ||
| selectedSite, | ||
| translate, | ||
| } = this.props; | ||
| const { searchQuery } = this.state; | ||
|
|
||
| let mappingProductPrice; | ||
|
|
||
| const price = get( productsList, [ 'domain_map', 'cost' ], null ); | ||
| if ( price ) { | ||
| mappingProductPrice = formatCurrency( price, currencyCode ); | ||
| mappingProductPrice = translate( | ||
| '%(cost)s per year plus registration costs at your current provider', | ||
| { args: { cost: mappingProductPrice } } | ||
| ); | ||
| transferPriceText = translate( 'Renews at %(cost)s', { args: { cost: domainProductPrice } } ); | ||
| } else { | ||
| transferPriceText = translate( '%(cost)s per year', { args: { cost: domainProductPrice } } ); |
There was a problem hiding this comment.
Extracted the common domainProductPrice check, and moved the rest of the checks to a nested if...else
| ); | ||
| } | ||
| if ( | ||
| isDomainMappingFree( selectedSite ?? undefined ) || |
There was a problem hiding this comment.
Quick fix since selectedSite can be undefined OR null, but isDomainMappingFree doesn't accept null
There was a problem hiding this comment.
You can add the | null type to the isDomainMappingFree, it's a change that makes sense.
| { reasons.map( ( phrase, index ) => { | ||
| if ( isEmpty( phrase ) ) { | ||
| return; | ||
| } |
There was a problem hiding this comment.
This isEmpty call be eliminated in favor of phrase === null. The value (member of reason array) is always either null or a result of translate().
| } | ||
|
|
||
| let mappingPriceText; | ||
| const price = get( productsList, [ 'domain_map', 'cost' ], null ); |
There was a problem hiding this comment.
This is the only usage of get in this file and can be easily replaced with ?.. Let's completely remove Lodash from here!
eebf1db to
d637096
Compare
| let mappingPriceText; | ||
| const price = productsList?.domain_map?.cost; | ||
| if ( price && currencyCode ) { | ||
| mappingPriceText = formatCurrency( price, currencyCode ); |
There was a problem hiding this comment.
nit: the formatCurrency result should be assigned to its own variable. Like cost.
There was a problem hiding this comment.
Totally agree, it's always hard to draw a line when doing these refactors, as there's much more that I'd like to change 😅
client/lib/cart-values/cart-items.ts
Outdated
| export function isDomainMappingFree( | ||
| selectedSite: undefined | { plan: { product_slug: string } } | ||
| ): boolean { | ||
| export function isDomainMappingFree( selectedSite: undefined | SiteDetails | null ): boolean { |
There was a problem hiding this comment.
nit: the sorting of the | subtypes looks very random. Usually we put the "real" type first:
SiteDetails | null | undefinedThis file seems to use some sort of Yoda convention, not sure why.
d637096 to
8380a7b
Compare
Related to #101833
Proposed Changes
Refactor the React component from class to functional in preparation for the work highlighted in #100243 (comment)
Why are these changes being made?
Refactoring to functional component will allow the usage of the necessary React hooks to complete #100243
Testing Instructions
Visit
/domains/add/use-your-domain/{SITE_SLUG}and make sure everything works as ontrunk.Tip
It's highly recommended to review commit-by-commit
Note
I couldn't find a way to visit this page organically. Who would be the best person / team to understand whether this whole branch of domain transfer steps can be deleted?
Pre-merge Checklist