Skip to content

[WIP] Refactor: Convert React.Component, React.SFC, React.PureCompoennt to each other #24518

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

Closed
wants to merge 8 commits into from
Closed

[WIP] Refactor: Convert React.Component, React.SFC, React.PureCompoennt to each other #24518

wants to merge 8 commits into from

Conversation

Jack-Works
Copy link
Contributor

@Jack-Works Jack-Works commented May 31, 2018

Related to #15090

This PR adds a refactor to convert SFC-like function to PureComponent or Component to SFC.
This will help one who wants to add lifecycle methods to an SFC component.

Example

class A extends React.PureComponent<T> {
    render() { return this.props.children; }
}
// ^ to v; or v to ^
const A: React.SFC<T> = (props) => props.children;

To-dos

  • Rewrite with new expected behaviour
  • Consider how to deal with other JSX using libraries like Vue, Preact or Mithril...
  • Add tests

Behavior in detail

     * This refactor follows this rule.
     * 0. [ ] Continue if we are in a .jsx or .tsx file.
     * 1. [ ] Get the current JSX config (reactNamespace, jsxFactory, jsx).
     *            Continue if there is a JSX provider
     *            ? It's better be React, ReactNative and Preact ?
     * 2. [ ] Continue if the selection include a SFCLikeDeclaration | ClassLikeDeclaration
     *        [ ] and the declaration is the direct child of the current SourceFile
     *        [ ] and we can find the name (Class name, or variable name) in the declaration
     *
     * For ClassLikeDeclaration detection:
     * A. [ ] Continue if the declaration is a subclass of `PureComponent` or `Component`
     * B. [ ] Continue if there is no reference to `this.state` or `this.setState`
     * C. [ ] Continue if there is no any property more than listed below
     *            [ ] `render`
     *            [ ] static `propTypes`
     *            [ ] static `contextTypes`
     *            [ ] static `defaultProps`
     *            [ ] static `displayName`
     *            [ ] static `childContextTypes`
     * D. [ ] Continue if the `render` method is not invalid
     * E. [ ] Provide an action, Convert `PureComponent` or `Component` to SFC
     *
     * For SFCLikeDeclaration detection:
     * a. [ ] Goto c, if the selection is a function declaration of type `React.SFC`.
     * b. [ ] Continue if we guess the selection is an SFC
     *            In @types/react, ReactNode is =
     *                ReactElement<any> | ReactText (number | string) // ReactChild
     *                | {} | ReactNode[] // ReactFragment
     *                | { key: Key | null; children: ReactNode; } // ReactPortal
     *                | string
     *                | number | boolean | null | undefined
     *         define type MeaningfulReactNode =
     *                     ReactElement<any> | MeaningfulReactNode[] | ReactPortal | string
     *         Guess as follow rules.
     *             [ ] i. Check all return path, make then an union undefined
     *             [ ] ii. Continue if parameters length < 3 (props?, context?)
     *             [ ] iii. Continue if all of member of U is compatiable with ReactNode
     *             [ ] iv. Continue if there is MeaningfulReactNode in U
     *             [ ] v. Continue if there is no reference to `this`
     *             [ ] vi. This is an SFC
     * c. [ ] Continue if there is a body
     * c. [ ] Provide an action, Convert SFC to `PureComponent` or `Component`
     *
     * For ClassLikeDeclaration transformation:
     * A. [ ] Get the Class `C`, get the class name `Name` (by ClassDeclaration, or variable declaration)
     * B. [ ] Collect the properties below
     *            [ ] `render`
     *            [ ] static `propTypes`?
     *            [ ] static `contextTypes`?
     *            [ ] static `defaultProps`?
     *            [ ] static `displayName`?
     *            [ ] static `childContextTypes`?
     * C. [ ] Replace
     *        [ ] all `this.props` to `props` in `render`,
     *        [ ] all `this.context` to `context` in `render`,
     *        [ ] and make sure there is no name conflict in the current lexical scope
     *        [ ] if there is, generate a random name other than `props` and `context`?
     * D. [ ] Create a FunctionDeclaration `F` named `Name`, with body `render`
     *        [ ] In .tsx file, add type annoation
     *        [ ] In .jsx file, add JSDoc Type annoation?
     * E. [ ] For those static properties, add something like `Name`.propTypes = ...
     * F. [ ] Replace `C` with `F`
     *
     * For SFCLikeDeclaration transformation:
     * A. [ ] Get the SFC `F`, get the class name `Name` (by FunctionDeclaration, or variable declaration)
     * B. [ ] Collect the properties below
     *            [ ] function body as `render`
     * C. [ ] Replace
     *        [ ] first parameter to `this.props` in `render`,
     *        [ ] second parameter to `this.context` in `render`,
     * D. [ ] Create a ClassDeclaration `C` named `Name` extends (React|Preact).PureComponent
     *        [ ] In .tsx file, add type arguments `T` if `F` is typed `(React|Preact).(Pure)?Component<T>`
     * E. [ ] Replace `F` with `C`

@@ -4313,5 +4313,13 @@
"Convert named imports to namespace import": {
"category": "Message",
"code": 95057
},
"Covert React.Component to SFC": {
Copy link
Contributor

@mhegazy mhegazy Jun 13, 2018

Choose a reason for hiding this comment

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

nit. Convert component to a Stateless Functional Component (SFC)

"category": "Message",
"code": 95058
},
"Covert React.SFC to PureComponent": {
Copy link
Contributor

@mhegazy mhegazy Jun 13, 2018

Choose a reason for hiding this comment

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

Convert Stateless Functional Component to a statefull component

Copy link
Contributor

Choose a reason for hiding this comment

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

@DanielRosenwasser can you help out with these messages..

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is Pure Component better than stateful component?

Refactor provides an action to PureComponent but not Component, because PureComponent has better performance in default, if developers want to use state, they can just remove Pure.

}
},
getEditsForAction(context: RefactorContext, actionName: string): RefactorEditInfo | undefined {
Debug.assert(actionName === actionNameComponentToSFC || actionName === actionNameSFCToPureComponent);
Copy link
Contributor

Choose a reason for hiding this comment

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

you want to check again for the same conditions to defensively guard against changes to the file state before the actions are requested.

I would extract the code in getAvailableActions to getConvertableReactElemnt that will return you the SFC/Class or undefined if not found.

Copy link
Contributor

Choose a reason for hiding this comment

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

See generateGetAccessorsAndSetAccessors.ts for a sample.

StatelessComponent: undefined,
} as any;
let importClause: ImportClause;
sourcefile.forEachChild(c => {
Copy link
Contributor

Choose a reason for hiding this comment

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

i do not think we should be walking the file as such. first there are easier ways to get the resolved module references, and second, this does not handle cases where anything is aliased.. e.g. reactNamespace, jsxFactory, path mapping in jsconfig.json.. etc..

I would base all the checks on the class/function, and then verify that whatever its import clause is coming from the same location as the JSXFactory.

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 can't get it, about the "easier ways to get the resolved module". I look around for the compiler API, there seemed to be a SourceFile.getNamedDeclarations can get me back all declarations. And in runtime I found it has a property called propertyName can give me the import declarations and its original name. But there is no such type on the type Declaration, it seemd not like the correct way to do it.

And, by using SourceFile.getNamedDeclarations, there is not much difference with I iterate over all children.

const { file } = context;
const span = getRefactorContextSpan(context);
const token = getTokenAtPosition(file, span.start, /*includeJsDocComment*/ false);
const component = getParentNodeInSpan(token, file, span);
Copy link
Contributor

Choose a reason for hiding this comment

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

this will enable the refactor in any location within the declaration. we want to limit it to spans that include the name of the declaration. so first get the token, walk up untill you find a class/class expression or a function/function expression/lambda, verify that the name of the class, or function keyword or class keyword or the name of the const/var on the left hand side is included in the span.

Copy link
Contributor

Choose a reason for hiding this comment

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

see generateGetAccessorAndSetAccessor.ts:: getConvertibleFieldAtPosition for an example.


// function checkJSXFactoryIsReact(host: LanguageServiceHost) {
// const config = host.getCompilationSettings();
// if (config.jsxFactory === undefined) { if (config.jsx) return true; }
Copy link
Contributor

Choose a reason for hiding this comment

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

as i noted erlier, you do not want to limit this to React or the module name "react" there is peact, vue, mithril, etc..

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But this refactor is meaningless for the Vue and Mithril, why don't we limit it to React, ReactNative and Preact?

if (!node.body) { return; }

// TODO: Should also check the actual type insteadof only receive explicit typed
const typeNode = node.type;
Copy link
Contributor

Choose a reason for hiding this comment

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

we should not be relying on type annoation. tsserver support JS experience as well. so these apply to both JS and TS files.

we should isntead try to "guess" if this is an SFC or not..

  • funciton declaration
    • or const declaration with initializer function expression or lambda
  • Zero, one or two parameters
  • function returnns a JSX element (this is gonna be a bit tricky, but we can start by saying the expression of all return statements must be a JSX element).

Copy link
Contributor

Choose a reason for hiding this comment

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

We can go further and say it has to be at the top-level of the file.

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, in todo I'll try to guess if it is an SFC after I done the explicit typed SFC.

context, propTypes, contextTypes, defaultProps and displayName are also in later stage of pr.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

And, hmm, what does it mean by at the top-level of the file? Like it is one of the SourceFile's children?

render = node.body;
}
else if (isExpression(node.body!)) {
render = createBlock([createReturn(node.body)]);
Copy link
Contributor

Choose a reason for hiding this comment

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

do not create a block untill you are applying the edit.

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 got a problem with manipulating the AST and get the textchange, do I do something wrong? Thanks

#24751

}

function isReactSFCDeclaration(node: Node, sourcefile: SourceFile): Component | undefined {
if (!isSFCLikeDeclaration(node)) { return; }
Copy link
Contributor

Choose a reason for hiding this comment

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

combine in one if statement


function isReactSFCDeclaration(node: Node, sourcefile: SourceFile): Component | undefined {
if (!isSFCLikeDeclaration(node)) { return; }
if (node.asteriskToken) { return; }
Copy link
Contributor

Choose a reason for hiding this comment

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

return undefined explicitly

const newNode: FunctionDeclaration = createFunctionDeclaration(
void 0, void 0, void 0, component.name, void 0,
[createParameter(void 0, void 0, void 0, "props", void 0)],
createTypeReferenceNode(react.SFC, component.propsType ? [component.propsType] : undefined),
Copy link

Choose a reason for hiding this comment

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

When I tested this, react.SFC was the string "React.Component". That's not a valid identifier, so it causes assertion failures in the formatter (#24751). Should use an EntityName node instead.
Also, when a node in the old tree is moved to the new tree, you should use getSynthesizedDeepClone to get new nodes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, with your help I can continue working on it

@RyanCavanaugh RyanCavanaugh added this to the Community milestone Sep 17, 2018
@Jack-Works
Copy link
Contributor Author

Due to new concepts in React@next (about 16.7), the complexity of this refator has been more higher. Since the develop has been paused for months and it's out of date, so I decided to close this pull request.

@Jack-Works Jack-Works closed this Oct 26, 2018
@Jessidhia
Copy link

I presume the new concepts is https://reactjs.org/docs/hooks-intro.html ?

@Jack-Works
Copy link
Contributor Author

Yes, and also React.memo( )

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants