Skip to content
This repository has been archived by the owner on Feb 23, 2024. It is now read-only.

Ts blocks #5668

Merged
merged 16 commits into from
Feb 7, 2022
Merged

Ts blocks #5668

merged 16 commits into from
Feb 7, 2022

Conversation

alexflorisca
Copy link
Member

@alexflorisca alexflorisca commented Jan 28, 2022

This PR converts the following components to Typescript

  • base/components/ProductList
  • base/components/FilterElementLabel
  • base/components/FilterSubmitButton
  • base/components/Form
  • base/components/LoadMoreButton
  • base/components/LoadingMask
  • base/components/Pagination
  • base/components/SortSelect

The ProductList component was quite tricky to get completely error free for a couple of reasons:

  • Couldn't find the right way to type withScrollToTop so that it accepts ProductList as a component. The HOC expects a constructor, yet ProductList returns a JSX.Element. If anyone has any bright ideas here, I'd love to hear them. I've tried to type the OriginalComponent prop in the withScrollToTop HOC as React.ComponentType, as suggested here
  • I'm casting an unknown[] as ProductResponseItem[] as a temporary shortcut. I've left an inline comment with more explanation about this.

Testing

Automated Tests

  • Changes in this PR are covered by Automated Tests.
    • Unit tests
    • E2E tests

Manual Testing

How to test the changes in this Pull Request:

  1. Smoke test the All Products block at the least, adding extra inner blocks and making sure they render correctly.

User Facing Testing

These are steps for user testing (where "user" is someone interacting with this change that is not editing any code).

  • Same as above

@alexflorisca alexflorisca added type: refactor The issue/PR is related to refactoring. type: cooldown Things that are queued for a cooldown period (assists with planning). focus: blocks Specific work involving or impacting how blocks behave. labels Jan 28, 2022
@alexflorisca alexflorisca self-assigned this Jan 28, 2022
@rubikuserbot rubikuserbot requested review from a team and mikejolley and removed request for a team January 28, 2022 14:31
@github-actions
Copy link
Contributor

Remove this once getCollection selector and resolver is c...

Remove this once getCollection selector and resolver is converted to TS.


https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/e0531fbb9f503a50e1b1b0ab6f4d09bc859252c7/assets/js/base/context/hooks/use-store-products.ts#L47-L51

🚀 This comment was generated by the automations bot based on a todo comment in e0531fb in #5668. cc @alexflorisca

@github-actions
Copy link
Contributor

github-actions bot commented Jan 28, 2022

Size Change: +507 B (0%)

Total Size: 812 kB

Filename Size Change
build/active-filters-frontend.js 6.37 kB +147 B (+2%)
build/active-filters.js 7.05 kB +145 B (+2%)
build/all-products-frontend.js 18.7 kB +101 B (+1%)
build/all-products.js 33.8 kB +73 B (0%)
build/all-reviews.js 8.06 kB -15 B (0%)
build/atomic-block-components/add-to-cart--atomic-block-components/button--atomic-block-components/image---8f355022.js 255 B +17 B (+7%) 🔍
build/atomic-block-components/add-to-cart--atomic-block-components/button--atomic-block-components/image---a7e2bb9b.js 2.67 kB -3 B (0%)
build/atomic-block-components/add-to-cart-frontend.js 6.89 kB -3 B (0%)
build/atomic-block-components/add-to-cart.js 6.46 kB -3 B (0%)
build/atomic-block-components/button-frontend.js 1.48 kB -1 B (0%)
build/atomic-block-components/button.js 853 B +1 B (0%)
build/atomic-block-components/image.js 1.05 kB -1 B (0%)
build/atomic-block-components/price-frontend.js 1.74 kB -4 B (0%)
build/atomic-block-components/price.js 1.69 kB -1 B (0%)
build/atomic-block-components/rating-frontend.js 703 B +5 B (+1%)
build/atomic-block-components/rating.js 704 B +4 B (+1%)
build/atomic-block-components/sale-badge-frontend.js 625 B +1 B (0%)
build/atomic-block-components/sale-badge.js 623 B +1 B (0%)
build/atomic-block-components/sku.js 386 B +1 B (0%)
build/atomic-block-components/stock-indicator--atomic-block-components/summary--atomic-block-components/title.js 468 B +3 B (+1%)
build/atomic-block-components/stock-indicator-frontend.js 940 B -2 B (0%)
build/atomic-block-components/stock-indicator.js 626 B +1 B (0%)
build/atomic-block-components/summary-frontend.js 1.24 kB -2 B (0%)
build/atomic-block-components/tag-list-frontend.js 459 B +1 B (0%)
build/atomic-block-components/title-frontend.js 1.21 kB -3 B (0%)
build/atomic-block-components/title.js 933 B -1 B (0%)
build/attribute-filter-frontend.js 16.8 kB +32 B (0%)
build/attribute-filter.js 13 kB +48 B (0%)
build/cart-blocks/accepted-payment-methods-frontend.js 1.15 kB +1 B (0%)
build/cart-blocks/checkout-button-frontend.js 1.14 kB -1 B (0%)
build/cart-blocks/empty-cart-frontend.js 346 B +1 B (0%)
build/cart-blocks/express-payment-frontend.js 5.18 kB -1 B (0%)
build/cart-blocks/filled-cart-frontend.js 766 B -1 B (0%)
build/cart-blocks/line-items-frontend.js 5.49 kB -3 B (0%)
build/cart-frontend.js 45.5 kB +11 B (0%)
build/cart.js 43.7 kB +5 B (0%)
build/checkout-blocks/billing-address--checkout-blocks/shipping-address-frontend.js 4.23 kB +2 B (0%)
build/checkout-blocks/contact-information-frontend.js 2.95 kB +4 B (0%)
build/checkout-blocks/express-payment-frontend.js 5.47 kB -7 B (0%)
build/checkout-blocks/order-note-frontend.js 1.13 kB -1 B (0%)
build/checkout-blocks/order-summary-frontend.js 11.4 kB -2 B (0%)
build/checkout-blocks/payment-frontend.js 7.71 kB -13 B (0%)
build/checkout-blocks/shipping-methods-frontend.js 4.92 kB -8 B (0%)
build/checkout-blocks/terms-frontend.js 1.22 kB +1 B (0%)
build/checkout-frontend.js 47.5 kB +10 B (0%)
build/checkout.js 45.2 kB +1 B (0%)
build/featured-category.js 8.51 kB +1 B (0%)
build/featured-product.js 9.62 kB +1 B (0%)
build/mini-cart-component-frontend.js 14.2 kB +17 B (0%)
build/mini-cart-contents.js 3.82 kB -1 B (0%)
build/mini-cart-frontend.js 1.77 kB +12 B (+1%)
build/mini-cart.js 6.39 kB -1 B (0%)
build/price-filter-frontend.js 12.6 kB +46 B (0%)
build/price-filter.js 8.5 kB +22 B (0%)
build/product-categories.js 3.17 kB -2 B (0%)
build/product-on-sale.js 7.98 kB +1 B (0%)
build/product-top-rated.js 7.9 kB +1 B (0%)
build/reviews-by-category.js 11.5 kB -16 B (0%)
build/reviews-by-product.js 12.6 kB -18 B (0%)
build/reviews-frontend.js 7.35 kB -27 B (0%)
build/single-product-frontend.js 22.2 kB +10 B (0%)
build/stock-filter-frontend.js 6.61 kB -223 B (-3%)
build/stock-filter.js 6.69 kB +140 B (+2%)
build/vendors--atomic-block-components/add-to-cart--cart-blocks/order-summary--checkout-blocks/billing-ad--c5eb4dcd-frontend.js 19 kB +1 B (0%)
build/wc-blocks-vendors.js 69.7 kB +2 B (0%)
ℹ️ View Unchanged
Filename Size
build/atomic-block-components/add-to-cart--atomic-block-components/button.js 1.48 kB
build/atomic-block-components/category-list-frontend.js 458 B
build/atomic-block-components/category-list.js 459 B
build/atomic-block-components/image-frontend.js 1.37 kB
build/atomic-block-components/sku-frontend.js 386 B
build/atomic-block-components/summary.js 926 B
build/atomic-block-components/tag-list.js 459 B
build/blocks-checkout.js 17.6 kB
build/cart-blocks/items-frontend.js 298 B
build/cart-blocks/order-summary-frontend.js 8.95 kB
build/cart-blocks/totals-frontend.js 321 B
build/checkout-blocks/actions-frontend.js 1.39 kB
build/checkout-blocks/billing-address-frontend.js 887 B
build/checkout-blocks/fields-frontend.js 343 B
build/checkout-blocks/shipping-address-frontend.js 974 B
build/checkout-blocks/totals-frontend.js 323 B
build/handpicked-products.js 7.09 kB
build/legacy-template.js 2.19 kB
build/price-format.js 1.18 kB
build/product-best-sellers.js 7.36 kB
build/product-category.js 8.49 kB
build/product-new.js 7.66 kB
build/product-search.js 2.18 kB
build/product-tag.js 7.81 kB
build/products-by-attribute.js 8.38 kB
build/single-product.js 10 kB
build/vendors--atomic-block-components/add-to-cart-frontend.js 7.51 kB
build/vendors--atomic-block-components/price--cart-blocks/line-items--cart-blocks/order-summary--checkout--8a3571de-frontend.js 5.71 kB
build/vendors--cart-blocks/line-items--checkout-blocks/order-summary-frontend.js 3.14 kB
build/vendors--cart-blocks/order-summary--checkout-blocks/billing-address--checkout-blocks/order-summary---eb4d2cec-frontend.js 4.74 kB
build/wc-blocks-data.js 8.84 kB
build/wc-blocks-editor-style-rtl.css 4.79 kB
build/wc-blocks-editor-style.css 4.79 kB
build/wc-blocks-google-analytics.js 1.56 kB
build/wc-blocks-middleware.js 949 B
build/wc-blocks-registry.js 2.7 kB
build/wc-blocks-shared-context.js 1.52 kB
build/wc-blocks-shared-hocs.js 1.14 kB
build/wc-blocks-style-rtl.css 21.9 kB
build/wc-blocks-style.css 21.9 kB
build/wc-blocks-vendors-style-rtl.css 1.28 kB
build/wc-blocks-vendors-style.css 1.28 kB
build/wc-blocks.js 2.62 kB
build/wc-payment-method-bacs.js 816 B
build/wc-payment-method-cheque.js 811 B
build/wc-payment-method-cod.js 909 B
build/wc-payment-method-paypal.js 837 B
build/wc-settings.js 2.61 kB

compressed-size-action

@@ -34,8 +44,8 @@ export const useStoreProducts = ( query ) => {
query,
} );
return {
products,
totalProducts: parseInt( totalProducts, 10 ),
products: products as ProductResponseItem[], // TODO: Remove this once getCollection selector and resolver is converted to TS.
Copy link
Member Author

Choose a reason for hiding this comment

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

On line 38 above we call the useCollection hook, which looks up some data from a store and returns it. We need to convert quite a few other things to TS in order to get the correct types here (useCollection hook, some selectors and resolvers in the data folder, and it's not an easy task as most of these return generic nested objects so it needs a little more thought. I didn't have the time to convert everything needed by the useStoreProducts hook in order to return the correct types from here, so I cast it to an array of ProductResponseItem which we are expecting here until we get to convert all other relevant hooks and state related code to TS.

@@ -54,7 +55,6 @@ const ProductSortSelect = ( { onChange, readOnly, value } ) => {
),
},
] }
readOnly={ readOnly }
Copy link
Member Author

Choose a reason for hiding this comment

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

removed the readOnly property from the SortSelect component as it was never used, and was being applied to a HTML Element which is not valid

return (
<SortSelect
className="wc-block-product-sort-select wc-block-components-product-sort-select"
name="orderby"
Copy link
Member Author

Choose a reason for hiding this comment

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

removed invalid name property being passed to SortSelect component. This is not defined or used in SortSelect

@tomasztunik
Copy link
Contributor

tomasztunik commented Jan 31, 2022

Hi Alex, got a question regarding typing the components.

Is there a reason we were not using generic React.FC and React.VFC functional component types but rather than that we type children when present and return type on every component definition?

Ie. for React Void Functional Component / VFC, children-less
https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/8e707a8931d9518b8d364ccc49ba9e9363943a1c/assets/js/base/components/filter-element-label/index.tsx#L23-L26

interface FilterElementLabelProps {
	name: string;
	count: number;
}

const FilterElementLabel: React.VFC< FilterElementLabelProps > = ( {
	name,
	count,
} ) => {

and for React Functional Component / FC, children included
https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/8e707a8931d9518b8d364ccc49ba9e9363943a1c/assets/js/base/components/form/index.tsx#L7-L17

 interface FormProps { 
 	className: string; 
 	onSubmit: ( event: FormEvent ) => void; 
 } 
  
 const Form: React.FC< FormProps > = ( { 
 	className, 
 	children, 
 	onSubmit = ( event ) => void event, 
 } ) => { 
    // ...
 }

Using React.FC and React.VFC we can be explicit about if the component accepts children or not without having to type them every time or having to inspect props to learn what type of component it was. On top of it, we would be future compatible if the base functional component interface evolved.

I've noticed it was used only once in the Chip component but I believe this to be the most explicit way to type functional components in React and would love us to follow this typing pattern.

What do you think?

@alexflorisca
Copy link
Member Author

alexflorisca commented Jan 31, 2022

Hi @tomasztunik,

Thanks for your comment. I agree that using React.FC is neater, however there is a lot of evidence that it's not actually best practice, and it's better to be explicit defining the types of the props and return type. There's been some discussion around this and I'll point you to a couple of P2 articles, (pca54o-2ym-p2 and pca54o-2WY-p2) for further reading and some links to a bunch of articles & prs that explain in more detail why using React.FC is generally a bad idea.

Copy link
Member

@mikejolley mikejolley left a comment

Choose a reason for hiding this comment

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

Hi @alexflorisca

Overall looks good, I added some inline feedback and questions.

export const getBlockMap = ( blockName ) =>
export const getBlockMap = (
blockName: string
): Record< string, React.ComponentType > =>
Copy link
Member

Choose a reason for hiding this comment

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

I see some types in our checkout block registry which look related and use:

component:
		| LazyExoticComponent< React.ComponentType< unknown > >
		| ( () => JSX.Element | null )
		| null;

Is this typing the same thing? https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/8475bb8c47499ad535aceb4391d9bafa30ec3228/packages/checkout/blocks-registry/types.ts#L36

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep, thanks for pointing this out. I've extracted a type RegisteredBlockComponent and re-used it instead

return (
<>
{ name }
{ Number.isFinite( count ) && (
<Label
label={ count }
label={ count.toString() }
Copy link
Member

Choose a reason for hiding this comment

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

I know this fixes the type, but I see some usage of this component like:

count={ blockAttributes.showCounts ? count : null }

I think this may error? We should fix the cases which consume this component, or make it optional.

Copy link
Member Author

Choose a reason for hiding this comment

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

I see what you mean. Those occurrences should also pass count.toString() but they're not typed yet. The Label component expects a string as the count, but it doesn't do any number logic with the count, it just outputs it as inside a HTML element, so should be safe.

assets/js/base/components/form/index.tsx Outdated Show resolved Hide resolved
assets/js/base/components/load-more-button/index.tsx Outdated Show resolved Hide resolved
@@ -11,14 +10,21 @@ import classNames from 'classnames';
import './style.scss';
import Spinner from '../spinner';

interface LoadingMaskProps {
children?: React.ReactChildren;
Copy link
Member

Choose a reason for hiding this comment

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

I see in other places in the codebase we are using ReactNode or ReactNode[] rather than React.ReactChildren; . Do you know which is best practice, and can we decide on a standard to use throughout?

Copy link
Member Author

@alexflorisca alexflorisca Feb 4, 2022

Choose a reason for hiding this comment

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

You're right, there are multiple ways to type children and this article sums them up pretty well. I suggest we stick with ReactNode or ReactNode[] for now as our codebase is so complex, that narrowing children down further will require changes at multiple levels where components are nested within each other. It will be quite time consuming to trace down every component that the children pass through and modify the types, I don't think it's worth the effort. It's something to bare in mind going forward though: being more specific with the types of children that a component is allowed is the better way to go and will catch more errors

assets/js/base/components/product-list/container.tsx Outdated Show resolved Hide resolved
@@ -195,7 +207,7 @@ const ProductList = ( {
! Number.isFinite( totalProducts ) &&
Number.isFinite( previousQueryTotals?.totalProducts ) &&
isEqual( totalQuery, previousQueryTotals?.totalQuery )
? Math.ceil( previousQueryTotals.totalProducts / perPage )
? Math.ceil( ( previousQueryTotals?.totalProducts || 0 ) / perPage )
Copy link
Member

Choose a reason for hiding this comment

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

Can we force this shape earlier to avoid checking for existance of totalProducts? Just for readability sake.

Copy link
Member Author

Choose a reason for hiding this comment

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

The problem here is that previousQueryTotals could be undefined. I think this is the cleanest way using the optional chaining (?.) operator and defining a default value inline if the prop isn't defined. The alternative I see would be to wrap that entire block in an if(previousQueryTotals) {} statement. I don't think we can force previousQueryTotals to never be undefined. If you have any other ideas I'm open to suggestions

assets/js/base/components/sort-select/index.tsx Outdated Show resolved Hide resolved
@alexflorisca
Copy link
Member Author

Thanks for the review @mikejolley, I've addressed most of what you rightly pointed out and answered some of your questions inline. I've also fixed the unit tests so fingers crossed everything should be green. Let me know if you spot anything else!

Copy link
Member

@mikejolley mikejolley left a comment

Choose a reason for hiding this comment

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

Thanks for replying to feedback.

@github-actions github-actions bot added this to the 7.0.0 milestone Feb 4, 2022
@alexflorisca alexflorisca merged commit c6c24aa into trunk Feb 7, 2022
@alexflorisca alexflorisca deleted the ts-blocks branch February 7, 2022 09:34
@sunyatasattva sunyatasattva added the skip-changelog PRs that you don't want to appear in the changelog. label Feb 14, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
focus: blocks Specific work involving or impacting how blocks behave. skip-changelog PRs that you don't want to appear in the changelog. type: cooldown Things that are queued for a cooldown period (assists with planning). type: refactor The issue/PR is related to refactoring.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants