Skip to content

Commit 5c451b8

Browse files
authored
Merge pull request #5 from Gelio/develop
Release v1.1.0
2 parents b807956 + 099610e commit 5c451b8

12 files changed

+154
-27
lines changed

.npmignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ tsconfig.json
88
tslint.json
99
.prettierrc
1010
.editorconfig
11+
CONTRIBUTING.md

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
11
# Changelog
22

3+
## v1.1.0 (2019-02-09)
4+
5+
- Allow using hooks inside React component decorators, such as `React.memo` or `React.forwardRef`.
6+
7+
For example, the following code sample now **does not** violate the rule:
8+
9+
```tsx
10+
const MyComponent = React.memo(props => {
11+
useEffect(() => {
12+
console.log('Counter changed');
13+
}, [props.value]);
14+
15+
return <div>Counter: {props.value}</div>;
16+
});
17+
```
18+
319
## v1.0.1 (2019-02-03)
420

521
- Updated README

CONTRIBUTING.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Contributing guidelines
2+
3+
Anyone is welcome to contribute to this project. Filing issues with detected bugs or false positives
4+
is appreciated, as is opening pull requests with fixes to those.
5+
6+
Before opening a pull request, make sure that all the tests pass by running
7+
8+
```sh
9+
npm run test
10+
```
11+
12+
It would be great if you added new test cases that cover the functionality you added/fixed in the
13+
pull request.
14+
15+
## Information for developers
16+
17+
Creating TSLint rules may seem complex in the beginning, but it is actually pretty easy once you
18+
understand the underlying concepts.
19+
20+
Each Typescript file is parsed into an Abstract Syntax Tree (AST). It is a tree structure that
21+
contains nodes like statements, expressions, identifier that appear in the file being linted. The
22+
rule can then analyze the AST and report rule violations.
23+
24+
Check the [TSLint documentation for developing TSLint rules](https://palantir.github.io/tslint/develop/custom-rules/)
25+
for more information on the subject.
26+
27+
[AST Explorer](https://astexplorer.net/) is an extremely useful tool when working with and exploring ASTs. It
28+
supports different parsers, so when developing TSLint rules make sure to pick the _typescript_
29+
parser from the settings bar at the top.

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "tslint-react-hooks",
3-
"version": "1.0.1",
3+
"version": "1.1.0",
44
"description": "TSLint rule that enforces the Rules of Hooks",
55
"main": "tslint-react-hooks.json",
66
"scripts": {

src/react-hooks-nesting-walker/is-binary-conditional-expression.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,5 @@ export function isBinaryConditionalExpression(
1717
return false;
1818
}
1919

20-
return binaryConditionalOperators.indexOf(node.operatorToken.kind) !== -1;
20+
return binaryConditionalOperators.includes(node.operatorToken.kind);
2121
}
Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,17 @@
1-
import {
2-
CallExpression,
3-
isPropertyAccessExpression,
4-
isIdentifier
5-
} from 'typescript';
1+
import { CallExpression } from 'typescript';
62

73
import { isHookIdentifier } from './is-hook-identifier';
4+
import { isReactApiExpression } from './is-react-api-expression';
85

96
/**
10-
* Tests is a `CallExpression` calls a React Hook
7+
* Tests if a `CallExpression` calls a React Hook
118
* @see https://github.com/facebook/react/blob/master/packages/eslint-plugin-react-hooks/src/RulesOfHooks.js#L26
129
*/
1310
export function isHookCall({ expression }: CallExpression) {
14-
if (isIdentifier(expression)) {
15-
/**
16-
* Test for direct `useHook` calls
17-
*/
18-
return isHookIdentifier(expression);
19-
} else if (isPropertyAccessExpression(expression)) {
20-
/**
21-
* Test for `React.useHook` calls
22-
*/
23-
return (
24-
isIdentifier(expression.expression) &&
25-
expression.expression.text === 'React' &&
26-
isHookIdentifier(expression.name)
27-
);
28-
}
29-
30-
return false;
11+
return isHookAccessExpression(expression);
3112
}
13+
14+
/**
15+
* Tests for `useHook` or `React.useHook` calls
16+
*/
17+
const isHookAccessExpression = isReactApiExpression(isHookIdentifier);
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import {
2+
Identifier,
3+
isPropertyAccessExpression,
4+
isIdentifier,
5+
Expression,
6+
} from 'typescript';
7+
8+
export type Predicate<T> = (t: T) => boolean;
9+
10+
/**
11+
* Tests whether an `Expression` is an identifier that matches a predicate. Accepts also property
12+
* access of that identifier from React's top-level API.
13+
*
14+
* @example
15+
* const isForwardRef = isReactApiExpression((identifier) => identifier.text === 'forwardRef');
16+
* // would match `isForwardRef` or `React.isForwardRef`
17+
* const matches = isForwardRef(node);
18+
*
19+
* @param predicate Predicate that is run on the actual identifier.
20+
*/
21+
export const isReactApiExpression = (predicate: Predicate<Identifier>) => (
22+
expression: Expression,
23+
) => {
24+
if (isIdentifier(expression)) {
25+
return predicate(expression);
26+
} else if (isPropertyAccessExpression(expression)) {
27+
return (
28+
isIdentifier(expression.expression) &&
29+
expression.expression.text === 'React' &&
30+
predicate(expression.name)
31+
);
32+
}
33+
34+
return false;
35+
};
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { isReactApiExpression } from './is-react-api-expression';
2+
3+
const reactComponentDecorators = ['forwardRef', 'memo'];
4+
5+
/**
6+
* Tests is an expression is a React top-level API component decorator (e.g. `React.forwardRef`,
7+
* `React.memo`)
8+
*/
9+
export const isReactComponentDecorator = isReactApiExpression(identifier =>
10+
reactComponentDecorators.includes(identifier.text),
11+
);

src/react-hooks-nesting-walker/react-hooks-nesting-walker.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@ import {
1313
isIdentifier,
1414
isSourceFile,
1515
isClassDeclaration,
16+
isCallExpression,
1617
} from 'typescript';
1718

1819
import { isHookCall } from './is-hook-call';
1920
import { ERROR_MESSAGES } from './error-messages';
2021
import { isBinaryConditionalExpression } from './is-binary-conditional-expression';
2122
import { isComponentOrHookIdentifier } from './is-component-or-hook-identifier';
23+
import { isReactComponentDecorator } from './is-react-component-decorator';
2224

2325
export class ReactHooksNestingWalker extends RuleWalker {
2426
public visitCallExpression(node: CallExpression) {
@@ -108,6 +110,16 @@ export class ReactHooksNestingWalker extends RuleWalker {
108110
return;
109111
}
110112

113+
/**
114+
* Allow using hooks when the function is passed to `React.memo` or `React.forwardRef`
115+
*/
116+
if (
117+
isCallExpression(ancestor.parent) &&
118+
isReactComponentDecorator(ancestor.parent.expression)
119+
) {
120+
return;
121+
}
122+
111123
// Disallow using hooks inside other kinds of functions
112124
this.addFailureAtNode(hookNode, ERROR_MESSAGES.invalidFunctionExpression);
113125
return;
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Tests for React top level APIs that wrap components
2+
3+
import React, { memo, forwardRef } from 'react';
4+
5+
// Hooks should be allowed inside functions passed to React.memo and React.forwardRef
6+
7+
const MemoizedComponent = React.memo(() => {
8+
const ref = useRef();
9+
10+
return <div>Memoized</div>;
11+
});
12+
13+
const RefForwardingComponent = React.forwardRef((props, ref) => {
14+
useEffect(() => {
15+
console.log('Forwarding refs');
16+
});
17+
18+
return <div ref={ref}>Forwarding refs</div>;
19+
});
20+
21+
22+
// The same as above, but not using the `React` prefix
23+
24+
const MemoizedComponent = memo(() => {
25+
const ref = useRef();
26+
27+
return <div>Memoized</div>;
28+
});
29+
30+
const RefForwardingComponent = forwardRef((props, ref) => {
31+
useEffect(() => {
32+
console.log('Forwarding refs');
33+
});
34+
35+
return <div ref={ref}>Forwarding refs</div>;
36+
});

tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
"noImplicitReturns": true,
99
"noImplicitThis": true,
1010
"strict": true,
11-
"target": "es2015"
11+
"target": "es2015",
12+
"lib": ["esnext"]
1213
},
1314
"include": ["src/**/*"],
1415
"exclude": ["node_modules", "dist"]

0 commit comments

Comments
 (0)