GitHub - wantedly/react-declassify: say goodbye to class components
source link: https://github.com/wantedly/react-declassify
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
react-declassify: say goodbye to class components
This codemod automatically transforms React class components into React functional components using Hooks for you!
Before | After |
---|---|
Features
- Supports props, states, methods, and refs.
- Comments, spaces, and styles are preserved thanks to the recast library.
- Designed to generate as idiomatic code as possible. Not something Babel or Webpack would generate!
- Based on classical heuristic automation; no need to be fearful about whimsy LLMs.
Usage
yarn add -D @codemod/cli react-declassify
# OR
npm install -D @codemod/cli react-declassify
npx codemod --plugin react-declassify 'src/**/*.tsx'
Example
Before:
import React from "react";
type Props = {
by: number;
};
type State = {
counter: number;
};
export class C extends React.Component<Props, State> {
static defaultProps = {
by: 1
};
constructor(props) {
super(props);
this.state = {
counter: 0
};
}
render() {
return (
<>
<button onClick={() => this.onClick()}>
{this.state.counter}
</button>
<p>Current step: {this.props.by}</p>
</>
);
}
onClick() {
this.setState({ counter: this.state.counter + this.props.by });
}
}
After:
import React from "react";
type Props = {
by?: number | undefined
};
type State = {
counter: number;
};
export const C: React.FC<Props> = props => {
const {
by = 1
} = props;
const [counter, setCounter] = React.useState<number>(0);
function onClick() {
setCounter(counter + by);
}
return <>
<button onClick={() => onClick()}>
{counter}
</button>
<p>Current step: {by}</p>
</>;
};
Before:
import React from "react";
export class C extends React.Component {
render() {
const { text, color } = this.props;
return <button style={{ color }} onClick={() => this.onClick()}>{text}</button>;
}
onClick() {
const { text, handleClick } = this.props;
alert(`${text} was clicked!`);
handleClick();
}
}
After:
import React from "react";
export const C = props => {
const {
text,
color,
handleClick
} = props;
function onClick() {
alert(`${text} was clicked!`);
handleClick();
}
return <button style={{ color }} onClick={() => onClick()}>{text}</button>;
};
Configuration
Disabling transformation
Adding to the class a comment including react-declassify-disable
will disable transformation of that class.
/* react-declassify-disable */
class MyComponent extends React.Component {}
Marking the component class as abstract
or /** @abstract */
also disables transformation.
Import style
The codemod follows your import style from the extends
clause. So
import React from "react";
class MyComponent extends React.Component {}
is transformed to
import React from "react";
const MyComponent: React.FC = () => {};
whereas
import { Component } from "react";
class MyComponent extends Component {}
is transformed to
import { Component, FC } from "react";
const MyComponent: FC = () => {};
It cannot be configured to mix these styles. For example it cannot emit React.FC
for typing while emitting useState
(not React.useState
) for hooks.
Receiving refs
Class 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.
Syntactic styles
This codemod relies on 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:
const MyComponent: FC = props => {
// ^^^^^ no parentheses
// ...
};
We have no control over this choice. Even if it were possible, allowing configurations on styles would make the codemod unnecessarily complex.
If you need to enforce specific styles, use Prettier or ESLint or whatever is your favorite to reformat the code after you apply the transformation.
Progress
- Convert render function (basic feature)
- Superclass detection
- Support
React.Component
- Support
React.PureComponent
- Support
- Class node type
- Support class declarations
- Support
export default class
declarations - Support class expressions
- TypeScript support
- Add
React.FC
annotation - Transform
P
type argument - Transform
S
type argument - Transform ref types
- Transform generic components
- Modify Props appropriately if defaultProps is present
- Modify Props appropriately if
children
seems to be used
- Add
- Support for
this.props
- Convert
this.props
toprops
parameter - Rename
props
if necessary - Hoist expansion of
this.props
- Rename prop variables if necessary
- transform
defaultProps
- Convert
- Support for user-defined methods
- Transform methods to
function
s - Transform class fields initialized as functions to
function
s - Use
useCallback
if deemed necessary - Auto-expand direct callback call (like
this.props.onClick()
) to indirect call - Rename methods if necessary
- Skip method-binding expressions (e.g.
onClick={this.onClick.bind(this)}
) - Skip method-binding statements (e.g.
this.onClick = this.onClick.bind(this)
)
- Transform methods to
- Support for
this.state
- Decompose
this.state
intouseState
variables - Rename states if necessary
- Support updating multiple states at once
- Support functional updates
- Support lazy initialization
- Decompose
- Support for refs
- Transform
createRef
touseRef
- Transform member assignment to
useRef
- Transform legacy string refs as far as possible
- Transform
- Support for lifecycles
- Transform componentDidMount, componentDidUpdate, and componentWillUnmount
- Support "raw" effects -- simply mapping the three callbacks to guarded effects.
- Support re-pairing effects
- Transform shouldComponentUpdate
- Transform componentDidMount, componentDidUpdate, and componentWillUnmount
- Support for receiving refs
- Use
forwardRef
+useImperativeHandle
when requested by the user
- Use
- Support for contexts
- Transform
contextType
touseContext
- Transform the second parameter for the legacy
contextTypes
- Transform
- Transform
static propTypes
to assignments - Rename local variables in
render
if necessary
Known limitations
Class refs
Symptom
You get the following type error:
test.tsx:1:1 - error TS2322: Type '{ ... }' is not assignable to type 'IntrinsicAttributes & Props'.
Property 'ref' does not exist on type 'IntrinsicAttributes & Props'.
1 ref={ref}
~~~
or you receive the following warning in the console:
Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
Check the render method of `C`.
at App
or you receive some sort of null error (e.g. Cannot read properties of undefined (reading 'a')
) because ref.current
is always undefined.
Type errors can also occur at useRef
in a component that uses the component under transformation:
test.tsx:1:1 - error TS2749: 'C' refers to a value, but is being used as a type here. Did you mean 'typeof C'?
41 const component = React.useRef<C | null>(null);
~
Cause
Class components receives refs, and the ref points to the instance of the class. Functional components do not receive refs by default.
Solution
This 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.
Stricter render types
Symptom
You get the following type error:
test.tsx:1:1 - error TS2322: Type '(props: Props) => ReactNode' is not assignable to type 'FC<Props>'.
Type 'ReactNode' is not assignable to type 'ReactElement<any, any> | null'.
1 const C: React.FC<Props> = (props) => {
~
Cause
In DefinitelyTyped, React.FC
is typed slightly stricter than the render
method. You are expected a single element or null
.
We leave this untransformed because it is known not to cause problems at runtime.
Solution
An extra layer of a frament <> ... </>
suffices to fix the type error.
Assumptions
- It assumes that the component only needs to reference the latest values of
this.props
orthis.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. - It assumes, by default, the component is always instantiated without refs.
- It assumes that the methods always receive the same
this
value as the one when the method is referenced. - It assumes that the component does not update the state conditionally by supplying
undefined
tothis.setState
. We need to replace various functionalities associated withthis
with alternative tools and the transformation relies on the fact that the value ofthis
is stable all across the class lifecycle.
Recommend
-
53
README.md Primer Components Primer React components Status ⚠️ This project is WIP and n...
-
12
“混合双打”之如何在 Class Components 中使用 React Hooks 2020-11-23 发布于
-
3
Tutorial How To Write Class-Based Components with Vue.js and TypeScript Vue.js Introduction
-
5
Nick Scialli • May 14, 2021 • 🚀 4 minute readIf you’re working with React class components, you may find yourself vexed by ‘this.state’ being undefined. The Issue You...
-
3
Full article available on Aviyel for free (leave a like here before you check it out on Aviyel) Hello, developers...
-
6
How To Convert Class Components to Hooks and Modernize Your React Apps ...
-
3
Home ...
-
5
In this article, I will show you how to use React to replace useEffect in most cases. I've been watching "Goodbye, useEffect" by Davi...
-
4
Feb 15th, 2023Potluck × Native Web Components × JS Class Mixins × Application Secrets👇 Download Show
-
9
Say Goodbye to Frustrated Users with React-Network-Notifier! 🚀 ...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK