Skip to content

Commit

Permalink
feat(class-to-functional): support for functional state updates (#86)
Browse files Browse the repository at this point in the history
  • Loading branch information
Boris Litvinsky authored Oct 30, 2019
1 parent 5e9cc5a commit 6c6fa06
Show file tree
Hide file tree
Showing 2 changed files with 154 additions and 15 deletions.
87 changes: 72 additions & 15 deletions src/modules/stateful-to-stateless.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,18 +81,24 @@ export function statefulToStateless(component) {
const buildRequire = template(`
STATE_SETTER(STATE_VALUE);
`);
path.node.arguments[0].properties.forEach(({ key, value }) => {
path.insertBefore(
buildRequire({
STATE_SETTER: t.identifier(
`set${capitalizeFirstLetter(key.name)}`
),
STATE_VALUE: value
})
);

stateProperties.set(key.name, value);
});

if (
t.isFunctionExpression(path.node.arguments[0]) ||
t.isArrowFunctionExpression(path.node.arguments[0])
) {
handleFunctionalStateUpdate(path, buildRequire);
} else {
path.node.arguments[0].properties.forEach(({ key, value }) => {
path.insertBefore(
buildRequire({
STATE_SETTER: t.identifier(
`set${capitalizeFirstLetter(key.name)}`
),
STATE_VALUE: value
})
);
});
}

path.remove();
}
Expand Down Expand Up @@ -322,9 +328,6 @@ export function statefulToStateless(component) {
}

const lifecycleEffectHook = buildEffectHook({ EFFECT: expressions });
// if(!(hasComponentDidUpdate(ast.program.body[0]))){
// lifecycleEffectHook.expression.arguments.push(t.arrayExpression([]));
// }

lifecycleEffectHook.expression.arguments.push(t.arrayExpression([]));

Expand Down Expand Up @@ -355,6 +358,60 @@ export function statefulToStateless(component) {
}
};
}
function handleFunctionalStateUpdate(path: any, buildRequire: any) {
const stateProducer = path.node.arguments[0];
const stateProducerArg = stateProducer.params[0];
const isPrevStateDestructured = t.isObjectPattern(stateProducerArg);
if (!isPrevStateDestructured) {
path.traverse({
Identifier(nestedPath) {
if (nestedPath.listKey === "params") {
nestedPath.scope.bindings.prev.referencePaths.forEach(ref => {
ref.parentPath.replaceWith(ref.container.property);
});
}
}
});
}

let stateUpdates;
if (t.isObjectExpression(stateProducer.body)) {
stateUpdates = stateProducer.body.properties;
} else {
stateUpdates = stateProducer.body.body.find(exp => t.isReturnStatement(exp))
.argument.properties;
}
stateUpdates.forEach(prop => {
const fn = arrowFunction(
[prop.key.name],
[],
[t.returnStatement(t.objectExpression([prop]))]
);

traverse(
fn,
{
Identifier(ss) {
if (ss.node.name === prop.key.name && ss.key !== 'key') {
ss.node.name = `prev${capitalizeFirstLetter(prop.key.name)}`;
}
}
},
path.scope,
path
);

path.insertBefore(
buildRequire({
STATE_SETTER: t.identifier(
`set${capitalizeFirstLetter(prop.key.name)}`
),
STATE_VALUE: fn
})
);
});
}

function arrowFunction(
params: any[],
paramDefaults: any[],
Expand Down
82 changes: 82 additions & 0 deletions src/test/class-to-functional.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,88 @@ describe("when refactoring stateful component into stateless component", () => {
.returns(true);
});

describe("when handling setState call that receives a function", () => {
it("it replaces it with a match state setter hook", async () => {
givenApprovedWarning();
sandbox.stub(editor, "selectedText").returns(`
class SomeComponent extends React.Component {
someMethod() {
this.setState((prev) => ({
foo:prev.foo
}));
}
render() {
return <div />;
}
}
`);

await statefulToStatelessComponent();

expect(fileSystem.replaceTextInFile).to.have.been.calledWith(
"const SomeComponent = props => {\n const someMethod = useCallback(() => {\n setFoo(prevFoo => {\n return {\n foo: prevFoo\n };\n });\n });\n return <div />;\n};",
selectedTextStart,
selectedTextEnd,
"/source.js"
);
});

it("it replaces multiple property updates with multiple state setters", async () => {
givenApprovedWarning();
sandbox.stub(editor, "selectedText").returns(`
class SomeComponent extends React.Component {
someMethod() {
this.setState((prev) => ({
foo:prev.foo,
bar:prev.bar
}));
}
render() {
return <div />;
}
}
`);

await statefulToStatelessComponent();

expect(fileSystem.replaceTextInFile).to.have.been.calledWith(
"const SomeComponent = props => {\n const someMethod = useCallback(() => {\n setFoo(prevFoo => {\n return {\n foo: prevFoo\n };\n });\n setBar(prevBar => {\n return {\n bar: prevBar\n };\n });\n });\n return <div />;\n};",
selectedTextStart,
selectedTextEnd,
"/source.js"
);
});

it("it handles destructring of previous state", async () => {
givenApprovedWarning();
sandbox.stub(editor, "selectedText").returns(`
class SomeComponent extends React.Component {
someMethod() {
this.setState(({foo, bar}) => ({
foo: foo,
bar: bar
}));
}
render() {
return <div />;
}
}
`);

await statefulToStatelessComponent();

expect(fileSystem.replaceTextInFile).to.have.been.calledWith(
"const SomeComponent = props => {\n const someMethod = useCallback(() => {\n setFoo(prevFoo => {\n return {\n foo: prevFoo\n };\n });\n setBar(prevBar => {\n return {\n bar: prevBar\n };\n });\n });\n return <div />;\n};",
selectedTextStart,
selectedTextEnd,
"/source.js"
);
});
});

it("add useState hook for any state variable referenced in the JSX", async () => {
givenApprovedWarning();
sandbox.stub(editor, "selectedText").returns(`
Expand Down

0 comments on commit 6c6fa06

Please sign in to comment.