Skip to content

Commit 375fb92

Browse files
committed
Support updating multiple states
1 parent 11a4e79 commit 375fb92

File tree

6 files changed

+84
-28
lines changed

6 files changed

+84
-28
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
- Added
44
- Support for method-binding patterns e.g. `this.foo = this.foo.bind(this);`
5+
- Support for multiple states in one `setState()` call
56

67
## 0.1.8
78

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ If you need to enforce specific styles, use Prettier or ESLint or whatever is yo
247247
- [ ] Support for `this.state`
248248
- [x] Decompose `this.state` into `useState` variables
249249
- [x] Rename states if necessary
250-
- [ ] Support updating multiple states at once
250+
- [x] Support updating multiple states at once
251251
- [ ] Support functional updates
252252
- [ ] Support lazy initialization
253253
- [ ] Support for refs

src/analysis.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export type {
2020
} from "./analysis/pre.js";
2121
export { preanalyzeClass } from "./analysis/pre.js";
2222
export type { LocalManager } from "./analysis/local.js";
23-
export type { StateObjAnalysis } from "./analysis/state.js";
23+
export type { StateObjAnalysis, SetStateSite, SetStateFieldSite } from "./analysis/state.js";
2424
export { needAlias } from "./analysis/prop.js";
2525
export type { PropsObjAnalysis } from "./analysis/prop.js";
2626

src/analysis/state.ts

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { trackMember } from "./track_member.js";
99

1010
export type StateObjAnalysis = {
1111
states: Map<string, StateAnalysis>;
12+
setStateSites: SetStateSite[];
1213
};
1314

1415
export type StateAnalysis = {
@@ -19,7 +20,7 @@ export type StateAnalysis = {
1920
sites: StateSite[];
2021
};
2122

22-
export type StateSite = StateInitSite | StateExprSite | SetStateSite;
23+
export type StateSite = StateInitSite | StateExprSite;
2324

2425
export type StateInitSite = {
2526
type: "state_init";
@@ -31,9 +32,14 @@ export type StateExprSite = {
3132
path: NodePath<Expression>;
3233
owner: string | undefined;
3334
};
35+
3436
export type SetStateSite = {
35-
type: "setState";
3637
path: NodePath<CallExpression>;
38+
fields: SetStateFieldSite[];
39+
}
40+
41+
export type SetStateFieldSite = {
42+
name: string;
3743
valuePath: NodePath<Expression>;
3844
}
3945

@@ -120,6 +126,7 @@ export function analyzeState(
120126
throw new AnalysisError(`Non-analyzable this.state`);
121127
}
122128
}
129+
const setStateSites: SetStateSite[] = [];
123130
for (const site of setStateAnalysis.sites) {
124131
if (site.type !== "expr" || site.hasWrite) {
125132
throw new AnalysisError(`Invalid use of this.setState`);
@@ -135,22 +142,25 @@ export function analyzeState(
135142
const arg0 = args[0]!;
136143
if (arg0.isObjectExpression()) {
137144
const props = arg0.get("properties");
138-
if (props.length !== 1) {
139-
throw new AnalysisError(`Multiple assignments in setState is not yet supported`);
140-
}
141-
const prop0 = props[0]!;
142-
if (!prop0.isObjectProperty()) {
143-
throw new AnalysisError(`Non-analyzable setState`);
144-
}
145-
const setStateName = memberName(prop0.node);
146-
if (setStateName == null) {
147-
throw new AnalysisError(`Non-analyzable setState name`);
145+
const fields: SetStateFieldSite[] = [];
146+
for (const prop of props) {
147+
if (!prop.isObjectProperty()) {
148+
throw new AnalysisError(`Non-analyzable setState`);
149+
}
150+
const setStateName = memberName(prop.node);
151+
if (setStateName == null) {
152+
throw new AnalysisError(`Non-analyzable setState name`);
153+
}
154+
// Ensure the state exists
155+
getState(setStateName);
156+
fields.push({
157+
name: setStateName,
158+
valuePath: prop.get("value") as NodePath<Expression>,
159+
});
148160
}
149-
const state = getState(setStateName);
150-
state.sites.push({
151-
type: "setState",
161+
setStateSites.push({
152162
path: gpPath,
153-
valuePath: prop0.get("value") as NodePath<Expression>,
163+
fields,
154164
});
155165
} else {
156166
throw new AnalysisError(`Non-analyzable setState`);
@@ -185,5 +195,5 @@ export function analyzeState(
185195
}
186196
state.init = state.sites.find((site): site is StateInitSite => site.type === "state_init");
187197
}
188-
return { states };
198+
return { states, setStateSites };
189199
}

src/index.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -832,6 +832,36 @@ describe("react-declassify", () => {
832832
expect(transform(input)).toBe(output);
833833
});
834834

835+
it("transforms multi-value assignments", () => {
836+
const input = dedent`\
837+
class C extends React.Component {
838+
render() {
839+
return <button onClick={() => this.setState({ foo: 3, bar: 4 })} />
840+
}
841+
frob() {
842+
this.setState({
843+
foo: 1,
844+
bar: 2,
845+
});
846+
}
847+
}
848+
`;
849+
const output = dedent`\
850+
const C = () => {
851+
const [foo, setFoo] = React.useState();
852+
const [bar, setBar] = React.useState();
853+
854+
function frob() {
855+
setFoo(1);
856+
setBar(2);
857+
}
858+
859+
return <button onClick={() => (setFoo(3), setBar(4))} />;
860+
};
861+
`;
862+
expect(transform(input)).toBe(output);
863+
});
864+
835865
it("transforms state types (type alias)", () => {
836866
const input = dedent`\
837867
type Props = {};

src/index.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { ArrowFunctionExpression, ClassMethod, ClassPrivateMethod, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, ObjectMethod, Pattern, RestElement, Statement, TSEntityName, TSType, TSTypeAnnotation, TSTypeParameterDeclaration, VariableDeclaration } from "@babel/types";
22
import type { NodePath, PluginObj, PluginPass } from "@babel/core";
33
import { assignReturnType, assignTypeAnnotation, assignTypeArguments, assignTypeParameters, importName, isTS, nonNullPath } from "./utils.js";
4-
import { AnalysisError, analyzeClass, preanalyzeClass, AnalysisResult, PreAnalysisResult, needsProps, LibRef, needAlias } from "./analysis.js";
4+
import { AnalysisError, analyzeClass, preanalyzeClass, AnalysisResult, PreAnalysisResult, needsProps, LibRef, needAlias, SetStateFieldSite } from "./analysis.js";
55

66
type Options = {};
77

@@ -147,17 +147,32 @@ function transformClass(analysis: AnalysisResult, options: { ts: boolean }, babe
147147
if (site.type === "expr") {
148148
// this.state.foo -> foo
149149
site.path.replaceWith(t.identifier(stateAnalysis.localName!));
150-
} else if (site.type === "setState") {
151-
// this.setState({ foo: 1 }) -> setFoo(1)
152-
site.path.replaceWith(
153-
t.callExpression(
154-
t.identifier(stateAnalysis.localSetterName!),
155-
[site.valuePath.node]
156-
)
157-
);
158150
}
159151
}
160152
}
153+
for (const site of analysis.state.setStateSites) {
154+
function setter(field: SetStateFieldSite) {
155+
// this.setState({ foo: 1 }) -> setFoo(1)
156+
return t.callExpression(
157+
t.identifier(analysis.state.states.get(field.name)!.localSetterName!),
158+
[field.valuePath.node]
159+
);
160+
}
161+
if (site.fields.length === 1) {
162+
const field = site.fields[0]!;
163+
site.path.replaceWith(setter(field));
164+
} else if (site.path.parentPath.isExpressionStatement()) {
165+
site.path.parentPath.replaceWithMultiple(
166+
site.fields.map((field) =>
167+
t.expressionStatement(setter(field))
168+
)
169+
);
170+
} else if (site.fields.length === 0) {
171+
site.path.replaceWith(t.nullLiteral());
172+
} else {
173+
site.path.replaceWith(t.sequenceExpression(site.fields.map(setter)));
174+
}
175+
}
161176
for (const [, field] of analysis.userDefined.fields) {
162177
if (field.type === "user_defined_function" || field.type === "user_defined_ref") {
163178
for (const site of field.sites) {

0 commit comments

Comments
 (0)