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:
parent
051bb64138
commit
0a8fe0a0c6
|
@ -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 }
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -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 ) }
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -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';
|
|
@ -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';
|
|
@ -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">
|
||||
|
|
|
@ -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
|
|
@ -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;
|
||||
}
|
|
@ -1,2 +1 @@
|
|||
export * from './use-select-shipping-rate';
|
||||
export * from './use-select-shipping-rates';
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue