Skip to content

Commit 63feb87

Browse files
committed
feat: Add codemod to transform string refs to arrow-functions
1 parent 243edf6 commit 63feb87

12 files changed

+299
-0
lines changed

README.md

+70
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,76 @@ guide](https://github.com/airbnb/javascript/blob/7684892951ef663e1c4e62ad57d662e
143143
npx react-codemod sort-comp <path>
144144
```
145145

146+
#### `string-refs`
147+
148+
WARNING: Only apply this codemod if you've fixed all warnings like this:
149+
150+
```
151+
Warning: Component "div" contains the string ref "inner". Support for string refs will be removed in a future major release. We recommend using useRef() or createRef() instead.
152+
```
153+
154+
This codemod will convert deprecated string refs to callback refs.
155+
156+
Input:
157+
158+
```jsx
159+
import * as React from "react";
160+
161+
class ParentComponent extends React.Component {
162+
render() {
163+
return <div ref="refComponent" />;
164+
}
165+
}
166+
```
167+
168+
Output:
169+
170+
```jsx
171+
import * as React from "react";
172+
173+
class ParentComponent extends React.Component {
174+
render() {
175+
return (
176+
<div
177+
ref={(current) => {
178+
this.refs["refComponent"] = current;
179+
}}
180+
/>
181+
);
182+
}
183+
}
184+
```
185+
186+
Note that this only works for string literals.
187+
Referring to the ref with a variable will not trigger the transform:
188+
Input:
189+
190+
```jsx
191+
import * as React from "react";
192+
193+
const refName = "refComponent";
194+
195+
class ParentComponent extends React.Component {
196+
render() {
197+
return <div ref={refName} />;
198+
}
199+
}
200+
```
201+
202+
Output (nothing changed):
203+
204+
```jsx
205+
import * as React from "react";
206+
207+
const refName = "refComponent";
208+
209+
class ParentComponent extends React.Component {
210+
render() {
211+
return <div ref={refName} />;
212+
}
213+
}
214+
```
215+
146216
#### `update-react-imports`
147217

148218
[As of Babel 7.9.0](https://babeljs.io/blog/2020/03/16/7.9.0#a-new-jsx-transform-11154-https-githubcom-babel-babel-pull-11154), when using `runtime: automatic` in `@babel/preset-react` or `@babel/plugin-transform-react-jsx`, you will not need to explicitly import React for compiling jsx. This codemod removes the redundant import statements. It also converts default imports (`import React from 'react'`) to named imports (e.g. `import { useState } from 'react'`).

bin/cli.js

+5
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,11 @@ const TRANSFORMER_INQUIRER_CHOICES = [
188188
'Reorders React component methods to match the ESLint react/sort-comp rule.',
189189
value: 'sort-comp'
190190
},
191+
{
192+
name:
193+
'string-refs: Converts deprecated string refs to callback refs.',
194+
value: 'string-refs'
195+
},
191196
{
192197
name: 'update-react-imports: Removes redundant import statements from explicitly importing React to compile JSX and converts default imports to destructured named imports',
193198
value: 'update-react-imports',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import * as React from "react";
2+
3+
class ParentComponent extends React.Component {
4+
render() {
5+
return (
6+
<div ref="P" id="P">
7+
<div ref="P_P1" id="P_P1">
8+
<span ref="P_P1_C1" id="P_P1_C1" />
9+
<span ref="P_P1_C2" id="P_P1_C2" />
10+
</div>
11+
<div ref="P_OneOff" id="P_OneOff" />
12+
</div>
13+
);
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import * as React from "react";
2+
3+
class ParentComponent extends React.Component {
4+
render() {
5+
return (
6+
<div ref={current => {
7+
this.refs['P'] = current;
8+
}} id="P">
9+
<div ref={current => {
10+
this.refs['P_P1'] = current;
11+
}} id="P_P1">
12+
<span ref={current => {
13+
this.refs['P_P1_C1'] = current;
14+
}} id="P_P1_C1" />
15+
<span ref={current => {
16+
this.refs['P_P1_C2'] = current;
17+
}} id="P_P1_C2" />
18+
</div>
19+
<div ref={current => {
20+
this.refs['P_OneOff'] = current;
21+
}} id="P_OneOff" />
22+
</div>
23+
);
24+
}
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import * as React from "react";
2+
3+
<div ref="bad" />;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import * as React from "react";
2+
3+
<div ref={current => {
4+
this.refs['bad'] = current;
5+
}} />;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import * as React from "react";
2+
3+
class ParentComponent extends React.Component {
4+
// Actual code probably has more accurate types.
5+
// Codemod might cause TypeScript errors but these are good errors since they reveal unsound code.
6+
refs: Record<string, any>;
7+
8+
render() {
9+
return (
10+
<div ref="P" id="P">
11+
<div ref="P_P1" id="P_P1">
12+
<span ref="P_P1_C1" id="P_P1_C1" />
13+
<span ref="P_P1_C2" id="P_P1_C2" />
14+
</div>
15+
<div ref="P_OneOff" id="P_OneOff" />
16+
</div>
17+
);
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import * as React from "react";
2+
3+
class ParentComponent extends React.Component {
4+
// Actual code probably has more accurate types.
5+
// Codemod might cause TypeScript errors but these are good errors since they reveal unsound code.
6+
refs: Record<string, any>;
7+
8+
render() {
9+
return (
10+
<div ref={current => {
11+
this.refs['P'] = current;
12+
}} id="P">
13+
<div ref={current => {
14+
this.refs['P_P1'] = current;
15+
}} id="P_P1">
16+
<span ref={current => {
17+
this.refs['P_P1_C1'] = current;
18+
}} id="P_P1_C1" />
19+
<span ref={current => {
20+
this.refs['P_P1_C2'] = current;
21+
}} id="P_P1_C2" />
22+
</div>
23+
<div ref={current => {
24+
this.refs['P_OneOff'] = current;
25+
}} id="P_OneOff" />
26+
</div>
27+
);
28+
}
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import * as React from "react";
2+
3+
class ParentComponent extends React.Component {
4+
render() {
5+
const refName = "P";
6+
// Giving up. Would need to implement scope tracking.
7+
return <div ref={refName} id="P"></div>;
8+
}
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import * as React from "react";
2+
3+
class ParentComponent extends React.Component {
4+
render() {
5+
const refName = "P";
6+
// Giving up. Would need to implement scope tracking.
7+
return <div ref={refName} id="P"></div>;
8+
}
9+
}
+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* Copyright 2015-present, Facebook, Inc.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
*/
8+
9+
"use strict";
10+
11+
const flowTests = [
12+
"literal-with-owner",
13+
"literal-without-owner",
14+
"value-with-owner",
15+
];
16+
17+
const typescriptTests = ["literal-with-owner"];
18+
19+
const defineTest = require("jscodeshift/dist/testUtils").defineTest;
20+
21+
describe("string-refs", () => {
22+
describe("flow", () => {
23+
flowTests.forEach((test) =>
24+
defineTest(__dirname, "string-refs", null, `string-refs/${test}`, {
25+
parser: "flow",
26+
})
27+
);
28+
});
29+
30+
describe("typescript", () => {
31+
typescriptTests.forEach((test) =>
32+
defineTest(
33+
__dirname,
34+
"string-refs",
35+
null,
36+
`string-refs/typescript/${test}`,
37+
{ parser: "tsx" }
38+
)
39+
);
40+
});
41+
});

transforms/string-refs.js

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* Copyright 2015-present, Facebook, Inc.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
*/
8+
9+
"use strict";
10+
11+
export default (file, api, options) => {
12+
const j = api.jscodeshift;
13+
14+
const printOptions = options.printOptions || {
15+
quote: "single",
16+
trailingComma: true,
17+
};
18+
19+
const root = j(file.source);
20+
21+
let hasModifications = false;
22+
23+
root
24+
.find(j.JSXAttribute, (node) => {
25+
return node.name.name === "ref";
26+
})
27+
.forEach((jsxAttributePath) => {
28+
const valuePath = jsxAttributePath.get("value");
29+
if (
30+
// Flow parser
31+
valuePath.value.type === "Literal" ||
32+
// TSX parser
33+
valuePath.value.type === "StringLiteral"
34+
) {
35+
hasModifications = true;
36+
// This might shadow existing variables.
37+
// But this should be safe since we control what identifiers we're reading in this block.
38+
// It will trigger ESLint's `no-shadow` though.
39+
// Babel has a helper to get a identifier that doesn't shadow existing vars.
40+
// Maybe JSCodeShift has such a helper as well?
41+
const currentIdentifierName = "current";
42+
valuePath.replace(
43+
// {(current) => { this.refs[valuePath.node.value] = current }}
44+
j.jsxExpressionContainer(
45+
j.arrowFunctionExpression(
46+
[j.identifier(currentIdentifierName)],
47+
j.blockStatement([
48+
j.expressionStatement(
49+
j.assignmentExpression(
50+
"=",
51+
j.memberExpression(
52+
j.memberExpression(
53+
j.thisExpression(),
54+
j.identifier("refs")
55+
),
56+
j.literal(valuePath.node.value)
57+
),
58+
j.identifier(currentIdentifierName)
59+
)
60+
),
61+
])
62+
)
63+
)
64+
);
65+
}
66+
});
67+
68+
return hasModifications ? root.toSource(printOptions) : file.source;
69+
};

0 commit comments

Comments
 (0)