Skip to content

Refactor "Use your domain" to functional component#101854

Merged
ciampo merged 10 commits intotrunkfrom
refactor/use-your-domain-class-to-functional
Mar 28, 2025
Merged

Refactor "Use your domain" to functional component#101854
ciampo merged 10 commits intotrunkfrom
refactor/use-your-domain-class-to-functional

Conversation

@ciampo
Copy link
Contributor

@ciampo ciampo commented Mar 25, 2025

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 on trunk.

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?

Screenshot 2025-03-25 at 19 14 12

Pre-merge Checklist

  • Has the general commit checklist been followed? (PCYsg-hS-p2)
  • Have you written new tests for your changes?
  • Have you tested the feature in Simple (P9HQHe-k8-p2), Atomic (P9HQHe-jW-p2), and self-hosted Jetpack sites (PCYsg-g6b-p2)?
  • Have you checked for TypeScript, React or other console errors?
  • Have you used memoizing on expensive computations? More info in Memoizing with create-selector and Using memoizing selectors and Our Approach to Data
  • Have we added the "[Status] String Freeze" label as soon as any new strings were ready for translation (p4TIVU-5Jq-p2)?
    • For UI changes, have we tested the change in various languages (for example, ES, PT, FR, or DE)? The length of text and words vary significantly between languages.
  • For changes affecting Jetpack: Have we added the "[Status] Needs Privacy Updates" label if this pull request changes what data or activity we track or use (p4TIVU-aUh-p2)?

@github-actions
Copy link

github-actions bot commented Mar 25, 2025

@matticbot
Copy link
Contributor

matticbot commented Mar 25, 2025

Here is how your PR affects size of JS and CSS bundles shipped to the user's browser:

Sections (~205 bytes removed 📉 [gzipped])

Details
name     parsed_size           gzip_size
domains      -2787 B  (-0.1%)     -205 B  (-0.0%)

Sections 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.
Gzip Size: Compressed size of the JS and CSS files. This much data needs to be downloaded over network.

Generated by performance advisor bot at iscalypsofastyet.com.

@matticbot
Copy link
Contributor

matticbot commented Mar 25, 2025

This PR modifies the release build for the following Calypso Apps:

For info about this notification, see here: PCYsg-OT6-p2

  • help-center
  • notifications
  • wpcom-block-editor

To test WordPress.com changes, run install-plugin.sh $pluginSlug refactor/use-your-domain-class-to-functional on your sandbox.

@ciampo ciampo force-pushed the refactor/use-your-domain-class-to-functional branch 3 times, most recently from e63f690 to 3fd2fd8 Compare March 25, 2025 18:17
@ciampo ciampo changed the title Refactor "Use your domain" component to functional Refactor "Use your domain" to functional component Mar 25, 2025
Comment on lines -48 to -64
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,
};
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 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

Copy link
Member

Choose a reason for hiding this comment

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

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.

Comment on lines -73 to -86
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,
};
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

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! },
Copy link
Contributor Author

@ciampo ciampo Mar 26, 2025

Choose a reason for hiding this comment

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

Added a ! because isEmpty is not acting as a type guard for domainProductSalePrice

Copy link
Member

Choose a reason for hiding this comment

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

You can remove the isEmpty check (it's lodash, so it's unwanted anyway) and replace it with domainProductSalePrice !== null.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

// @ts-expect-error despite the TS error, formatCurrency works with a
// `null` currencyCode and uses a fallback currency.
mappingPriceText = formatCurrency( price, currencyCode );
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 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

Copy link
Member

Choose a reason for hiding this comment

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

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

Copy link
Member

@sirbrillig sirbrillig Mar 26, 2025

Choose a reason for hiding this comment

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

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.

Copy link
Contributor Author

@ciampo ciampo Mar 27, 2025

Choose a reason for hiding this comment

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

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

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 }
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 removed the busy prop since, in the current implementation, it was always computing as false


export default connect(
( state ) => ( {
currentUser: getCurrentUser( 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.

Removed in the new version as it was not used in code

productsList: getProductsList( state ),
} ),
{
errorNotice,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also removed as I couldn't see any error notice being dispatched in the code

@ciampo ciampo force-pushed the refactor/use-your-domain-class-to-functional branch from 3fd2fd8 to f08ef0f Compare March 26, 2025 08:41
Comment on lines 466 to 467
recordTransferButtonClickInUseYourDomain,
recordMappingButtonClickInUseYourDomain,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved to inline dispatch calls

recordTransferButtonClickInUseYourDomain,
recordMappingButtonClickInUseYourDomain,
}
)( withCartKey( withShoppingCart( localize( UseYourDomainStep ) ) ) );
Copy link
Contributor Author

Choose a reason for hiding this comment

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

  • localize => useTranslate()
  • withShoppingCart => useShoppingCart()
  • withCartKey => useCartKey()

Comment on lines 428 to 429
{ this.renderSelectTransfer() }
{ this.renderSelectMapping() }
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Refactored these two render functions (and the underlying renderOptionContent) to an internal UseYourDomainStepContent component

Comment on lines 161 to 153
! isEmpty( domainProductSalePrice ) &&
! isNextDomainFree( cart ) &&
! isDomainBundledWithPlan( cart, searchQuery ) &&
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Inverted the boolean check since we can't early return in the render function.

Comment on lines 173 to 172
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 } } );
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Extracted the common domainProductPrice check, and moved the rest of the checks to a nested if...else

);
}
if (
isDomainMappingFree( selectedSite ?? undefined ) ||
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Quick fix since selectedSite can be undefined OR null, but isDomainMappingFree doesn't accept null

Copy link
Member

Choose a reason for hiding this comment

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

You can add the | null type to the isDomainMappingFree, it's a change that makes sense.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ciampo ciampo self-assigned this Mar 26, 2025
@ciampo ciampo requested review from a team March 26, 2025 09:28
@matticbot matticbot added the [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. label Mar 26, 2025
@ciampo ciampo marked this pull request as ready for review March 26, 2025 09:29
{ reasons.map( ( phrase, index ) => {
if ( isEmpty( phrase ) ) {
return;
}
Copy link
Member

Choose a reason for hiding this comment

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

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().

Copy link
Contributor Author

Choose a reason for hiding this comment

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

}

let mappingPriceText;
const price = get( productsList, [ 'domain_map', 'cost' ], null );
Copy link
Member

Choose a reason for hiding this comment

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

This is the only usage of get in this file and can be easily replaced with ?.. Let's completely remove Lodash from here!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Let's! f52f737

@ciampo ciampo force-pushed the refactor/use-your-domain-class-to-functional branch from eebf1db to d637096 Compare March 27, 2025 17:36
@ciampo ciampo requested review from jsnajdr and sirbrillig March 27, 2025 17:46
Copy link
Member

@jsnajdr jsnajdr left a comment

Choose a reason for hiding this comment

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

I like this 🙂

let mappingPriceText;
const price = productsList?.domain_map?.cost;
if ( price && currencyCode ) {
mappingPriceText = formatCurrency( price, currencyCode );
Copy link
Member

Choose a reason for hiding this comment

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

nit: the formatCurrency result should be assigned to its own variable. Like cost.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

af0065c

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 😅

export function isDomainMappingFree(
selectedSite: undefined | { plan: { product_slug: string } }
): boolean {
export function isDomainMappingFree( selectedSite: undefined | SiteDetails | null ): boolean {
Copy link
Member

Choose a reason for hiding this comment

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

nit: the sorting of the | subtypes looks very random. Usually we put the "real" type first:

SiteDetails | null | undefined

This file seems to use some sort of Yoda convention, not sure why.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yup 8380a7b

@ciampo ciampo force-pushed the refactor/use-your-domain-class-to-functional branch from d637096 to 8380a7b Compare March 28, 2025 11:45
@ciampo ciampo merged commit 2eede8c into trunk Mar 28, 2025
13 checks passed
@ciampo ciampo deleted the refactor/use-your-domain-class-to-functional branch March 28, 2025 12:28
@github-actions github-actions bot removed the [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. label Mar 28, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

Comments