Skip to content

Commit 6c936ae

Browse files
committed
BREAKING CHANGE: refactor -> create custom rule for import of useIsFocused to prevent from override by import eslint rule
1 parent e5799c7 commit 6c936ae

File tree

7 files changed

+122
-15
lines changed

7 files changed

+122
-15
lines changed

example-app/eslint-breaking-examples/break-use-is-focused-import-rule.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Save without formatting: [⌘ + K] > [S]
22

3-
// This should trigger one error breaking eslint-plugin-react-native:
4-
// no-restricted-imports
3+
// This should trigger one error breaking custom performance rule:
4+
// @bam.tech/no-use-is-focused
55

66
import { useIsFocused } from "@react-navigation/native";
77
import { Text } from "react-native";

packages/eslint-plugin/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ This plugin exports some custom rules that you can optionally use in your projec
117117
| [no-different-displayname](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/no-different-displayname.md) | Enforce component displayName to match with component name || | 🔧 |
118118
| [no-flatlist](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/no-flatlist.md) | Disallow importing `FlatList` from `react-native` due to potential performance concerns or the preference for alternative components. | ![badge-performance][] | | 🔧 |
119119
| [no-react-navigation-stack](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/no-react-navigation-stack.md) | Disallow importing from `@react-navigation/stack` and suggest using `@react-navigation/native-stack` instead. | ![badge-performance][] | | 🔧 |
120+
| [no-use-is-focused](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/no-use-is-focused.md) | Disallow importing `useIsFocused` from `@react-navigation/native` to encourage using `useFocusEffect` instead. | ![badge-performance][] | | 🔧 |
120121
| [prefer-user-event](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/prefer-user-event.md) | Enforces usage of userEvent over fireEvent in tests. | | | 🔧 |
121122
| [require-named-effect](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/require-named-effect.md) | Enforces the use of named functions inside a useEffect | | | |
122123

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Disallow importing `useIsFocused` from `@react-navigation/native` to encourage using `useFocusEffect` instead (`@bam.tech/no-use-is-focused`)
2+
3+
💼 This rule is enabled in the `performance` config.
4+
5+
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
6+
7+
<!-- end auto-generated rule header -->
8+
9+
Prevents from using "useIsFocused" to avoid performance issues. "useFocusEffect" should be used instead.
10+
11+
## Rule details
12+
13+
Examples of **incorrect** code for this rule:
14+
15+
```jsx
16+
import { useIsFocused } from "@react-navigation/native";
17+
```

packages/eslint-plugin/lib/configs/performance.ts

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,12 @@ import { defineConfig } from "eslint-define-config";
22

33
export const performanceConfig = defineConfig({
44
rules: {
5-
"no-restricted-imports": [
6-
"error",
7-
{
8-
paths: [
9-
{
10-
name: "@react-navigation/native",
11-
importNames: ["useIsFocused"],
12-
message:
13-
"Please use useFocusEffect instead of useIsFocused to avoid excessive rerenders.",
14-
},
15-
],
16-
},
17-
],
185
"@bam.tech/no-animated-without-native-driver": "error",
196
"@bam.tech/avoid-intl-number-format": "error",
207
"@bam.tech/avoid-react-native-svg": "warn",
218
"@bam.tech/no-flatlist": "error",
229
"@bam.tech/no-react-navigation-stack": "error",
10+
"@bam.tech/no-use-is-focused": "error",
2311
},
2412
overrides: [
2513
{

packages/eslint-plugin/lib/rules/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { preferUserEventRule } from "./prefer-user-event";
77
import { requireNamedEffectRule } from "./require-named-effect";
88
import { noFlatListImportRule } from "./no-flatlist";
99
import { noReactNavigationStackImportRule } from "./no-react-navigation-stack";
10+
import { noUseIsFocusedImportRule } from "./no-use-is-focused";
1011

1112
export default {
1213
"await-user-event": awaitUserEventRule,
@@ -18,4 +19,5 @@ export default {
1819
"avoid-react-native-svg": avoidReactNativeSvgImportRule,
1920
"no-flatlist": noFlatListImportRule,
2021
"no-react-navigation-stack": noReactNavigationStackImportRule,
22+
"no-use-is-focused": noUseIsFocusedImportRule,
2123
};
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import type { Rule } from "eslint";
2+
import type { ImportDeclaration, CallExpression, Property } from "estree";
3+
4+
// Custom Rule: No Import of useIsFocused from @react-navigation/native
5+
export const noUseIsFocusedImportRule: Rule.RuleModule = {
6+
meta: {
7+
type: "problem",
8+
docs: {
9+
description:
10+
"Disallow importing `useIsFocused` from `@react-navigation/native` to encourage using `useFocusEffect` instead.",
11+
category: "Best Practices",
12+
recommended: true,
13+
url: "https://github.com/bamlab/react-native-project-config/tree/main/packages/eslint-plugin/docs/rules/no-use-is-focused.md",
14+
},
15+
messages: {
16+
noUseIsFocusedImport:
17+
"Please use 'useFocusEffect' instead of 'useIsFocused' to avoid excessive rerenders: 'useIsFocused' will trigger rerender both when the page goes in and out of focus.",
18+
},
19+
schema: [],
20+
fixable: "code",
21+
},
22+
23+
create(context) {
24+
return {
25+
ImportDeclaration(node: ImportDeclaration) {
26+
if (node.source.value === "@react-navigation/native") {
27+
node.specifiers.forEach((specifier) => {
28+
if (
29+
specifier.type === "ImportSpecifier" &&
30+
specifier.imported.name === "useIsFocused"
31+
) {
32+
context.report({
33+
node: specifier,
34+
messageId: "noUseIsFocusedImport",
35+
});
36+
}
37+
});
38+
}
39+
},
40+
CallExpression(node: CallExpression) {
41+
if (
42+
node.callee.type === "Identifier" &&
43+
node.callee.name === "require" &&
44+
node.arguments.length > 0 &&
45+
node.arguments[0].type === "Literal" &&
46+
node.arguments[0].value === "@react-navigation/native"
47+
) {
48+
const ancestors = context.getAncestors();
49+
const parent = ancestors[ancestors.length - 1]; // Get the direct parent of the node
50+
51+
if (
52+
parent.type === "VariableDeclarator" &&
53+
parent.id.type === "ObjectPattern"
54+
) {
55+
const properties = parent.id.properties as Property[];
56+
const useIsFocusedProperty = properties.find(
57+
(prop) =>
58+
prop.type === "Property" &&
59+
prop.key.type === "Identifier" &&
60+
prop.key.name === "useIsFocused",
61+
);
62+
63+
if (useIsFocusedProperty) {
64+
context.report({
65+
node: useIsFocusedProperty,
66+
messageId: "noUseIsFocusedImport",
67+
});
68+
}
69+
}
70+
}
71+
},
72+
};
73+
},
74+
};
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Save without formatting: [⌘ + K] > [S]
2+
3+
// This should trigger an error breaking eslint-plugin-bam-custom-rules:
4+
// bam-custom-rules/no-use-is-focused
5+
6+
import { noUseIsFocusedImportRule } from "../../../lib/rules/no-use-is-focused";
7+
import { RuleTester } from "eslint";
8+
9+
const ruleTester = new RuleTester({
10+
parser: require.resolve("@typescript-eslint/parser"),
11+
});
12+
13+
const valid = [`import { useFocusEffect } from "@react-navigation/native";`];
14+
15+
const invalid = [`import { useIsFocused } from "@react-navigation/native";`];
16+
17+
ruleTester.run("no-use-is-focused", noUseIsFocusedImportRule, {
18+
valid,
19+
invalid: invalid.map((code) => ({
20+
code,
21+
errors: [
22+
`Please use 'useFocusEffect' instead of 'useIsFocused' to avoid excessive rerenders: 'useIsFocused' will trigger rerender both when the page goes in and out of focus.`,
23+
],
24+
})),
25+
});

0 commit comments

Comments
 (0)