Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New codemod to help migrating React components "defaultProps" usage #3681

Merged
merged 15 commits into from
Dec 20, 2024

Conversation

CarlosCortizasCT
Copy link
Contributor

@CarlosCortizasCT CarlosCortizasCT commented Dec 17, 2024

Summary

The way we are using React components defaultProps in our codebase is no longer supported in the new React 19 version (reference).

This is the main concept:

// BEFORE
type TMyComponentProps = {
  prop1: string;
  prop2: string;
  prop3: string;
};

function MyComponent(props: TMyComponentProps) {
  return (
    <ul>
    <li>Prop 1: {props.prop1}</li>
    <li>Prop 2: {props.prop2}</li>
    <li>Prop 3: {props.prop3}</li>
  </ul>
  );
}
MyComponent.defaultProps = {
  prop1: 'My default value',
};


// AFTER
type TMyComponentProps = {
  prop1?: string; // <--- Make sure this is marked as optional
  prop2: string;
  prop3: string;
};

function MyComponent({ prop1: 'My default value', ...props }: TMyComponentProps) {
  return (
    <ul>
    <li>Prop 1: {prop1}</li> {* <--- Direct access to destructured property *}
    <li>Prop 2: {props.prop2}</li>
    <li>Prop 3: {props.prop3}</li>
  </ul>
  );
}
// <--- No "defaultProps" component property

Description

There are multiple adjustments that are needed and this codemod does not address 100% of them but it helps with most of the common use cases. However, a double check review is recommended after running it; at least checking the Typescript types and building the codebase.

These are the main steps the codemod goes through:

  1. Search for React components that have a defaultProps property
MyComponent.defaultProps = {
  prop1: 'My default value',
};
  1. Extract the keys and values for the default props
  2. Update the component function signature
// BEFORE
function MyComponent(props: TMyComponentProps) { ... }

// AFTER
function MyComponent({ prop1, ...props }: TMyComponentProps) { ... }
  1. Refactor the usages of the default props in the body of the component
// BEFORE
function MyComponent(props: TMyComponentProps) {
  return (
    <ul>
      <li>Prop 1: {props.prop1}</li>
      <li>Prop 2: {props.prop2}</li>
      <li>Prop 3: {props.prop3}</li>
    </ul>
  );
}

// AFTER
// BEFORE
function MyComponent({ prop1, ...props }: TMyComponentProps) {
  return (
    <ul>
      <li>Prop 1: {prop1}</li>
      <li>Prop 2: {props.prop2}</li>
      <li>Prop 3: {props.prop3}</li>
    </ul>
  );
}
  1. Update the component TS type definition so we make sure the default props are optional
// BEFORE 
type TMyComponentProps = {
  prop1: string;
  prop2: string;
  prop3: string;
};

// AFTER
type TMyComponentProps = {
  prop1?: string;
  prop2: string;
  prop3: string;
};
  1. Remove the defaultProps assignment from the component
// Delete this code block
MyComponent.defaultProps = {
  prop1: 'My default value',
};

NOTE

I tried to be very verbose with the inline comments of the codemod explaining what every block is for.

Copy link

vercel bot commented Dec 17, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
mc-app-kit-playground ✅ Ready (Inspect) Visit Preview 💬 Add feedback Dec 19, 2024 3:52pm
merchant-center-application-kit-components-playground ✅ Ready (Inspect) Visit Preview 💬 Add feedback Dec 19, 2024 3:52pm

Copy link

changeset-bot bot commented Dec 17, 2024

🦋 Changeset detected

Latest commit: 4df345c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 36 packages
Name Type
@commercetools-frontend/codemod Minor
@commercetools-applications/merchant-center-template-starter-typescript Minor
@commercetools-applications/merchant-center-template-starter Minor
@commercetools-applications/merchant-center-custom-view-template-starter-typescript Minor
@commercetools-applications/merchant-center-custom-view-template-starter Minor
@commercetools-backend/eslint-config-node Minor
@commercetools-backend/express Minor
@commercetools-backend/loggers Minor
@commercetools-frontend/actions-global Minor
@commercetools-frontend/application-components Minor
@commercetools-frontend/application-config Minor
@commercetools-frontend/application-shell-connectors Minor
@commercetools-frontend/application-shell Minor
@commercetools-frontend/assets Minor
@commercetools-frontend/babel-preset-mc-app Minor
@commercetools-frontend/browser-history Minor
@commercetools-frontend/constants Minor
@commercetools-frontend/create-mc-app Minor
@commercetools-frontend/cypress Minor
@commercetools-frontend/eslint-config-mc-app Minor
@commercetools-frontend/i18n Minor
@commercetools-frontend/jest-preset-mc-app Minor
@commercetools-frontend/jest-stylelint-runner Minor
@commercetools-frontend/l10n Minor
@commercetools-frontend/mc-dev-authentication Minor
@commercetools-frontend/mc-html-template Minor
@commercetools-frontend/mc-scripts Minor
@commercetools-frontend/notifications Minor
@commercetools-frontend/permissions Minor
@commercetools-frontend/react-notifications Minor
@commercetools-frontend/sdk Minor
@commercetools-frontend/sentry Minor
@commercetools-frontend/url-utils Minor
@commercetools-local/playground Minor
@commercetools-local/visual-testing-app Minor
@commercetools-website/components-playground Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@CarlosCortizasCT CarlosCortizasCT self-assigned this Dec 17, 2024
@CarlosCortizasCT CarlosCortizasCT added the fe-chapter-rotation Tasks coming from frontend chapter work label Dec 17, 2024
Copy link
Contributor

@tdeekens tdeekens left a comment

Choose a reason for hiding this comment

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

I can follow along in a reasonable level of detail.

.changeset/purple-panthers-attack.md Outdated Show resolved Hide resolved
packages/codemod/README.md Outdated Show resolved Hide resolved
return <div>{prop1}</div>;
}
```
*/
Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for the comments!

...defaultPropsKeys.map((key) => {
const id = j.identifier(key);
const newProp = j.property('init', id, id);
newProp.shorthand = true;
Copy link
Contributor

Choose a reason for hiding this comment

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

Wasn't aware of this.

// Default props can be defined inline or as a reference to another object
// INLINE -- MyComponent.defaultProps: { prop1: 'value1', prop2: 'value2' }
// REFERENCE -- MyComponent.defaultProps: defaultProps
if (defaultPropsNode.type === 'Identifier') {
Copy link
Contributor

Choose a reason for hiding this comment

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

Good point. Didn't think of this use case.

);
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I can follow along. Took me a while to realize - thanks to the comments - that we need to handle different ways in which React.FCs can be written.

CarlosCortizasCT and others added 2 commits December 17, 2024 15:37
Co-authored-by: Tobias Deekens <[email protected]>
Co-authored-by: Tobias Deekens <[email protected]>
Copy link
Contributor

@ddouglasz ddouglasz left a comment

Choose a reason for hiding this comment

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

Thank you, especially for the step by step comments 🙌🏽

member.key.type === 'Identifier' &&
destructuredKeys.includes(member.key.name)
) {
member.optional = true;
Copy link
Contributor

Choose a reason for hiding this comment

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

This is where we make the type member of the prop optional when it is part of the prop right ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. I need to add another comment.

updateComponentTypes({
j,
root,
typeName: `${componentName}Props`,
Copy link
Contributor

Choose a reason for hiding this comment

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

This is also something I noticed, if we build the typeName from this, sometimes, there are cases where the typeName is inconsistent and we end up having files with typeNames as just ${componentName} without the Props suffix.
This also would have to be updated manually for cases like these.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, this relies on us using a consistent naming.
I think I can improve it further as a follow-up.

Copy link
Member

@emmenko emmenko Dec 18, 2024

Choose a reason for hiding this comment

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

We can remove the suffix Props from the existing name to make sure we can append it. If the component name doesn't end up with Props, the replace doesn't do anything and we can still append the suffix correctly.

Suggested change
typeName: `${componentName}Props`,
typeName: `${componentName.replace(/(Props)$/, '')}Props`,

Copy link
Contributor Author

Choose a reason for hiding this comment

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

To be honest, I run this codemod on the 100+ component in the ui-kit repository and didn't find this problem so I would prefer to actually have a use case for changing it.

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've just realized I was wrong. I had to adjust some things manually in ui-kit but didn't think it was because of this.
However, I would like to do it as a follow-up in order to not expand the scope of this PR.

Copy link
Member

Choose a reason for hiding this comment

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

I would like to do it as a follow-up in order to not expand the scope of this PR

Do you mean adding .replace(/(Props)$/, '')? Or are you referring to other things?

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 had a more elaborated idea in mind but I already implemented while I was waiting for the review.

Please take a look at this commit: 9af02f7

refactoredParameter.typeAnnotation = functionPropsParam.typeAnnotation;
break;
default:
console.warn(
Copy link
Contributor

Choose a reason for hiding this comment

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

🙌🏽

Copy link
Member

@emmenko emmenko left a comment

Choose a reason for hiding this comment

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

Thanks @CarlosCortizasCT ! 🙌

Could you write some tests for this new codemod? See test folder.

@CarlosCortizasCT
Copy link
Contributor Author

Thanks @CarlosCortizasCT ! 🙌

Could you write some tests for this new codemod? See test folder.

@emmenko that's a legit request but the problem is that jscodeshift test utils are synchronous and the new codemod is asynchronous so it does not work.
I could not find a simple way to overcome this challenge and I don't think it makes sense to spend time writing a different set of utilities.

If you have any suggestion on how to easily implement the tests, please let me know.

@emmenko
Copy link
Member

emmenko commented Dec 18, 2024

Ah ok, sorry I didn't know that it's an issue in the test if we use async.

Then it's fine as long as you are confident that it works as expected 🤗

@CarlosCortizasCT
Copy link
Contributor Author

Ah ok, sorry I didn't know that it's an issue in the test if we use async.

Then it's fine as long as you are confident that it works as expected 🤗

So far I'm doing manual testing to verify everything works as expected so I'm pretty confident.

Copy link
Member

@emmenko emmenko left a comment

Choose a reason for hiding this comment

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

💯

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
fe-chapter-rotation Tasks coming from frontend chapter work
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants