Coder Social home page Coder Social logo

codemod-replace-react-fc-typescript's Introduction

codemod-replace-react-fc-typescript

A codemod using jscodeshift to remove React.FC and React.SFC from your codebase

gif animation showing how this codemod removes React.FC

(It's recommended to run your favorite formatting tool after the codemod ๐Ÿ˜‰ )

๐Ÿ’ก Running the codemod with Intuita will automatically format your code with prettier.

๐Ÿ‘จโ€๐Ÿซ Motivation

IF you use React and Typescript, you might have come across this GitHub PR in Create React App's repo about removing React.FC from their base template of a Typescript project.

The three main points that made me buy this was the fact that:

  • There's an implicit definition of children - all your components will have children typed!
  • They don't support generics
  • It does not correctly work with defaultProps

as well as other downsides (check out the PR description for that)

Motivated by that PR, and a lot of blog posts who also shared similar conclusions (and this ADR from Spotify's team in which they recorded the decision of removing React.FC from they codebase too), I wrote this little codemod that drops React.FC, React.FunctionComponent and React.SFC (which was also deprecated) and replaces the Props as the type of the unique argument in the component definition.

Let's see it with code

// before codemod runs
type Props2 = { id: number };
export const MyComponent2: React.FC<Props2> = (props) => {
  return <span>{props.id}</span>
}

// after codemod runs
type Props2 = { id: number };
export const MyComponent2 = (props: Props2) => {
  return <span>{props.id}</span>
}

It also works if the Props are defined inline

// before codemod runs
export const MyComponent4: React.FC<{ inlineProp: number, disabled?: boolean }> = (props) => <span>foo</span>

// after codemod runs
export const MyComponent4 = (
  props: {
    inlineProp: number,
    disabled?: boolean
  }
) => <span>foo</span>

It works with generics too!

// before codemod runs
type GenericsProps<T extends any> = { config: T }
export const MyComponentWithGenerics: React.FC<GenericsProps<string>> = (props) => <span>{props.config}</span>
export const MyComponentWithGenerics2: React.FC<GenericsProps<{ text: string }>> = ({ config: { text }}) => <span>{text}</span>

// after codemod runs
type GenericsProps<T extends any> = { config: T }
export const MyComponentWithGenerics = (props: GenericsProps<string>) => <span>{props.config}</span>
export const MyComponentWithGenerics2 = (
  {
    config: { text }
  }: GenericsProps<{ text: string }>
) => <span>{text}</span>

and with props defined with intersection

// before codemod runs
const WithIntersection: React.FC<Props1 & Props2> = ({ id, ...restProps }) => <span>{id}</span>

// after codemod runs
const WithIntersection = ( { id, ...restProps }: Props1 & Props2 ) => <span>{id}</span>

and with component modules defined using intersection

// before codemod runs
import React from 'react';
import { OtherComponent } from "./other-component";

interface Props { text: string }
const WithComponentIntersection: React.FC<Props> & {
  OtherComponent: typeof OtherComponent;
} = (props) => {
  return <span>{props.text}</span>
}
WithComponentIntersection.OtherComponent = OtherComponent;

// after codemod runs
import React from 'react';
import { OtherComponent } from "./other-component";

interface Props { text: string }
const WithComponentIntersection = (props: Props) => {
  return <span>{props.text}</span>
}
WithComponentIntersection.OtherComponent = OtherComponent;

Even with no Props!

// before codemod runs
const NoPropsComponent: React.FC = () => <span>foo</span>

// after codemod runs
const NoPropsComponent = () => <span>foo</span>

You don't have to stick with arrow functions only; all the previous scenarios work with regular named functions as well

// before codemod runs
import React from 'react'

interface Props { text: string }
const HelloWorld: React.SFC<Props> = function HelloWorld(props) {
  return <div>Hi {props.someValue}</div>
}

// after codemod runs
import React from 'react'

interface Props { text: string }
const HelloWorld = function HelloWorld(props: Props) {
  return <div>Hi {props.someValue}</div>
}

It also works when you use a function that accepts a component definition

// before codemod runs
import React from 'react';
import { observer } from "mobx-react-lite";

type Props = { id: number };
const functionAcceptsComponent: React.FC<Props> = observer((props) => {
  return <span>{props.id}</span>
})

// after codemod runs
import React from 'react';
import { observer } from "mobx-react-lite";

type Props = { id: number };
export const functionAcceptsComponent = observer((props: Props) => {
  return <span>{props.id}</span>
})

This codemod also works when using FC, FunctionComponent and SFC as a named export

// before codemod runs
import React, { FC } from 'react'

const NamedExportComponent: FC<Props> = (props) => <span>foo</span>

// after codemod runs
import React, { FC } from 'react'

const NamedExportComponent = (props: Props) => <span>foo</span>

๐Ÿงฐ How to use

Using Intuita

gif animation showing how to use Intuita to run this codemod

To run the codemod with the Intuita VS Code extension, install Intuita VS Code extension and run the codemod:

Intuita Run Codemod

๐Ÿ’ก To learn more about running codemods using Intuita, check the usage guide here.

Using jscodeshift

Run the following command

npx jscodeshift -- -t https://raw.githubusercontent.com/gndelia/codemod-replace-react-fc-typescript/main/dist/index.js --extensions=tsx --verbose=2 <FOLDER-YOU-WANT-TO-TRANSFORM>

There are other options you can read in the jscodeshift's Readme.

jscodeshift only accepts local transform files, or remote self-contained files. That's why I compiled the transform file into one distributable file using @vercel/ncc. If you don't want to run this remote file (because you might not trust, although you can read the source - it is totally safe), you can download this repo and run

npx jscodeshift -- -t Path/To/Repo/transform.ts --extensions=tsx --verbose=2 <FOLDER-YOU-WANT-TO-TRANSFORM>

๐Ÿ““ Notes

  • The codemod focuses on replacing the nodes but does not do styling. If you run the codemod using jscodeshift CLI, you might want to run Prettier or your favorite formatting tool after the code has been modified. However, if you run the codemod using intuita, you can safely skip this part as Intuita automatically formats the output with prettier.

Example:

import React from 'react'

interface Props { id: number, text: string }
const Component: React.FC<Props> = (props) => (
  <div>
    <span>{props.id}</span>
  </div>
)

after running the codemod, you might lose the parenthesis

import React from 'react'

interface Props { id: number, text: string }
const Component = (props: Props) => <div>
  <span>{props.id}</span>
</div>

this is because those parenthesis are not strictly required for the code to work. You can fix this by running Prettier (or whatever tool you're using to format your code) easily, as the code is still valid

  • If your component was using the implicit definition of children provided by React.FC, you will have to add the explicit definition or the code won't compile. For example, the following code
import React from 'react'

type Props = { title: string }
const Component: React.FC<Props> = ({ title, children }) => <div title={title}>{children}</div>

will be transformed into this after running the codemod

import React from 'react'

type Props = { title: string }
const Component = ({ title, children }: Props) => <div title={title}>{children}</div>

However, it won't compile because children is not part of your Props definition anymore. You can solve this by manually adding the type of children again.

The value that React.FC provides (that accepts anything you would accept in js as children) is { children?: ReactNode }. I'm intentionally not automatically adding it because you can restrict it to what you only want to accept (for instance, just a string, a number, only one component, and so on), and you know better than I do what you need.

Contributors โœจ

Thanks goes to these wonderful people (emoji key):

Gonzalo D'Elia
Gonzalo D'Elia

๐Ÿ’ป โš ๏ธ ๐Ÿ“– ๐Ÿ‘€ ๐Ÿš‡
Bryan Lee
Bryan Lee

โš ๏ธ ๐Ÿ› ๐Ÿ“– ๐Ÿ’ป
Yaroslav Lapin
Yaroslav Lapin

๐Ÿ“–
Mohab Sameh
Mohab Sameh

๐Ÿ“–

This project follows the all-contributors specification. Contributions of any kind welcome!

codemod-replace-react-fc-typescript's People

Contributors

allcontributors[bot] avatar dependabot[bot] avatar gndelia avatar jlarky avatar liby avatar mohab-sameh avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

codemod-replace-react-fc-typescript's Issues

Can we reverse it?

Interestingly, now that React 18 types have removed the implicit children problem, I want to go the other way, from a function with props typed as an object to React.FunctionalComponent.. How hard would that be?

Allow users to run this codemod from npx

The current run command seems to be excessive in complexity. There just should be really one step, something along the lines of

npx codemod-command <FOLDER-YOU-WANT-TO-TRANSFORM> <other-optional-flags>

Do some research on how to implement this - it might require implementing an npm package, perhaps?

Add option to add children to props

The current behaviour removes React.FC from the component declaration, which has an implicit definition of children, leaving its definition to be set manually in each case. An option could be set to forcefully add { children?: React.ReactNode } as props, which are the value that React.FC offers. It should be an opt-in option

Does not work when encounter function component with decorator

The script does not work in the following three situations:

1. Function components with `observer` decorator receive parameters: throw error
// before codemod runs
import { observer } from "mobx-react-lite";

type Props = { id: number };
export const MyComponent: React.FC<Props> = observer((props) => {
  return <span>{props.id}</span>
});

// after codemod runs
โฏ jscodeshift -t https://raw.githubusercontent.com/gndelia/codemod-replace-react-fc-typescript/main/dist/index.js --extensions=tsx --verbose=2 ./my-component.tsx
Processing 1 files... 
Spawning 1 workers...
Sending 1 files to free worker...
TypeError: Cannot read property 'length' of undefined
    at addPropsTypeToComponentBody (/private/var/folders/ym/1mprqx7x45bdrz5zt253_0nw0000gn/T/jscodeshift202162-41931-4ws4fq.5drs.js:30:42)
    at NodePath.<anonymous> (/private/var/folders/ym/1mprqx7x45bdrz5zt253_0nw0000gn/T/jscodeshift202162-41931-4ws4fq.5drs.js:105:13)
    at /Users/bryan/.nvs/node/14.15.5/x64/lib/node_modules/jscodeshift/src/Collection.js:75:36
    at Array.forEach (<anonymous>)
    at Collection.forEach (/Users/bryan/.nvs/node/14.15.5/x64/lib/node_modules/jscodeshift/src/Collection.js:74:18)
    at exports.default (/private/var/folders/ym/1mprqx7x45bdrz5zt253_0nw0000gn/T/jscodeshift202162-41931-4ws4fq.5drs.js:103:14)
    at /Users/bryan/.nvs/node/14.15.5/x64/lib/node_modules/jscodeshift/src/Worker.js:157:29
    at /Users/bryan/.nvs/node/14.15.5/x64/lib/node_modules/jscodeshift/node_modules/graceful-fs/graceful-fs.js:123:16
    at FSReqCallback.readFileAfterClose [as oncomplete] (internal/fs/read_file_context.js:63:3)
 SKIP ./header.component.tsx
All done. 
Results: 
0 errors
0 unmodified
1 skipped
0 ok
Time elapsed: 1.317seconds 
1.1 Function components with `observer` decorator do not receive arguments: work normally
// before codemod runs
import { observer } from "mobx-react-lite";

export const MyComponent: React.FC = observer(() => {
  return <span>Demo</span>
});

// after codemod runs
export const MyComponent = observer(() => {
  return <span>Demo</span>
});
2. When there is intersection types in function component: skip execution
// before codemod runs
import { OtherComponent } from "./other-component";
interface Props {
  text: string;
}
export const MyComponent: React.FC<Props> & {
  OtherComponent: typeof OtherComponent;
} = ({ text }) => <span>{text}</span>;
MyComponent.OtherComponent = OtherComponent;

// after codemod runs
โฏ  jscodeshift -t https://raw.githubusercontent.com/gndelia/codemod-replace-react-fc-typescript/main/dist/index.js --extensions=tsx --verbose=0 ./my-component.tsx
Processing 1 files...
Spawning 1 workers...
Sending 1 files to free worker...
All done.
Results:
0 errors
0 unmodified
1 skipped
0 ok
Time elapsed: 0.894seconds
3. React.FunctionComponent will be skipped execution
// before codemod runs
export const MyComponent: React.FunctionComponent = () => <span>Demo</span>;

// after codemod runs
โฏ  jscodeshift -t https://raw.githubusercontent.com/gndelia/codemod-replace-react-fc-typescript/main/dist/index.js --extensions=tsx --verbose=0 ./my-component.tsx
Processing 1 files... 
Spawning 1 workers...
Sending 1 files to free worker...
 SKIP ./main.component.tsx
All done. 
Results: 
0 errors
0 unmodified
1 skipped
0 ok
Time elapsed: 1.300seconds 

I tried to modify the script to execute without errors; however, I was unable to add a type definition after the argument while keeping observer().

// before codemod runs
export const MyComponent: React.FC<{ text: string }> = observer(({text}) => <span>{text}</span>)

// after codemod runs
export const MyComponent = observer(({text}) => <span>{text}</span>)

Link to the modified code: https://pastebin.ubuntu.com/p/h5d2gZyWZg/

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.