{"id":20434422,"url":"https://github.com/wantedly/react-declassify","last_synced_at":"2025-04-04T15:12:06.119Z","repository":{"id":88335030,"uuid":"607032073","full_name":"wantedly/react-declassify","owner":"wantedly","description":"say goodbye to class components","archived":false,"fork":false,"pushed_at":"2025-03-27T04:37:46.000Z","size":1862,"stargazers_count":95,"open_issues_count":14,"forks_count":2,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-04-02T21:19:01.218Z","etag":null,"topics":["automation","babel","codemod","react","reacthooks"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/wantedly.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-02-27T06:57:52.000Z","updated_at":"2025-03-07T21:16:36.000Z","dependencies_parsed_at":"2023-09-25T13:11:54.202Z","dependency_job_id":"9b851d18-ce8a-4003-969e-16f7fba093e8","html_url":"https://github.com/wantedly/react-declassify","commit_stats":{"total_commits":130,"total_committers":2,"mean_commits":65.0,"dds":0.07692307692307687,"last_synced_commit":"023237f6ce4628bff2cd89c7d728e992e55c7c1c"},"previous_names":[],"tags_count":12,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wantedly%2Freact-declassify","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wantedly%2Freact-declassify/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wantedly%2Freact-declassify/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wantedly%2Freact-declassify/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/wantedly","download_url":"https://codeload.github.com/wantedly/react-declassify/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247198469,"owners_count":20900081,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["automation","babel","codemod","react","reacthooks"],"created_at":"2024-11-15T08:26:35.683Z","updated_at":"2025-04-04T15:12:06.101Z","avatar_url":"https://github.com/wantedly.png","language":"TypeScript","readme":"# react-declassify: say goodbye to class components\n\n\u003cimg src=\"./img/logo.png\" height=\"150\"\u003e\n\nThis codemod automatically transforms **React class components** into **React functional components using Hooks** for you!\n\n| Before                                         | After                                        |\n| ---------------------------------------------- | -------------------------------------------- |\n| ![before example 1](./img/example1-before.png) | ![after example 1](./img/example1-after.png) |\n\n## Features\n\n- ✅ Supports props, states, methods, and refs.\n- ✅ Comments, spaces, and styles are preserved thanks to the [recast](https://github.com/benjamn/recast) library.\n- ✅ Designed to generate as idiomatic code as possible. Not something Babel or Webpack would generate!\n- ✅ Based on classical heuristic automation; no need to be fearful about whimsy LLMs.\n\n## Why do we need this?\n\nClass components are [still going to be supported by React for the foreseeable future](https://react.dev/reference/react/Component). However, it is no longer recommended to write new components in class-style.\n\nSo what about the existing components? Although React will continue to support these, you may struggle to maintain them because:\n\n- New libraries and new versions of existing libraries tend to focus on Hooks-style components, and you may find you in a difficulty adopting the components to the libraries.\n- Class components may appear alien to those who are young in React development experience.\n\nThus it is still a good idea to migrate from class components to Hooks-based components.\n\nHowever, as this is not a simple syntactic change, migration needs a careful hand work and a careful review. This tool is a classic automation, it reduces a risk of introducing human errors during migration.\n\n## Usage\n\n```\nyarn add -D @codemod/cli react-declassify\n# OR\nnpm install -D @codemod/cli react-declassify\n```\n\nthen\n\n```\nnpx codemod --plugin react-declassify 'src/**/*.tsx'\n```\n\n## Example\n\nBefore:\n\n\u003c!-- prettier-ignore --\u003e\n```tsx\nimport React from \"react\";\n\ntype Props = {\n  by: number;\n};\n\ntype State = {\n  counter: number;\n};\n\nexport class C extends React.Component\u003cProps, State\u003e {\n  static defaultProps = {\n    by: 1\n  };\n\n  constructor(props) {\n    super(props);\n    this.state = {\n      counter: 0\n    };\n  }\n\n  render() {\n    return (\n      \u003c\u003e\n        \u003cbutton onClick={() =\u003e this.onClick()}\u003e\n          {this.state.counter}\n        \u003c/button\u003e\n        \u003cp\u003eCurrent step: {this.props.by}\u003c/p\u003e\n      \u003c/\u003e\n    );\n  }\n\n  onClick() {\n    this.setState({ counter: this.state.counter + this.props.by });\n  }\n}\n```\n\nAfter:\n\n\u003c!-- prettier-ignore --\u003e\n```tsx\nimport React from \"react\";\n\ntype Props = {\n  by?: number | undefined\n};\n\ntype State = {\n  counter: number;\n};\n\nexport const C: React.FC\u003cProps\u003e = props =\u003e {\n  const {\n    by = 1\n  } = props;\n\n  const [counter, setCounter] = React.useState\u003cnumber\u003e(0);\n\n  function onClick() {\n    setCounter(counter + by);\n  }\n\n  return \u003c\u003e\n    \u003cbutton onClick={() =\u003e onClick()}\u003e\n      {counter}\n    \u003c/button\u003e\n    \u003cp\u003eCurrent step: {by}\u003c/p\u003e\n  \u003c/\u003e;\n};\n```\n\nBefore:\n\n\u003c!-- prettier-ignore --\u003e\n```jsx\nimport React from \"react\";\n\nexport class C extends React.Component {\n  render() {\n    const { text, color } = this.props;\n    return \u003cbutton style={{ color }} onClick={() =\u003e this.onClick()}\u003e{text}\u003c/button\u003e;\n  }\n\n  onClick() {\n    const { text, handleClick } = this.props;\n    alert(`${text} was clicked!`);\n    handleClick();\n  }\n}\n```\n\nAfter:\n\n\u003c!-- prettier-ignore --\u003e\n```jsx\nimport React from \"react\";\n\nexport const C = props =\u003e {\n  const {\n    text,\n    color,\n    handleClick\n  } = props;\n\n  function onClick() {\n    alert(`${text} was clicked!`);\n    handleClick();\n  }\n\n  return \u003cbutton style={{ color }} onClick={() =\u003e onClick()}\u003e{text}\u003c/button\u003e;\n};\n```\n\n## Errors\n\nHard errors are indicated by `/* react-declassify-disable Cannot perform transformation */`.\n\nSoft errors are indicated by special variable names including:\n\n- `TODO_this`\n\nHard errors stop transformation of the whole class while stop errors do not. You need to fix the errors to conclude transformation.\n\n## Configuration\n\n### Disabling transformation\n\nAdding to the class a comment including `react-declassify-disable` will disable transformation of that class.\n\n```js\n/* react-declassify-disable */\nclass MyComponent extends React.Component {}\n```\n\nMarking the component class as `abstract` or `/** @abstract */` also disables transformation.\n\n### Import style\n\nThe codemod follows your import style from the `extends` clause. So\n\n```js\nimport React from \"react\";\n\nclass MyComponent extends React.Component {}\n```\n\nis transformed to\n\n```js\nimport React from \"react\";\n\nconst MyComponent: React.FC = () =\u003e {};\n```\n\nwhereas\n\n```js\nimport { Component } from \"react\";\n\nclass MyComponent extends Component {}\n```\n\nis transformed to\n\n```js\nimport { Component, FC } from \"react\";\n\nconst MyComponent: FC = () =\u003e {};\n```\n\nIt cannot be configured to mix these styles. For example it cannot emit `React.FC` for typing while emitting `useState` (not `React.useState`) for hooks.\n\n### Receiving refs\n\nClass components may receive refs; this is to be supported in the future. Once it is implemented, you will be able to add special directives in the component to enable the feature.\n\n### Syntactic styles\n\nThis codemod relies on [recast](https://github.com/benjamn/recast) for pretty-printing and sometimes generates code that does not match your preferred style. This is ineviable. For example it does not currently emit parentheses for the arrow function:\n\n\u003c!-- prettier-ignore --\u003e\n```js\nconst MyComponent: FC = props =\u003e {\n  //                    ^^^^^ no parentheses\n  // ...\n};\n```\n\nWe have no control over this choice. Even if it were possible, allowing configurations on styles would make the codemod unnecessarily complex.\n\nIf you need to enforce specific styles, use Prettier or ESLint or whatever is your favorite to reformat the code after you apply the transformation.\n\n## Progress\n\n- [x] Convert render function (basic feature)\n- [x] Superclass detection\n  - [x] Support `React.Component`\n  - [x] Support `React.PureComponent`\n- [ ] Class node type\n  - [x] Support class declarations\n  - [x] Support `export default class` declarations\n  - [ ] Support class expressions\n- [ ] TypeScript support\n  - [x] Add `React.FC` annotation\n  - [x] Transform `P` type argument\n  - [x] Transform `S` type argument\n  - [x] Transform ref types\n  - [x] Transform generic components\n  - [x] Modify Props appropriately if defaultProps is present\n  - [ ] Modify Props appropriately if `children` seems to be used\n- [ ] Support for `this.props`\n  - [x] Convert `this.props` to `props` parameter\n  - [ ] Rename `props` if necessary\n  - [x] Hoist expansion of `this.props`\n  - [x] Rename prop variables if necessary\n  - [x] transform `defaultProps`\n- [ ] Support for user-defined methods\n  - [x] Transform methods to `function`s\n  - [x] Transform class fields initialized as functions to `function`s\n  - [x] Use `useCallback` if deemed necessary\n  - [x] Auto-expand direct callback call (like `this.props.onClick()`) to indirect call\n  - [x] Rename methods if necessary\n  - [x] Skip method-binding expressions (e.g. `onClick={this.onClick.bind(this)}`)\n  - [x] Skip method-binding statements (e.g. `this.onClick = this.onClick.bind(this)`)\n- [ ] Support for `this.state`\n  - [x] Decompose `this.state` into `useState` variables\n  - [x] Rename states if necessary\n  - [x] Support updating multiple states at once\n  - [ ] Support functional updates\n  - [ ] Support lazy initialization\n- [ ] Support for refs\n  - [x] Transform `createRef` to `useRef`\n  - [x] Transform member assignment to `useRef`\n  - [ ] Transform legacy string refs as far as possible\n- [ ] Support for lifecycles\n  - [ ] Transform componentDidMount, componentDidUpdate, and componentWillUnmount\n    - [x] Support \"raw\" effects -- simply mapping the three callbacks to guarded effects.\n    - [ ] Support re-pairing effects\n  - [ ] Transform shouldComponentUpdate\n- [ ] Support for receiving refs\n  - [ ] Use `forwardRef` + `useImperativeHandle` when requested by the user\n- [ ] Support for contexts\n  - [ ] Transform `contextType` to `useContext`\n  - [ ] Transform the second parameter for the legacy `contextTypes`\n- [ ] Transform `static propTypes` to assignments\n- [x] Rename local variables in `render` if necessary\n\n## Known limitations\n\n### Class refs\n\n#### Symptom\n\nYou get the following type error:\n\n```\ntest.tsx:1:1 - error TS2322: Type '{ ... }' is not assignable to type 'IntrinsicAttributes \u0026 Props'.\n  Property 'ref' does not exist on type 'IntrinsicAttributes \u0026 Props'.\n\n1 ref={ref}\n  ~~~\n```\n\nor you receive the following warning in the console:\n\n```\nWarning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?\n\nCheck the render method of `C`.\n    at App\n```\n\nor you receive some sort of null error (e.g. `Cannot read properties of undefined (reading 'a')`) because `ref.current` is always undefined.\n\nType errors can also occur at `useRef` in a component that uses the component under transformation:\n\n```\ntest.tsx:1:1 - error TS2749: 'C' refers to a value, but is being used as a type here. Did you mean 'typeof C'?\n\n41 const component = React.useRef\u003cC | null\u003e(null);\n                                  ~\n```\n\n#### Cause\n\nClass components receives refs, and the ref points to the instance of the class. Functional components do not receive refs by default.\n\n#### Solution\n\nThis is not implemented now. However, once it is implemented you can opt in ref support by certain directives. It will generate `forwardRef` + `useImperativeHandle` to expose necessary APIs.\n\n### Stricter render types\n\n### Symptom\n\nYou get the following type error:\n\n```\ntest.tsx:1:1 - error TS2322: Type '(props: Props) =\u003e ReactNode' is not assignable to type 'FC\u003cProps\u003e'.\n  Type 'ReactNode' is not assignable to type 'ReactElement\u003cany, any\u003e | null'.\n\n1 const C: React.FC\u003cProps\u003e = (props) =\u003e {\n        ~\n```\n\n### Cause\n\nIn DefinitelyTyped, `React.FC` is typed slightly stricter than the `render` method. You are expected a single element or `null`.\n\nWe leave this untransformed because it is known not to cause problems at runtime.\n\n### Solution\n\nAn extra layer of a frament `\u003c\u003e ... \u003c/\u003e` suffices to fix the type error.\n\n## Assumptions\n\n- It assumes that the component only needs to reference the latest values of `this.props` or `this.state`. This assumption is necessary because there is a difference between class components and funtion components in how the callbacks capture props or states. To transform the code in an idiomatic way, this assumption is necessary.\n- It assumes, by default, the component is always instantiated without refs.\n- It assumes that the methods always receive the same `this` value as the one when the method is referenced.\n- It assumes that the component does not update the state conditionally by supplying `undefined` to `this.setState`. We need to replace various functionalities associated with `this` with alternative tools and the transformation relies on the fact that the value of `this` is stable all across the class lifecycle.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwantedly%2Freact-declassify","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwantedly%2Freact-declassify","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwantedly%2Freact-declassify/lists"}