@samitouri / QOS-React-2 / commits / 3fd9bd8e74

Add RulesOfHooks support for `use`

Usage of the new `use` hook needs to conform to the rules of hooks, with the one exception that it can be called conditionally. ghstack-source-id: 7ea5beceaf2c080f2c48821b5117bdd0c1194836 Pull Request resolved: https://github.com/facebook/react/pull/25370

Lauren Tan committed Oct 4, 2022 at 11:52 UTC 3fd9bd8e748231f2efb3624ee29f55633c0f3e05
2 files changed +102 -6
packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js
+83 -3
@@ -257,7 +257,6 @@ const tests = {
257 code: normalizeIndent`
258 // Valid because they're not matching use[A-Z].
259 fooState();
260 - use();
260 _use();
261 _useState();
262 use_hook();
@@ -496,8 +495,6 @@ const tests = {
495 },
496 {
497 code: normalizeIndent`
499 - Hook.use();
500 - Hook._use();
498 Hook.useState();
499 Hook._useState();
500 Hook.use42();
@@ -1146,6 +1143,45 @@ if (__EXPERIMENTAL__) {
1143 }
1144 `,
1145 },
1146 + {
1147 + code: normalizeIndent`
1148 + function App() {
1149 + const text = use(Promise.resolve('A'));
1150 + return <Text text={text} />
1151 + }
1152 + `,
1153 + },
1154 + {
1155 + code: normalizeIndent`
1156 + function App() {
1157 + if (shouldShowText) {
1158 + const text = use(query);
1159 + return <Text text={text} />
1160 + }
1161 + return <Text text={shouldFetchBackupText ? use(backupQuery) : "Nothing to see here"} />
1162 + }
1163 + `,
1164 + },
1165 + {
1166 + code: normalizeIndent`
1167 + function App() {
1168 + let data = [];
1169 + for (const query of queries) {
1170 + const text = use(item);
1171 + data.push(text);
1172 + }
1173 + return <Child data={data} />
1174 + }
1175 + `,
1176 + },
1177 + {
1178 + code: normalizeIndent`
1179 + function App() {
1180 + const data = someCallback((x) => use(x));
1181 + return <Child data={data} />
1182 + }
1183 + `,
1184 + },
1185 ];
1186 tests.invalid = [
1187 ...tests.invalid,
@@ -1220,6 +1256,50 @@ if (__EXPERIMENTAL__) {
1256 `,
1257 errors: [useEventError('onClick')],
1258 },
1259 + {
1260 + code: normalizeIndent`
1261 + Hook.use();
1262 + Hook._use();
1263 + Hook.useState();
1264 + Hook._useState();
1265 + Hook.use42();
1266 + Hook.useHook();
1267 + Hook.use_hook();
1268 + `,
1269 + errors: [
1270 + topLevelError('Hook.use'),
1271 + topLevelError('Hook.useState'),
1272 + topLevelError('Hook.use42'),
1273 + topLevelError('Hook.useHook'),
1274 + ],
1275 + },
1276 + {
1277 + code: normalizeIndent`
1278 + function notAComponent() {
1279 + use(promise);
1280 + }
1281 + `,
1282 + errors: [functionError('use', 'notAComponent')],
1283 + },
1284 + {
1285 + code: normalizeIndent`
1286 + const text = use(promise);
1287 + function App() {
1288 + return <Text text={text} />
1289 + }
1290 + `,
1291 + errors: [topLevelError('use')],
1292 + },
1293 + {
1294 + code: normalizeIndent`
1295 + class C {
1296 + m() {
1297 + use(promise);
1298 + }
1299 + }
1300 + `,
1301 + errors: [classError('use')],
1302 + },
1303 ];
1304 }
1305
packages/eslint-plugin-react-hooks/src/RulesOfHooks.js
+19 -3
@@ -16,6 +16,9 @@
16 */
17
18 function isHookName(s) {
19 + if (__EXPERIMENTAL__) {
20 + return s === 'use' || /^use[A-Z0-9]/.test(s);
21 + }
22 return /^use[A-Z0-9]/.test(s);
23 }
24
@@ -107,6 +110,13 @@ function isUseEventIdentifier(node) {
110 return false;
111 }
112
113 +function isUseIdentifier(node) {
114 + if (__EXPERIMENTAL__) {
115 + return node.type === 'Identifier' && node.name === 'use';
116 + }
117 + return false;
118 +}
119 +
120 export default {
121 meta: {
122 type: 'problem',
@@ -458,7 +468,8 @@ export default {
468
469 for (const hook of reactHooks) {
470 // Report an error if a hook may be called more then once.
461 - if (cycled) {
471 + // `use(...)` can be called in loops.
472 + if (cycled && !isUseIdentifier(hook)) {
473 context.report({
474 node: hook,
475 message:
@@ -479,7 +490,11 @@ export default {
490 // path segments.
491 //
492 // Special case when we think there might be an early return.
482 - if (!cycled && pathsFromStartToEnd !== allPathsFromStartToEnd) {
493 + if (
494 + !cycled &&
495 + pathsFromStartToEnd !== allPathsFromStartToEnd &&
496 + !isUseIdentifier(hook) // `use(...)` can be called conditionally.
497 + ) {
498 const message =
499 `React Hook "${context.getSource(hook)}" is called ` +
500 'conditionally. React Hooks must be called in the exact ' +
@@ -525,7 +540,8 @@ export default {
540 // anonymous function expressions. Hopefully this is clarifying
541 // enough in the common case that the incorrect message in
542 // uncommon cases doesn't matter.
528 - if (isSomewhereInsideComponentOrHook) {
543 + // `use(...)` can be called in callbacks.
544 + if (isSomewhereInsideComponentOrHook && !isUseIdentifier(hook)) {
545 const message =
546 `React Hook "${context.getSource(hook)}" cannot be called ` +
547 'inside a callback. React Hooks must be called in a ' +