Skip to content

Commit

Permalink
Simplify hooks.
Browse files Browse the repository at this point in the history
  • Loading branch information
asvinb committed Sep 24, 2024
1 parent 716581b commit 2c4f863
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 105 deletions.
34 changes: 28 additions & 6 deletions js/src/components/paid-ads/campaign-assets-form.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
import { useState, useMemo } from '@wordpress/element';
import { useState, useMemo, useEffect, useRef } from '@wordpress/element';
import { noop } from 'lodash';

/**
Expand Down Expand Up @@ -74,21 +74,42 @@ export default function CampaignAssetsForm( {
onChange = noop,
...adaptiveFormProps
} ) {
const formRef = useRef();
const initialAssetGroup = useMemo( () => {
return convertAssetEntityGroupToFormValues( assetEntityGroup );
}, [ assetEntityGroup ] );

const [ baseAssetGroup, setBaseAssetGroup ] = useState( initialAssetGroup );
const [ hasImportedAssets, setHasImportedAssets ] = useState( false );
const [ countryCodes, setCountryCodes ] = useState( [] );
const { validateCampaignWithCountryCodes } =
useValidateCampaignWithCountryCodes( countryCodes );
const {
validateCampaignWithCountryCodes,
dailyBudget,
refreshCountryCodes,
currencyCode,
} = useValidateCampaignWithCountryCodes();

// Grab the recommendations for the initial country codes.
useEffect( () => {
refreshCountryCodes( initialCampaign.countryCodes );
}, [ initialCampaign.countryCodes, refreshCountryCodes ] );

const handleOnChange = ( _, values, arg ) => {
setCountryCodes( values.countryCodes );
onChange( _, values, arg );

// Whenever there's a change, update the country codes in the validation function.
refreshCountryCodes( values.countryCodes );
};

useEffect( () => {
const { setValue } = formRef.current;

// Trigger a form value change to refresh the validation function once again with the new budget values
// If the validation function and values do not change, then the validation will not be triggerred since the `validate`
// function uses useCallback and will not be re-created.
setValue( 'dailyBudget', dailyBudget );
// Sometimes the currency code takes time to resolve and the budget data is already available.
setValue( 'currencyCode', currencyCode );
}, [ dailyBudget, currencyCode ] );

const extendAdapter = ( formContext ) => {
const assetGroupErrors = validateAssetGroup( formContext.values );
const finalUrl = assetEntityGroup?.[ ASSET_GROUP_KEY.FINAL_URL ];
Expand Down Expand Up @@ -127,6 +148,7 @@ export default function CampaignAssetsForm( {

return (
<AdaptiveForm
ref={ formRef }
initialValues={ {
...initialCampaign,
...initialAssetGroup,
Expand Down
2 changes: 1 addition & 1 deletion js/src/components/paid-ads/validateCampaign.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const validateCampaign = ( values, opts ) => {

if (
Number.isFinite( values.amount ) &&
Number.isFinite( opts.dailyBudget )
Number.isFinite( opts?.dailyBudget )
) {
const { amount } = values;
const { dailyBudget, formatAmount } = opts;
Expand Down
53 changes: 0 additions & 53 deletions js/src/hooks/useHighestBudgetRecommendation.js

This file was deleted.

87 changes: 67 additions & 20 deletions js/src/hooks/useValidateCampaignWithCountryCodes.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
/**
* External dependencies
*/
import { useSelect } from '@wordpress/data';
import { useState, useEffect } from '@wordpress/element';

/**
* Internal dependencies
*/
import useHighestBudgetRecommendation from './useHighestBudgetRecommendation';
import { STORE_KEY } from '.~/data/constants';
import useAdsCurrency from './useAdsCurrency';
import validateCampaign from '.~/components/paid-ads/validateCampaign';
import getHighestBudget from '.~/utils/getHighestBudget';

/**
* @typedef {import('.~/components/types.js').CampaignFormValues} CampaignFormValues
Expand All @@ -21,27 +29,66 @@ import validateCampaign from '.~/components/paid-ads/validateCampaign';
/**
* Validate campaign form. Accepts the form values object and returns errors object.
*
* @param {Array<CountryCode>} [countryCodes] Country code array. If not provided, the validate function will not take into account budget recommendations.
* @param {Array<CountryCode>} [initialCountryCodes] Country code array. If not provided, the validate function will not take into account budget recommendations.
* @return {ValidateCampaignWithCountryCodesHook} An object containing the `validateCampaignWithCountryCodes` function and a `loading` state.
*/
const useValidateCampaignWithCountryCodes = ( countryCodes ) => {
const { dailyBudget, formatAmount, hasFinishedResolution } =
useHighestBudgetRecommendation( countryCodes );

/**
* Validate campaign form. Accepts the form values object and returns errors object.
*
* @param {CampaignFormValues} values Campaign form values.
* @return {Object} An object containing any validation errors. If no errors, the object will be empty.
*/
const validateCampaignWithCountryCodes = ( values ) => {
return validateCampaign( values, {
dailyBudget,
formatAmount,
} );
};

return { validateCampaignWithCountryCodes, hasFinishedResolution };
const useValidateCampaignWithCountryCodes = ( initialCountryCodes ) => {
const {
formatAmount,
adsCurrencyConfig: { code },
} = useAdsCurrency();
const [ countryCodes, setCountryCodes ] = useState( [] );

useEffect( () => {
setCountryCodes( initialCountryCodes );
}, [ initialCountryCodes ] );

return useSelect(
( select ) => {
// If no country codes are provided, return the default validateCampaign function.
if ( ! countryCodes?.length ) {
return {
validateCampaignWithCountryCodes: validateCampaign,
dailyBudget: null,
formatAmount,
setCountryCodes,
currencyCode: code,
hasFinishedResolution: true,
};
}

const { getAdsBudgetRecommendations, hasFinishedResolution } =
select( STORE_KEY );
const budgetData = getAdsBudgetRecommendations( countryCodes );
const budget = getHighestBudget( budgetData?.recommendations );

/**
* Validate campaign form. Accepts the form values object and returns errors object.
*
* @param {CampaignFormValues} values Campaign form values.
* @return {Object} An object containing any validation errors. If no errors, the object will be empty.
*/
const validateCampaignWithCountryCodes = ( values ) => {
return validateCampaign( values, {
dailyBudget: budget?.daily_budget,
formatAmount,
} );
};

return {
validateCampaignWithCountryCodes,
dailyBudget: budget?.daily_budget,
formatAmount,
refreshCountryCodes: setCountryCodes,
currencyCode: code,
hasFinishedResolution:
hasFinishedResolution( 'getAdsBudgetRecommendations', [
countryCodes,
] ) && code,
};
},
[ countryCodes, formatAmount, code ]
);
};

export default useValidateCampaignWithCountryCodes;
Original file line number Diff line number Diff line change
Expand Up @@ -45,38 +45,19 @@ export default function PaidAdsSetupSections( {
onStatesReceived,
countryCodes,
} ) {
const {
hasFinishedResolution: hasResolvedValidateCampaignWithCountryCodes,
validateCampaignWithCountryCodes,
} = useValidateCampaignWithCountryCodes( countryCodes );
const { validateCampaignWithCountryCodes, hasFinishedResolution } =
useValidateCampaignWithCountryCodes( countryCodes );
const { billingStatus } = useGoogleAdsAccountBillingStatus();

const onStatesReceivedRef = useRef();
onStatesReceivedRef.current = onStatesReceived;

/**
* Resolve the initial paid ads data from the given paid ads data.
* Parts of the resolved data are used in the `initialValues` prop of `Form` component.
*
* @param {PaidAdsData} paidAds The paid ads data as the base to be resolved with other states.
* @return {PaidAdsData} The resolved paid ads data.
*/
function resolveInitialPaidAds( paidAds ) {
const nextPaidAds = { ...paidAds };
nextPaidAds.isValid = ! Object.keys(
validateCampaignWithCountryCodes( nextPaidAds )
).length;

return nextPaidAds;
}

const [ paidAds, setPaidAds ] = useState( () => {
// Resolve the starting paid ads data with the campaign data stored in the client session.
const startingPaidAds = {
...defaultPaidAds,
...clientSession.getCampaign(),
};
return resolveInitialPaidAds( startingPaidAds );
return startingPaidAds;
} );

const isBillingCompleted =
Expand All @@ -97,15 +78,20 @@ export default function PaidAdsSetupSections( {
For example, refresh page during onboarding flow after the billing setup is finished.
*/
useEffect( () => {
const isValid = ! Object.keys(
validateCampaignWithCountryCodes( paidAds )
).length;
const nextPaidAds = {
...paidAds,
isReady: paidAds.isValid && isBillingCompleted,
isValid,
isReady: isValid && isBillingCompleted,
};

onStatesReceivedRef.current( nextPaidAds );
clientSession.setCampaign( nextPaidAds );
}, [ paidAds, isBillingCompleted ] );
}, [ paidAds, isBillingCompleted, validateCampaignWithCountryCodes ] );

if ( ! billingStatus || ! hasResolvedValidateCampaignWithCountryCodes ) {
if ( ! billingStatus || ! hasFinishedResolution ) {
return (
<Section>
<SpinnerCard />
Expand Down

0 comments on commit 2c4f863

Please sign in to comment.