Skip to content

Commit f399b84

Browse files
author
Boris Litvinsky
committed
feat(class-to-functional): added support for converting class properties into useRef. Closes #77
1 parent 44879eb commit f399b84

File tree

2 files changed

+135
-42
lines changed

2 files changed

+135
-42
lines changed

src/modules/stateful-to-stateless.ts

Lines changed: 110 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ const buildStateHook = template(`
2929
const [STATE_PROP, STATE_SETTER] = useState(STATE_VALUE);
3030
`);
3131

32+
const buildRefHook = template(`
33+
const VAR_NAME = useRef(INITIAL_VALUE);
34+
`);
35+
3236
const buildEffectHook = template(`
3337
useEffect(() => { EFFECT });
3438
`);
@@ -40,11 +44,37 @@ useCallback(CALLBACK);
4044
export function statefulToStateless(component) {
4145
const functionBody = [];
4246
const stateProperties = new Map();
43-
47+
const refProperties = new Map();
4448
const RemoveThisVisitor = {
4549
MemberExpression(path) {
46-
if (t.isThisExpression(path.node.object)) {
47-
path.replaceWith(path.node.property);
50+
if (path.node.wasVisited || path.shouldSkip) return;
51+
if (
52+
isHooksForFunctionalComponentsExperimentOn() &&
53+
path.key !== "callee"
54+
) {
55+
if (
56+
t.isIdentifier(path.node.property) &&
57+
!["state", "props"].includes(path.node.property.name)
58+
) {
59+
if (!refProperties.has(path.node.property.name)) {
60+
refProperties.set(path.node.property.name, undefined);
61+
}
62+
}
63+
64+
const replacement = t.memberExpression(
65+
t.identifier(path.node.property.name),
66+
t.identifier("current")
67+
);
68+
69+
(replacement as any).wasVisited = true;
70+
71+
path.replaceWith(replacement);
72+
73+
path.skip();
74+
} else {
75+
if (t.isThisExpression(path.node.object)) {
76+
path.replaceWith(path.node.property);
77+
}
4878
}
4979
}
5080
};
@@ -75,48 +105,41 @@ export function statefulToStateless(component) {
75105

76106
const RemoveSetStateAndForceUpdateVisitor = {
77107
CallExpression(path) {
78-
if (t.isMemberExpression(path.node.callee)) {
79-
if (t.isThisExpression(path.node.callee.object)) {
80-
if (isHooksForFunctionalComponentsExperimentOn()) {
81-
if (path.node.callee.property.name === "forceUpdate") {
82-
path.remove();
83-
} else if (path.node.callee.property.name === "setState") {
84-
const buildRequire = template(`
108+
if (
109+
t.isMemberExpression(path.node.callee) &&
110+
t.isThisExpression(path.node.callee.object)
111+
) {
112+
if (isHooksForFunctionalComponentsExperimentOn()) {
113+
if (path.node.callee.property.name === "forceUpdate") {
114+
path.remove();
115+
} else if (path.node.callee.property.name === "setState") {
116+
const buildRequire = template(`
85117
STATE_SETTER(STATE_VALUE);
86118
`);
87119

88-
if (
89-
t.isFunctionExpression(path.node.arguments[0]) ||
90-
t.isArrowFunctionExpression(path.node.arguments[0])
91-
) {
92-
handleFunctionalStateUpdate(path, buildRequire, stateProperties);
93-
} else {
94-
path.node.arguments[0].properties.forEach(({ key, value }) => {
95-
path.insertBefore(
96-
buildRequire({
97-
STATE_SETTER: t.identifier(
98-
`set${capitalizeFirstLetter(key.name)}`
99-
),
100-
STATE_VALUE: value
101-
})
102-
);
103-
104-
if (!stateProperties.has(key.name)) {
105-
stateProperties.set(key.name, void 0);
106-
}
107-
});
108-
}
109-
110-
path.remove();
111-
}
112-
} else {
113-
if (
114-
["setState", "forceUpdate"].indexOf(
115-
path.node.callee.property.name
116-
) !== -1
117-
) {
118-
path.remove();
120+
if (isStateChangedThroughFunction(path.node.arguments[0])) {
121+
covertStateChangeThroughFunction(
122+
path,
123+
buildRequire,
124+
stateProperties
125+
);
126+
} else {
127+
convertStateChangeThroughObject(
128+
path,
129+
buildRequire,
130+
stateProperties
131+
);
119132
}
133+
134+
path.remove();
135+
}
136+
} else {
137+
if (
138+
["setState", "forceUpdate"].indexOf(
139+
path.node.callee.property.name
140+
) !== -1
141+
) {
142+
path.remove();
120143
}
121144
}
122145
}
@@ -288,6 +311,8 @@ export function statefulToStateless(component) {
288311
t.isArrowFunctionExpression(propValue)
289312
) {
290313
copyNonLifeCycleMethods(path);
314+
} else {
315+
refProperties.set(path.node.key.name, path.node.value);
291316
}
292317
if (t.isObjectExpression(propValue) && path.node.key.name === "state") {
293318
(propValue.properties as t.ObjectProperty[]).map(({ key, value }) => {
@@ -319,6 +344,17 @@ export function statefulToStateless(component) {
319344
traverse(ast, visitor);
320345

321346
if (isHooksForFunctionalComponentsExperimentOn()) {
347+
const refHookExpression = Array.from(refProperties).map(
348+
([key, defaultValue]) => {
349+
return buildRefHook({
350+
VAR_NAME: t.identifier(key),
351+
INITIAL_VALUE: defaultValue
352+
});
353+
}
354+
);
355+
356+
functionBody.unshift(...refHookExpression);
357+
322358
if (effectBody || effectTeardown) {
323359
const expressions = [];
324360
if (effectBody) {
@@ -358,11 +394,41 @@ export function statefulToStateless(component) {
358394
text: processedJSX,
359395
metadata: {
360396
stateHooksPresent: stateProperties.size > 0,
397+
refHooksPresent: refProperties.size > 0,
361398
nonLifeycleMethodsPresent
362399
}
363400
};
364401
}
365-
function handleFunctionalStateUpdate(path: any, buildRequire: any, stateProperties) {
402+
function isStateChangedThroughFunction(setStateArg: any) {
403+
return (
404+
t.isFunctionExpression(setStateArg) ||
405+
t.isArrowFunctionExpression(setStateArg)
406+
);
407+
}
408+
409+
function convertStateChangeThroughObject(
410+
path: any,
411+
buildRequire: any,
412+
stateProperties: Map<any, any>
413+
) {
414+
path.node.arguments[0].properties.forEach(({ key, value }) => {
415+
path.insertBefore(
416+
buildRequire({
417+
STATE_SETTER: t.identifier(`set${capitalizeFirstLetter(key.name)}`),
418+
STATE_VALUE: value
419+
})
420+
);
421+
if (!stateProperties.has(key.name)) {
422+
stateProperties.set(key.name, void 0);
423+
}
424+
});
425+
}
426+
427+
function covertStateChangeThroughFunction(
428+
path: any,
429+
buildRequire: any,
430+
stateProperties
431+
) {
366432
const stateProducer = path.node.arguments[0];
367433
const stateProducerArg = stateProducer.params[0];
368434
const isPrevStateDestructured = t.isObjectPattern(stateProducerArg);
@@ -494,10 +560,12 @@ export async function statefulToStatelessComponent() {
494560

495561
const {
496562
stateHooksPresent,
563+
refHooksPresent,
497564
nonLifeycleMethodsPresent
498565
} = selectionProccessingResult.metadata;
499566
const usedHooks = [
500567
...(stateHooksPresent ? ["useState"] : []),
568+
...(refHooksPresent ? ["useRef"] : []),
501569
...(nonLifeycleMethodsPresent ? ["useCallback"] : [])
502570
];
503571

src/test/class-to-functional.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,31 @@ describe("when refactoring stateful component into stateless component", () => {
270270
.returns(true);
271271
});
272272

273+
describe('when handling class properties', () => {
274+
it("it replaces it with a match state setter hook", async () => {
275+
givenApprovedWarning();
276+
sandbox.stub(editor, "selectedText").returns(`
277+
class SomeComponent extends React.Component {
278+
foo = 3;
279+
someMethod() {
280+
this.foo = 4
281+
}
282+
render() {
283+
return <div />;
284+
}
285+
}
286+
`);
287+
288+
await statefulToStatelessComponent();
289+
290+
expect(fileSystem.replaceTextInFile).to.have.been.calledWith(
291+
"const SomeComponent = props => {\n const foo = useRef(3);\n const someMethod = useCallback(() => {\n foo.current = 4;\n });\n return <div />;\n};", selectedTextStart,
292+
selectedTextEnd,
293+
"/source.js"
294+
);
295+
});
296+
});
297+
273298
describe("when handling setState call that receives a function", () => {
274299
it("it replaces it with a match state setter hook", async () => {
275300
givenApprovedWarning();

0 commit comments

Comments
 (0)