@samitouri / QOS-React / commits / a807c307c4

[eslint] Check forwardRef callbacks (#17255)

* [eslint] Check forwardRef callbacks (#17220) * [eslint] Make tests more realistic (#17220) * [eslint] Check anonymous callback of React.memo for rules-of-hooks (#17220) * [eslint] Add tests for callbacks not known to be components (#17220) * [eslint] Correct comments and add another test (#17220)

David Garner committed Nov 17, 2019 at 13:39 UTC a807c307c496d96e8ff79f53cf2d6203c45cf0c6
2 files changed +174 -2
packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js
+133
@@ -141,6 +141,46 @@ const tests = {
141 return useHook1(useHook2());
142 }
143 `,
144 + `
145 + // Valid because hooks can be used in anonymous arrow-function arguments
146 + // to forwardRef.
147 + const FancyButton = React.forwardRef((props, ref) => {
148 + useHook();
149 + return <button {...props} ref={ref} />
150 + });
151 + `,
152 + `
153 + // Valid because hooks can be used in anonymous function arguments to
154 + // forwardRef.
155 + const FancyButton = React.forwardRef(function (props, ref) {
156 + useHook();
157 + return <button {...props} ref={ref} />
158 + });
159 + `,
160 + `
161 + // Valid because hooks can be used in anonymous function arguments to
162 + // forwardRef.
163 + const FancyButton = forwardRef(function (props, ref) {
164 + useHook();
165 + return <button {...props} ref={ref} />
166 + });
167 + `,
168 + `
169 + // Valid because hooks can be used in anonymous function arguments to
170 + // React.memo.
171 + const MemoizedFunction = React.memo(props => {
172 + useHook();
173 + return <button {...props} />
174 + });
175 + `,
176 + `
177 + // Valid because hooks can be used in anonymous function arguments to
178 + // memo.
179 + const MemoizedFunction = memo(function (props) {
180 + useHook();
181 + return <button {...props} />
182 + });
183 + `,
184 `
185 // Valid because classes can call functions.
186 // We don't consider these to be hooks.
@@ -262,6 +302,24 @@ const tests = {
302 });
303 }
304 `,
305 + `
306 + // This is valid because "use"-prefixed functions called in
307 + // unnamed function arguments are not assumed to be hooks.
308 + React.unknownFunction((foo, bar) => {
309 + if (foo) {
310 + useNotAHook(bar)
311 + }
312 + });
313 + `,
314 + `
315 + // This is valid because "use"-prefixed functions called in
316 + // unnamed function arguments are not assumed to be hooks.
317 + unknownFunction(function(foo, bar) {
318 + if (foo) {
319 + useNotAHook(bar)
320 + }
321 + });
322 + `,
323 `
324 // Regression test for incorrectly flagged valid code.
325 function RegressionTest() {
@@ -437,6 +495,32 @@ const tests = {
495 `,
496 errors: [genericError('useHookInsideCallback')],
497 },
498 + {
499 + code: `
500 + // Invalid because it's a common misunderstanding.
501 + // We *could* make it valid but the runtime error could be confusing.
502 + const ComponentWithHookInsideCallback = React.forwardRef((props, ref) => {
503 + useEffect(() => {
504 + useHookInsideCallback();
505 + });
506 + return <button {...props} ref={ref} />
507 + });
508 + `,
509 + errors: [genericError('useHookInsideCallback')],
510 + },
511 + {
512 + code: `
513 + // Invalid because it's a common misunderstanding.
514 + // We *could* make it valid but the runtime error could be confusing.
515 + const ComponentWithHookInsideCallback = React.memo(props => {
516 + useEffect(() => {
517 + useHookInsideCallback();
518 + });
519 + return <button {...props} />
520 + });
521 + `,
522 + errors: [genericError('useHookInsideCallback')],
523 + },
524 {
525 code: `
526 // Invalid because it's a common misunderstanding.
@@ -695,6 +779,55 @@ const tests = {
779 // conditionalError('useState'),
780 ],
781 },
782 + {
783 + code: `
784 + // Invalid because it's dangerous and might not warn otherwise.
785 + // This *must* be invalid.
786 + const FancyButton = React.forwardRef((props, ref) => {
787 + if (props.fancy) {
788 + useCustomHook();
789 + }
790 + return <button ref={ref}>{props.children}</button>;
791 + });
792 + `,
793 + errors: [conditionalError('useCustomHook')],
794 + },
795 + {
796 + code: `
797 + // Invalid because it's dangerous and might not warn otherwise.
798 + // This *must* be invalid.
799 + const FancyButton = forwardRef(function(props, ref) {
800 + if (props.fancy) {
801 + useCustomHook();
802 + }
803 + return <button ref={ref}>{props.children}</button>;
804 + });
805 + `,
806 + errors: [conditionalError('useCustomHook')],
807 + },
808 + {
809 + code: `
810 + // Invalid because it's dangerous and might not warn otherwise.
811 + // This *must* be invalid.
812 + const MemoizedButton = memo(function(props) {
813 + if (props.fancy) {
814 + useCustomHook();
815 + }
816 + return <button>{props.children}</button>;
817 + });
818 + `,
819 + errors: [conditionalError('useCustomHook')],
820 + },
821 + {
822 + code: `
823 + // This is invalid because "use"-prefixed functions used in named
824 + // functions are assumed to be hooks.
825 + React.unknownFunction(function notAComponent(foo, bar) {
826 + useProbablyAHook(bar)
827 + });
828 + `,
829 + errors: [functionError('useProbablyAHook', 'notAComponent')],
830 + },
831 {
832 code: `
833 // Invalid because it's dangerous.
packages/eslint-plugin-react-hooks/src/RulesOfHooks.js
+41 -2
@@ -54,6 +54,41 @@ function isComponentName(node) {
54 }
55 }
56
57 +function isReactFunction(node, functionName) {
58 + return (
59 + node.name === functionName ||
60 + (node.type === 'MemberExpression' &&
61 + node.object.name === 'React' &&
62 + node.property.name === functionName)
63 + );
64 +}
65 +
66 +/**
67 + * Checks if the node is a callback argument of forwardRef. This render function
68 + * should follow the rules of hooks.
69 + */
70 +
71 +function isForwardRefCallback(node) {
72 + return !!(
73 + node.parent &&
74 + node.parent.callee &&
75 + isReactFunction(node.parent.callee, 'forwardRef')
76 + );
77 +}
78 +
79 +/**
80 + * Checks if the node is a callback argument of React.memo. This anonymous
81 + * functional component should follow the rules of hooks.
82 + */
83 +
84 +function isMemoCallback(node) {
85 + return !!(
86 + node.parent &&
87 + node.parent.callee &&
88 + isReactFunction(node.parent.callee, 'memo')
89 + );
90 +}
91 +
92 function isInsideComponentOrHook(node) {
93 while (node) {
94 const functionName = getFunctionName(node);
@@ -62,6 +97,9 @@ function isInsideComponentOrHook(node) {
97 return true;
98 }
99 }
100 + if (isForwardRefCallback(node) || isMemoCallback(node)) {
101 + return true;
102 + }
103 node = node.parent;
104 }
105 return false;
@@ -290,7 +328,8 @@ export default {
328 // `undefined` then we know either that we have an anonymous function
329 // expression or our code path is not in a function. In both cases we
330 // will want to error since neither are React function components or
293 - // hook functions.
331 + // hook functions - unless it is an anonymous function argument to
332 + // forwardRef or memo.
333 const codePathFunctionName = getFunctionName(codePathNode);
334
335 // This is a valid code path for React hooks if we are directly in a React
@@ -301,7 +340,7 @@ export default {
340 const isDirectlyInsideComponentOrHook = codePathFunctionName
341 ? isComponentName(codePathFunctionName) ||
342 isHook(codePathFunctionName)
304 - : false;
343 + : isForwardRefCallback(codePathNode) || isMemoCallback(codePathNode);
344
345 // Compute the earliest finalizer level using information from the
346 // cache. We expect all reachable final segments to have a cache entry