Remove `useSelectShippingRate` hook and adjust how local state works for `PackageRates` (https://github.com/woocommerce/woocommerce-blocks/pull/5802)

* Convert radio component to TS and support uncontrolled components

* Further radio control to typescript changes

* Combine useSelectShippingRate and useSelectShippingRates

* Remove useSelectShippingRate hook

* Move local radio checked state to PackageRates

* This is a Controlled component - update inline docs

* useSelectShippingRates -> useSelectShippingRate rename
This commit is contained in:
Mike Jolley 2022-02-10 11:59:43 +00:00 committed by GitHub
parent 051bb64138
commit 0a8fe0a0c6
11 changed files with 182 additions and 186 deletions

View File

@ -57,7 +57,7 @@ interface PackageProps {
export const ShippingRatesControlPackage = ( {
packageId,
className,
className = '',
noResultsMessage,
renderOption,
packageData,
@ -65,10 +65,7 @@ export const ShippingRatesControlPackage = ( {
collapse = false,
showItems = false,
}: PackageProps ): ReactElement => {
const { selectShippingRate, selectedShippingRate } = useSelectShippingRate(
packageId,
packageData.shipping_rates
);
const { selectShippingRate } = useSelectShippingRate();
const header = (
<>
@ -117,8 +114,12 @@ export const ShippingRatesControlPackage = ( {
className={ className }
noResultsMessage={ noResultsMessage }
rates={ packageData.shipping_rates }
onSelectRate={ selectShippingRate }
selected={ selectedShippingRate }
onSelectRate={ ( newShippingRateId ) =>
selectShippingRate( newShippingRateId, packageId )
}
selectedRate={ packageData.shipping_rates.find(
( rate ) => rate.selected
) }
renderOption={ renderOption }
/>
);

View File

@ -1,11 +1,11 @@
/**
* External dependencies
*/
import { useState, useEffect } from '@wordpress/element';
import RadioControl, {
RadioControlOptionLayout,
} from '@woocommerce/base-components/radio-control';
import type { PackageRateOption } from '@woocommerce/type-defs/shipping';
import type { ReactElement } from 'react';
import type { CartShippingPackageShippingRate } from '@woocommerce/type-defs/cart';
/**
@ -20,18 +20,30 @@ interface PackageRates {
option: CartShippingPackageShippingRate
) => PackageRateOption;
className?: string;
noResultsMessage: ReactElement;
selected?: string;
noResultsMessage: JSX.Element;
selectedRate: CartShippingPackageShippingRate | undefined;
}
const PackageRates = ( {
className,
className = '',
noResultsMessage,
onSelectRate,
rates,
renderOption = renderPackageRateOption,
selected,
}: PackageRates ): ReactElement => {
selectedRate,
}: PackageRates ): JSX.Element => {
const selectedRateId = selectedRate?.rate_id || '';
// Store selected rate ID in local state so shipping rates changes are shown in the UI instantly.
const [ selectedOption, setSelectedOption ] = useState( selectedRateId );
// Update the selected option if cart state changes in the data stores.
useEffect( () => {
if ( selectedRateId ) {
setSelectedOption( selectedRateId );
}
}, [ selectedRateId ] );
if ( rates.length === 0 ) {
return noResultsMessage;
}
@ -40,10 +52,11 @@ const PackageRates = ( {
return (
<RadioControl
className={ className }
onChange={ ( selectedRateId: string ) => {
onSelectRate( selectedRateId );
onChange={ ( value: string ) => {
setSelectedOption( value );
onSelectRate( value );
} }
selected={ selected }
selected={ selectedOption }
options={ rates.map( renderOption ) }
/>
);

View File

@ -1,52 +0,0 @@
/**
* External dependencies
*/
import classnames from 'classnames';
import { withInstanceId } from '@wordpress/compose';
/**
* Internal dependencies
*/
import RadioControlOption from './option';
import './style.scss';
const RadioControl = ( {
className = '',
instanceId,
id,
selected,
onChange = () => {},
options = [],
} ) => {
const radioControlId = id || instanceId;
return (
options.length && (
<div
className={ classnames(
'wc-block-components-radio-control',
className
) }
>
{ options.map( ( option ) => (
<RadioControlOption
key={ `${ radioControlId }-${ option.value }` }
name={ `radio-control-${ radioControlId }` }
checked={ option.value === selected }
option={ option }
onChange={ ( value ) => {
onChange( value );
if ( typeof option.onChange === 'function' ) {
option.onChange( value );
}
} }
/>
) ) }
</div>
)
);
};
export default withInstanceId( RadioControl );
export { RadioControlOption };
export { default as RadioControlOptionLayout } from './option-layout';

View File

@ -0,0 +1,55 @@
/**
* External dependencies
*/
import classnames from 'classnames';
import { useInstanceId } from '@wordpress/compose';
/**
* Internal dependencies
*/
import RadioControlOption from './option';
import type { RadioControlProps } from './types';
import './style.scss';
const RadioControl = ( {
className = '',
id,
selected,
onChange = () => void 0,
options = [],
}: RadioControlProps ): JSX.Element | null => {
const instanceId = useInstanceId( RadioControl );
const radioControlId = id || instanceId;
if ( ! options.length ) {
return null;
}
return (
<div
className={ classnames(
'wc-block-components-radio-control',
className
) }
>
{ options.map( ( option ) => (
<RadioControlOption
key={ `${ radioControlId }-${ option.value }` }
name={ `radio-control-${ radioControlId }` }
checked={ option.value === selected }
option={ option }
onChange={ ( value: string ) => {
onChange( value );
if ( typeof option.onChange === 'function' ) {
option.onChange( value );
}
} }
/>
) ) }
</div>
);
};
export default RadioControl;
export { default as RadioControlOption } from './option';
export { default as RadioControlOptionLayout } from './option-layout';

View File

@ -1,8 +1,7 @@
/**
* External dependencies
* Internal dependencies
*/
import type { ReactElement } from 'react';
import type { PackageRateOption } from '@woocommerce/type-defs/shipping';
import type { RadioControlOptionLayout } from './types';
const OptionLayout = ( {
label,
@ -10,7 +9,7 @@ const OptionLayout = ( {
description,
secondaryDescription,
id,
}: Partial< PackageRateOption > ): ReactElement => {
}: RadioControlOptionLayout ): JSX.Element => {
return (
<div className="wc-block-components-radio-control__option-layout">
<div className="wc-block-components-radio-control__label-group">

View File

@ -7,8 +7,14 @@ import classnames from 'classnames';
* Internal dependencies
*/
import OptionLayout from './option-layout';
import type { RadioControlOptionProps } from './types';
const Option = ( { checked, name, onChange, option } ) => {
const Option = ( {
checked,
name,
onChange,
option,
}: RadioControlOptionProps ): JSX.Element => {
const {
value,
label,
@ -16,7 +22,8 @@ const Option = ( { checked, name, onChange, option } ) => {
secondaryLabel,
secondaryDescription,
} = option;
const onChangeValue = ( event ) => onChange( event.target.value );
const onChangeValue = ( event: React.ChangeEvent< HTMLInputElement > ) =>
onChange( event.target.value );
return (
<label

View File

@ -0,0 +1,40 @@
/**
* External dependencies
*/
import type { ReactElement } from 'react';
export interface RadioControlProps {
// Class name for control.
className?: string;
// ID for the control.
id?: string;
// The selected option. This is a controlled component.
selected: string;
// Fired when an option is changed.
onChange: ( value: string ) => void;
// List of radio control options.
options: RadioControlOption[];
}
export interface RadioControlOptionProps {
checked: boolean;
name?: string;
onChange: ( value: string ) => void;
option: RadioControlOption;
}
interface RadioControlOptionContent {
label: string;
description?: string | ReactElement | undefined;
secondaryLabel?: string | ReactElement | undefined;
secondaryDescription?: string | undefined;
}
export interface RadioControlOption extends RadioControlOptionContent {
value: string;
onChange?: ( value: string ) => void;
}
export interface RadioControlOptionLayout extends RadioControlOptionContent {
id?: string;
}

View File

@ -1,2 +1 @@
export * from './use-select-shipping-rate';
export * from './use-select-shipping-rates';

View File

@ -1,81 +1,70 @@
/**
* External dependencies
*/
import { useState, useEffect, useRef, useCallback } from '@wordpress/element';
import isShallowEqual from '@wordpress/is-shallow-equal';
import { CartShippingPackageShippingRate } from '@woocommerce/type-defs/cart';
import { useDispatch, useSelect } from '@wordpress/data';
import { useCallback } from '@wordpress/element';
import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
import { useThrowError } from '@woocommerce/base-hooks';
/**
* Internal dependencies
*/
import { useSelectShippingRates } from './use-select-shipping-rates';
import { useStoreEvents } from '../use-store-events';
/**
* Selected rates are derived by looping over the shipping rates.
* This is a custom hook for selecting shipping rates for a shipping package.
*
* @param {Array} shippingRates Array of shipping rates.
* @return {string} Selected rate id.
*/
// This will find the selected rate ID in an array of shipping rates.
const deriveSelectedRateId = (
shippingRates: CartShippingPackageShippingRate[]
) => shippingRates.find( ( rate ) => rate.selected )?.rate_id;
/**
* This is a custom hook for tracking selected shipping rates for a package and selecting a rate. State is used so
* changes are reflected in the UI instantly.
*
* @param {string} packageId Package ID to select rates for.
* @param {Array} shippingRates an array of packages with shipping rates.
* @return {Object} This hook will return an object with these properties:
* - selectShippingRate: A function that immediately returns the selected rate and dispatches an action generator.
* - selectedShippingRate: The selected rate id.
* - isSelectingRate: True when rates are being resolved to the API.
*/
export const useSelectShippingRate = (
packageId: string | number,
shippingRates: CartShippingPackageShippingRate[]
): {
selectShippingRate: ( newShippingRateId: string ) => unknown;
selectedShippingRate: string | undefined;
export const useSelectShippingRate = (): {
// Returns a function that accepts a shipping rate ID and a package ID.
selectShippingRate: (
newShippingRateId: string,
packageId: string | number
) => unknown;
// True when a rate is currently being selected and persisted to the server.
isSelectingRate: boolean;
} => {
const throwError = useThrowError();
const { dispatchCheckoutEvent } = useStoreEvents();
// Rates are selected via the shipping data context provider.
const { selectShippingRate, isSelectingRate } = useSelectShippingRates();
const { selectShippingRate: dispatchSelectShippingRate } = ( useDispatch(
storeKey
) as {
selectShippingRate: unknown;
} ) as {
selectShippingRate: (
newShippingRateId: string,
packageId: string | number
) => Promise< unknown >;
};
// Selected rates are stored in state. This allows shipping rates changes to be shown in the UI instantly.
// Defaults to the currently selected rate_id.
const [ selectedShippingRate, setSelectedShippingRate ] = useState( () =>
deriveSelectedRateId( shippingRates )
);
// This ref is used to track when changes come in via the props. When the incoming shipping rates change, update our local state if there are changes to selected methods.
const currentShippingRates = useRef( shippingRates );
useEffect( () => {
if ( ! isShallowEqual( currentShippingRates.current, shippingRates ) ) {
currentShippingRates.current = shippingRates;
setSelectedShippingRate( deriveSelectedRateId( shippingRates ) );
}
}, [ shippingRates ] );
// Sets a rate for a package in state (so changes are shown right away to consumers of the hook) and in the stores.
const setPackageRateId = useCallback(
( newShippingRateId ) => {
setSelectedShippingRate( newShippingRateId );
selectShippingRate( newShippingRateId, packageId );
// Selects a shipping rate, fires an event, and catch any errors.
const selectShippingRate = useCallback(
( newShippingRateId, packageId ) => {
dispatchSelectShippingRate( newShippingRateId, packageId )
.then( () => {
dispatchCheckoutEvent( 'set-selected-shipping-rate', {
shippingRateId: newShippingRateId,
} );
} )
.catch( ( error ) => {
// Throw an error because an error when selecting a rate is problematic.
throwError( error );
} );
},
[ packageId, selectShippingRate, dispatchCheckoutEvent ]
[ dispatchSelectShippingRate, dispatchCheckoutEvent, throwError ]
);
// See if rates are being selected.
const isSelectingRate = useSelect< boolean >( ( select ) => {
return select( storeKey ).isShippingRateBeingSelected();
}, [] );
return {
selectShippingRate: setPackageRateId,
selectedShippingRate,
selectShippingRate,
isSelectingRate,
};
};

View File

@ -1,55 +0,0 @@
/**
* External dependencies
*/
import { useDispatch, useSelect } from '@wordpress/data';
import { useCallback } from '@wordpress/element';
import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
import { useThrowError } from '@woocommerce/base-hooks';
/**
* This is a custom hook for selecting shipping rates
*
* @return {Object} This hook will return an object with these properties:
* - selectShippingRate: A function that immediately returns the selected rate and dispatches an action generator.
* - isSelectingRate: True when rates are being resolved to the API.
*/
export const useSelectShippingRates = (): {
selectShippingRate: (
newShippingRateId: string,
packageId: string | number
) => unknown;
isSelectingRate: boolean;
} => {
const throwError = useThrowError();
const { selectShippingRate } = ( useDispatch( storeKey ) as {
selectShippingRate: unknown;
} ) as {
selectShippingRate: (
newShippingRateId: string,
packageId: string | number
) => Promise< unknown >;
};
// Sets a rate for a package in state (so changes are shown right away to consumers of the hook) and in the stores.
const setRate = useCallback(
( newShippingRateId, packageId ) => {
selectShippingRate( newShippingRateId, packageId ).catch(
( error ) => {
// we throw this error because an error on selecting a rate is problematic.
throwError( error );
}
);
},
[ throwError, selectShippingRate ]
);
// See if rates are being selected.
const isSelectingRate = useSelect< boolean >( ( select ) => {
return select( storeKey ).isShippingRateBeingSelected();
}, [] );
return {
selectShippingRate: setRate,
isSelectingRate,
};
};

View File

@ -27,7 +27,7 @@ import {
import { useCheckoutContext } from '../checkout-state';
import { useCustomerDataContext } from '../customer';
import { useStoreCart } from '../../../hooks/cart/use-store-cart';
import { useSelectShippingRates } from '../../../hooks/shipping/use-select-shipping-rates';
import { useSelectShippingRate } from '../../../hooks/shipping/use-select-shipping-rate';
/**
* @typedef {import('@woocommerce/type-defs/contexts').ShippingDataContext} ShippingDataContext
@ -60,7 +60,7 @@ export const ShippingDataProvider = ( { children } ) => {
shippingRatesLoading,
cartErrors,
} = useStoreCart();
const { selectShippingRate, isSelectingRate } = useSelectShippingRates();
const { selectShippingRate, isSelectingRate } = useSelectShippingRate();
const [ shippingErrorStatus, dispatchErrorStatus ] = useReducer(
errorStatusReducer,
NONE