├── .cursor
└── rules
│ ├── code-style.mdc
│ ├── eslint.mdc
│ ├── project-general.mdc
│ └── testing.mdc
├── .github
├── ISSUE_TEMPLATE
│ └── bug_report.md
├── pull_request_template.md
└── workflows
│ ├── pullRequest.yml
│ └── release.yml
├── .gitignore
├── .husky
├── commit-msg
└── pre-commit
├── .lintstagedrc.json
├── .npmrc
├── .tool-versions
├── README.md
├── __tests__
├── eslint-v8
│ ├── ruleTester.ts
│ └── rules.test.ts
├── eslint-v9
│ ├── ruleTester.ts
│ └── rules.test.ts
└── testcases
│ ├── requireMemo.ts
│ ├── requireUseCallback.ts
│ ├── requireUseMemo.ts
│ └── requireUseMemoChildren.ts
├── commitlint.config.js
├── docs
└── rules
│ ├── eslint-v9-migration-guide.md
│ ├── require-memo.md
│ ├── require-usememo-children.md
│ └── require-usememo.md
├── examples
├── README.md
├── v8-traditional
│ ├── .eslintrc.js
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ ├── App.js
│ │ ├── Custom.js
│ │ └── index.js
│ └── yarn.lock
└── v9-flat
│ ├── README.md
│ ├── eslint.config.js
│ ├── package.json
│ ├── src
│ ├── App.js
│ ├── Custom.js
│ └── index.js
│ └── yarn.lock
├── jest.config.js
├── knip.json
├── package.json
├── rollup.config.js
├── src
├── constants
│ └── messages.ts
├── flat-config.ts
├── index.ts
├── require-memo
│ ├── index.ts
│ ├── types.ts
│ └── utils.ts
├── require-usememo-children.ts
├── require-usememo
│ ├── constants.ts
│ ├── index.ts
│ ├── types.ts
│ └── utils.ts
├── traditional-config.ts
├── types.ts
└── utils
│ ├── compatibility.ts
│ ├── getVariableInScope.ts
│ └── index.ts
├── tsconfig.json
└── yarn.lock
/.cursor/rules/code-style.mdc:
--------------------------------------------------------------------------------
1 | ---
2 | description:
3 | globs: src/**/*,*.tsx,*.ts,*.js
4 | alwaysApply: false
5 | ---
6 |
7 | # Code Style Guidelines
8 |
9 | ## TypeScript
10 |
11 | The project uses TypeScript.
12 |
13 | ## React Best Practices
14 |
15 | Follow React best practices for component design, state management, and performance optimization.
16 |
17 | ## Memoization
18 |
19 | Use `useMemo`, `useCallback`, and `React.memo` appropriately to optimize performance.
20 |
21 | ## Error Handling
22 |
23 | Implement robust error handling and validation.
24 |
25 | ## Performance Considerations
26 |
27 | - Avoid RegExp where simple string operations suffice
28 | - Consider the real-time nature of ESLint plugins in IDEs
29 | - Optimize for frequent calls and minimal overhead
30 | - Profile critical paths before making optimizations
31 | - Prefer simple, direct implementations over complex ones
32 | - Be especially careful with operations that run on every file parse
--------------------------------------------------------------------------------
/.cursor/rules/eslint.mdc:
--------------------------------------------------------------------------------
1 | ---
2 | description:
3 | globs:
4 | alwaysApply: true
5 | ---
6 |
7 | # ESLint Guidelines
8 |
9 | ## What is ESLint?
10 |
11 | ESLint is a static code analysis tool for identifying problematic patterns found in ECMAScript/JavaScript code.
12 |
13 | ## Purpose
14 |
15 | To enforce code style, catch potential errors, and improve code quality.
16 |
17 | ## Configuration
18 |
19 | This project supports both ESLint v8 (traditional configuration using `.eslintrc.json`) and ESLint v9 (flat configuration using `eslint.config.js`).
20 |
21 | ## Plugin
22 |
23 | This project provides an ESLint plugin named `@arthurgeron/eslint-plugin-react-usememo`.
24 |
25 | ## Rules
26 |
27 | The plugin includes the following rules:
28 |
29 | - `require-usememo`: Requires complex values passed as props or used as hook dependencies to be wrapped in `useMemo` or `useCallback`.
30 | - `require-memo`: Requires all function components to be wrapped in `React.memo()`.
31 | - `require-usememo-children`: Requires complex values passed as children to be wrapped in `useMemo` or `useCallback`.
32 |
33 | ## Compatibility
34 |
35 | The plugin is designed to be compatible with both ESLint v8 and v9, using utility functions in `src/utils/compatibility.ts` to handle differences between the versions.
36 |
37 |
38 | ## Documentation
39 |
40 | For more details on individual rules and migration guides, refer to the documentation files in the `docs/rules` directory:
41 |
42 | - [require-memo](mdc:../docs/rules/require-memo.md)
43 | - [require-usememo](mdc:../docs/rules/require-usememo.md)
44 | - [require-usememo-children](mdc:../docs/rules/require-usememo-children.md)
45 | - [Migrating to V9.x](mdc:https:/eslint.org/docs/latest/use/migrate-to-9.0.0)
46 | - [ESLint v9 Migration Guide](mdc:../docs/rules/eslint-v9-migration-guide.md)
47 | - [Creating ESLint V9 Plugins](mdc:https:/eslint.org/docs/latest/extend/plugins)
48 | - [Sharing ESLint V9 Configurations](mdc:https:/eslint.org/docs/latest/extend/shareable-configs)
49 | - [Custom ESLint V9 Processors](mdc:https:/eslint.org/docs/latest/extend/custom-processors)
50 | - [Custom ESLint V9 Parsers](mdc:https:/eslint.org/docs/latest/extend/custom-parsers)
51 | - [Custom ESLint V9 Formatters](mdc:https:/eslint.org/docs/latest/extend/custom-formatters)
52 | - [Custom ESLint V9 Rules](mdc:https:/eslint.org/docs/latest/extend/custom-rules)
53 | - [Custom ESLint V9 Rule Tutorial](mdc:https:/eslint.org/docs/latest/extend/custom-rule-tutorial)
54 |
55 | ## ESLint Plugin Development
56 |
57 | - Remember rules run frequently in IDE environments
58 | - Optimize core operations that run on every node visit
59 | - Cache results where possible
60 | - Use simple type checks and string operations over complex regex
61 | - Consider the impact on IDE performance
--------------------------------------------------------------------------------
/.cursor/rules/project-general.mdc:
--------------------------------------------------------------------------------
1 | ---
2 | description:
3 | globs: package.json,README.md,src/**/*
4 | alwaysApply: false
5 | ---
6 | # Project General Guidelines
7 |
8 | ## Project Objective
9 |
10 | The primary goal of this project is to provide an ESLint plugin that helps developers optimize React applications by enforcing the use of `useMemo`, `useCallback`, and `React.memo` to prevent unnecessary re-renders and improve performance.
11 |
12 | ## General Structure
13 |
14 | - `src/`: Contains the source code for the ESLint plugin, including the rule implementations and utility functions.
15 | - `docs/`: Contains documentation for the plugin and its rules.
16 | - `examples/`: Contains example React projects demonstrating the use of the plugin with different ESLint configurations.
17 | - `__tests__/`: Contains test cases for the plugin rules.
18 | - `src/utils/compatibility.ts`: Provides compatibility utilities for ESLint v8 and v9.
19 |
20 | ## Key Dependencies
21 |
22 | - `yarn`: Package manager
23 | - `eslint`: The core ESLint library. Both V8 and V9 (eslint-v9).
24 | - `react`: The React library (used in examples and tests).
25 |
26 | ## ESLint Version Support
27 |
28 | The project is designed to work with both ESLint v8 and v9. The `src/utils/compatibility.ts` file provides utilities to handle differences between the two versions.
29 |
30 | ## Configuration Files
31 |
32 | - ESLint v8: `.eslintrc.json`
33 | - ESLint v9: `eslint.config.js`
34 |
35 | ## Documentation
36 |
37 | For more details on project setup and overall guidelines, please refer to [README.md](mdc:../../README.md) and the documentation in the `docs/` directory.
--------------------------------------------------------------------------------
/.cursor/rules/testing.mdc:
--------------------------------------------------------------------------------
1 | ---
2 | description: Testing Guidelines for AI Agents
3 | globs:
4 | - "__tests__/**/*"
5 | - "test-plugin.js"
6 | alwaysApply: false
7 | ---
8 |
9 | # Testing Guidelines
10 |
11 | ## Testing Approach
12 |
13 | The project uses Jest for testing.
14 |
15 | ## Test Case Structure
16 |
17 | Test cases are located in the `__tests__` directory and are separated into `validTestCases` and `invalidTestCases` to cover both passing and failing scenarios.
18 |
19 | ## Version Compatibility Testing
20 |
21 | The same test cases are executed against both ESLint v8 and ESLint v9 to ensure compatibility. This is achieved by extracting shared test cases into the `__tests__/testcases` directory and utilizing the appropriate ESLint runner in:
22 | - `__tests__/eslint-v8/rules.test.ts` for ESLint v8
23 | - `__tests__/eslint-v9/rules.test.ts` for ESLint v9
24 |
25 | ## Test Execution
26 |
27 | Tests are executed using the command `yarn test`.
28 |
29 | ## Test Case Utility
30 |
31 | The test cases are generated by a set of utility functions located in the `__tests__/testcases` directory:
32 | - `createRequireMemoTestCases` from `__tests__/testcases/requireMemo.ts`
33 | - `createRequireUseCallbackTestCases` from `__tests__/testcases/requireUseCallback.ts`
34 | - `createRequireUseMemoTestCases` from `__tests__/testcases/requireUseMemo.ts`
35 | - `createRequireUseMemoChildrenTestCases` from `__tests__/testcases/requireUseMemoChildren.ts`
36 |
37 | ## Example Test Execution
38 |
39 | ```bash
40 | yarn test
41 | ```
42 |
43 | **4. `.cursor/rules/code-style.mdc`**
44 |
45 | ```markdown
46 | ---
47 | description: Code Style Guidelines for AI Agents
48 | globs:
49 | - "src/**/*"
50 | alwaysApply: false
51 | ---
52 |
53 | # Code Style Guidelines
54 |
55 | ## TypeScript
56 |
57 | The project uses TypeScript.
58 |
59 | ## React Best Practices
60 |
61 | Follow React best practices for component design, state management, and performance optimization.
62 |
63 | ## Memoization
64 |
65 | Use `useMemo`, `useCallback`, and `React.memo` appropriately to optimize performance.
66 |
67 | ## Error Handling
68 |
69 | Implement robust error handling and validation.
70 | ```
71 |
72 | These files are now ready to be placed in the `.cursor/rules` directory of your project.
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: "[BUG]"
5 | labels: not verified
6 | assignees: arthurgeron
7 |
8 | ---
9 |
10 | ---
11 | name: Bug report for @arthurgeron/eslint-plugin-react-usememo
12 | about: Report a problem or unexpected behavior
13 | ---
14 |
15 | ## Prerequisites
16 |
17 | Before you create a new issue, please ensure that this is not a duplicate issue and that you have gone through all the debugging steps.
18 |
19 | ## Minimal repro
20 |
21 | Please provide a minimal piece of code or reproducible example that demonstrates the issue.
22 |
23 | ```javascript
24 | // Paste your code here
25 | ``````
26 | > Some rules process import statements. Make sure to include them if they're relevant to this issue.
27 |
28 | ### Installed packages versions
29 | **ESLint:**
30 | **Eslint-plugin-react-usememo:**
31 |
32 | > You can get these details by using `npm ls --depth=0`.
33 |
34 | ### Rule and its options
35 |
36 | Which rule from the eslint-plugin-react-usememo is being used? What configuration options are set for this rule?
37 |
38 | ### Error log / Stack trace
39 |
40 | Please provide the full error log or stack trace, if any.
41 |
42 | ```
43 | # copy and paste your error log here
44 | ```
45 |
46 |
47 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 |
4 | ## Description
5 |
6 |
7 | ## Checklist
8 |
9 |
10 |
11 | - [ ] **Tests**: I have created multiple test scenarios for my changes.
12 | - [ ] **Passing Tests**: All existing and new tests are pass successfully.
13 | - [ ] **Version Bump**: I have increased the package version number in package.json, following the [Semantic Versioning](https://semver.org/) (SEMVER) standard.
14 | - [ ] **Version Bump**: I have increased the package version number in src/flat-config.ts and src/traditional-config.ts, following the [Semantic Versioning](https://semver.org/) (SEMVER) standard.
15 | - [ ] **Documentation**: I have updated documentation to reflect the changes made, if necessary.
16 | - [ ] **Focused Changes**: My code changes are focused solely on the matter described above.
17 |
18 | ## Additional Information
19 |
20 |
21 |
--------------------------------------------------------------------------------
/.github/workflows/pullRequest.yml:
--------------------------------------------------------------------------------
1 | name: Pull-request
2 |
3 | on: [pull_request]
4 |
5 | jobs:
6 | install:
7 | name: Installing packages
8 | runs-on: ubuntu-latest
9 | steps:
10 | - name: Checkout
11 | uses: actions/checkout@v3
12 | with:
13 | fetch-depth: 1
14 |
15 | - name: Read .tool-versions
16 | id: tool-versions
17 | run: |
18 | NODE_VERSION=$(grep "nodejs" .tool-versions | awk '{print $2}')
19 | YARN_VERSION=$(grep "yarn" .tool-versions | awk '{print $2}')
20 | echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT
21 | echo "yarn_version=$YARN_VERSION" >> $GITHUB_OUTPUT
22 |
23 | - name: Use Node.js from .tool-versions
24 | uses: actions/setup-node@v3
25 | with:
26 | node-version: ${{ steps.tool-versions.outputs.node_version }}
27 | cache: 'yarn'
28 | cache-dependency-path: '**/yarn.lock'
29 |
30 | - name: Setup Yarn from .tool-versions
31 | run: |
32 | npm install -g yarn@${{ steps.tool-versions.outputs.yarn_version }}
33 |
34 | - name: Cache node modules
35 | uses: actions/cache@v3
36 | id: cache
37 | with:
38 | path: node_modules
39 | key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
40 | restore-keys: |
41 | ${{ runner.os }}-
42 |
43 | - name: Install Dependencies
44 | if: steps.cache.outputs.cache-hit != 'true' # It will run install dependencies just if hash of yarn.lock changed
45 | run: yarn install --frozen-lockfile --prefer-offline
46 | env:
47 | NODE_AUTH_TOKEN: ${{ secrets.GIT_PACKAGES_TOKEN }}
48 |
49 | test:
50 | name: Run Unit Tests
51 | runs-on: ubuntu-latest
52 | needs: install
53 | steps:
54 | - name: Checkout
55 | uses: actions/checkout@v3
56 | with:
57 | fetch-depth: 1
58 |
59 | - name: Read .tool-versions
60 | id: tool-versions
61 | run: |
62 | NODE_VERSION=$(grep "nodejs" .tool-versions | awk '{print $2}')
63 | YARN_VERSION=$(grep "yarn" .tool-versions | awk '{print $2}')
64 | echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT
65 | echo "yarn_version=$YARN_VERSION" >> $GITHUB_OUTPUT
66 |
67 | - name: Use Node.js from .tool-versions
68 | uses: actions/setup-node@v3
69 | with:
70 | node-version: ${{ steps.tool-versions.outputs.node_version }}
71 | cache: 'yarn'
72 | cache-dependency-path: '**/yarn.lock'
73 |
74 | - name: Setup Yarn from .tool-versions
75 | run: |
76 | npm install -g yarn@${{ steps.tool-versions.outputs.yarn_version }}
77 |
78 | - name: Restore node_modules
79 | uses: actions/cache@v3
80 | id: restore-build
81 | with:
82 | path: node_modules
83 | key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
84 |
85 | - name: Restore jest cache
86 | uses: actions/cache@v3
87 | with:
88 | path: .jest-cache
89 | key: ${{ runner.os }}-${{ hashFiles('**/.jest-cache/**') }}
90 |
91 | - name: Run tests
92 | run: yarn test
93 |
94 | build:
95 | name: Test build
96 | runs-on: ubuntu-latest
97 | needs: install
98 | steps:
99 | - name: Checkout
100 | uses: actions/checkout@v3
101 | with:
102 | fetch-depth: 1
103 |
104 | - name: Read .tool-versions
105 | id: tool-versions
106 | run: |
107 | NODE_VERSION=$(grep "nodejs" .tool-versions | awk '{print $2}')
108 | YARN_VERSION=$(grep "yarn" .tool-versions | awk '{print $2}')
109 | echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT
110 | echo "yarn_version=$YARN_VERSION" >> $GITHUB_OUTPUT
111 |
112 | - name: Use Node.js from .tool-versions
113 | uses: actions/setup-node@v3
114 | with:
115 | node-version: ${{ steps.tool-versions.outputs.node_version }}
116 | cache: 'yarn'
117 | cache-dependency-path: '**/yarn.lock'
118 |
119 | - name: Setup Yarn from .tool-versions
120 | run: |
121 | npm install -g yarn@${{ steps.tool-versions.outputs.yarn_version }}
122 |
123 | - name: Restore node_modules
124 | uses: actions/cache@v3
125 | id: restore-build
126 | with:
127 | path: node_modules
128 | key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
129 |
130 | - name: Run build
131 | run: yarn build
132 |
133 | deadcode:
134 | name: Find dead code
135 | runs-on: ubuntu-latest
136 | needs: install
137 | steps:
138 | - name: Checkout
139 | uses: actions/checkout@v3
140 | with:
141 | fetch-depth: 1
142 |
143 | - name: Read .tool-versions
144 | id: tool-versions
145 | run: |
146 | NODE_VERSION=$(grep "nodejs" .tool-versions | awk '{print $2}')
147 | YARN_VERSION=$(grep "yarn" .tool-versions | awk '{print $2}')
148 | echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT
149 | echo "yarn_version=$YARN_VERSION" >> $GITHUB_OUTPUT
150 |
151 | - name: Use Node.js from .tool-versions
152 | uses: actions/setup-node@v3
153 | with:
154 | node-version: ${{ steps.tool-versions.outputs.node_version }}
155 | cache: 'yarn'
156 | cache-dependency-path: '**/yarn.lock'
157 |
158 | - name: Setup Yarn from .tool-versions
159 | run: |
160 | npm install -g yarn@${{ steps.tool-versions.outputs.yarn_version }}
161 |
162 | - name: Restore node_modules
163 | uses: actions/cache@v3
164 | id: restore-build
165 | with:
166 | path: node_modules
167 | key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
168 |
169 | - name: Find dead code
170 | run: yarn deadCode
171 |
172 | version-check:
173 | name: Check package version
174 | runs-on: ubuntu-latest
175 | steps:
176 | - name: Checkout PR
177 | uses: actions/checkout@v3
178 | with:
179 | fetch-depth: 1
180 |
181 | - name: Checkout main
182 | uses: actions/checkout@v3
183 | with:
184 | ref: main
185 | path: main
186 | fetch-depth: 1
187 |
188 | - name: Compare versions
189 | id: version_check
190 | run: |
191 | PR_VERSION=$(node -p "require('./package.json').version")
192 | MAIN_VERSION=$(node -p "require('./main/package.json').version")
193 | if [ "$PR_VERSION" = "$MAIN_VERSION" ]; then
194 | echo "ERROR_MESSAGE=Error: Package version has not been updated" >> $GITHUB_OUTPUT
195 | exit 1
196 | elif ! npx semver -r ">$MAIN_VERSION" "$PR_VERSION" > /dev/null; then
197 | echo "ERROR_MESSAGE=Error: New version ($PR_VERSION) is not greater than current version ($MAIN_VERSION)" >> $GITHUB_OUTPUT
198 | exit 1
199 | else
200 | echo "Package version has been updated correctly"
201 | fi
202 |
203 | - name: Comment PR
204 | if: failure()
205 | uses: actions/github-script@v6
206 | with:
207 | github-token: ${{secrets.GITHUB_TOKEN}}
208 | script: |
209 | github.rest.issues.createComment({
210 | issue_number: context.issue.number,
211 | owner: context.repo.owner,
212 | repo: context.repo.repo,
213 | body: '${{ steps.version_check.outputs.ERROR_MESSAGE }}'
214 | })
215 |
216 | check-v9-example:
217 | name: Check v9-flat example for eslint errors
218 | runs-on: ubuntu-latest
219 | needs: install
220 | steps:
221 | - name: Checkout
222 | uses: actions/checkout@v3
223 | with:
224 | fetch-depth: 1
225 |
226 | - name: Use Node.js 23.10.0
227 | uses: actions/setup-node@v3
228 | with:
229 | node-version: 23.10.0
230 | cache: 'yarn'
231 | cache-dependency-path: '**/yarn.lock'
232 |
233 | - name: Setup Yarn 1.22.22
234 | run: |
235 | npm install -g yarn@1.22.22
236 |
237 | - name: Restore node_modules
238 | uses: actions/cache@v3
239 | id: restore-build
240 | with:
241 | path: node_modules
242 | key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
243 |
244 | - name: Build plugin
245 | run: |
246 | yarn
247 | yarn build
248 |
249 | - name: Check for eslint errors in v9-flat example
250 | run: |
251 | cd examples/v9-flat
252 | yarn
253 | if ! yarn lint 2>&1 | grep -q "@arthurgeron/react-usememo/require-usememo"; then
254 | echo "Error: Expected eslint error '@arthurgeron/react-usememo/require-usememo' not found in v9-flat example"
255 | exit 1
256 | fi
257 |
258 | check-v8-example:
259 | name: Check v8-traditional example for eslint errors
260 | runs-on: ubuntu-latest
261 | needs: install
262 | steps:
263 | - name: Checkout
264 | uses: actions/checkout@v3
265 | with:
266 | fetch-depth: 1
267 |
268 | - name: Use Node.js 23.10.0
269 | uses: actions/setup-node@v3
270 | with:
271 | node-version: 23.10.0
272 | cache: 'yarn'
273 | cache-dependency-path: '**/yarn.lock'
274 |
275 | - name: Setup Yarn 1.22.22
276 | run: |
277 | npm install -g yarn@1.22.22
278 |
279 | - name: Restore node_modules
280 | uses: actions/cache@v3
281 | id: restore-build
282 | with:
283 | path: node_modules
284 | key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
285 |
286 | - name: Build plugin
287 | run: |
288 | yarn
289 | yarn build
290 |
291 | - name: Check for eslint errors in v8-traditional example
292 | run: |
293 | cd examples/v8-traditional
294 | yarn
295 | if ! yarn lint 2>&1 | grep -q "@arthurgeron/react-usememo/require-usememo"; then
296 | echo "Error: Expected eslint error '@arthurgeron/react-usememo/require-usememo' not found in v8-traditional example"
297 | exit 1
298 | fi
299 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | jobs:
8 | install:
9 | name: Installing packages
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout
13 | uses: actions/checkout@v3
14 | with:
15 | fetch-depth: 1
16 |
17 | - name: Read .tool-versions
18 | id: tool-versions
19 | run: |
20 | NODE_VERSION=$(grep "nodejs" .tool-versions | awk '{print $2}')
21 | YARN_VERSION=$(grep "yarn" .tool-versions | awk '{print $2}')
22 | echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT
23 | echo "yarn_version=$YARN_VERSION" >> $GITHUB_OUTPUT
24 |
25 | - name: Use Node.js from .tool-versions
26 | uses: actions/setup-node@v3
27 | with:
28 | node-version: ${{ steps.tool-versions.outputs.node_version }}
29 | cache: 'yarn'
30 | cache-dependency-path: '**/yarn.lock'
31 |
32 | - name: Setup Yarn from .tool-versions
33 | run: |
34 | npm install -g yarn@${{ steps.tool-versions.outputs.yarn_version }}
35 |
36 | - name: Cache node modules
37 | uses: actions/cache@v3
38 | id: cache
39 | with:
40 | path: node_modules
41 | key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
42 | restore-keys: |
43 | ${{ runner.os }}-
44 |
45 | - name: Install Dependencies
46 | if: steps.cache.outputs.cache-hit != 'true' # It will run install dependencies just if hash of yarn.lock changed
47 | run: yarn install --frozen-lockfile --prefer-offline
48 | env:
49 | NODE_AUTH_TOKEN: ${{ secrets.GIT_PACKAGES_TOKEN }}
50 |
51 | test:
52 | name: Run Unit Tests
53 | runs-on: ubuntu-latest
54 | needs: install
55 | steps:
56 | - name: Checkout
57 | uses: actions/checkout@v3
58 | with:
59 | fetch-depth: 1
60 |
61 | - name: Read .tool-versions
62 | id: tool-versions
63 | run: |
64 | NODE_VERSION=$(grep "nodejs" .tool-versions | awk '{print $2}')
65 | YARN_VERSION=$(grep "yarn" .tool-versions | awk '{print $2}')
66 | echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT
67 | echo "yarn_version=$YARN_VERSION" >> $GITHUB_OUTPUT
68 |
69 | - name: Use Node.js from .tool-versions
70 | uses: actions/setup-node@v3
71 | with:
72 | node-version: ${{ steps.tool-versions.outputs.node_version }}
73 | cache: 'yarn'
74 | cache-dependency-path: '**/yarn.lock'
75 |
76 | - name: Setup Yarn from .tool-versions
77 | run: |
78 | npm install -g yarn@${{ steps.tool-versions.outputs.yarn_version }}
79 |
80 | - name: Restore node_modules
81 | uses: actions/cache@v3
82 | id: restore-build
83 | with:
84 | path: node_modules
85 | key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
86 |
87 | - name: Restore jest cache
88 | uses: actions/cache@v3
89 | with:
90 | path: .jest-cache
91 | key: ${{ runner.os }}-${{ hashFiles('**/.jest-cache/**') }}
92 |
93 | - name: Run tests
94 | run: yarn test
95 |
96 | build:
97 | name: Test build
98 | runs-on: ubuntu-latest
99 | needs: install
100 | steps:
101 | - name: Checkout
102 | uses: actions/checkout@v3
103 | with:
104 | fetch-depth: 1
105 |
106 | - name: Read .tool-versions
107 | id: tool-versions
108 | run: |
109 | NODE_VERSION=$(grep "nodejs" .tool-versions | awk '{print $2}')
110 | YARN_VERSION=$(grep "yarn" .tool-versions | awk '{print $2}')
111 | echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT
112 | echo "yarn_version=$YARN_VERSION" >> $GITHUB_OUTPUT
113 |
114 | - name: Use Node.js from .tool-versions
115 | uses: actions/setup-node@v3
116 | with:
117 | node-version: ${{ steps.tool-versions.outputs.node_version }}
118 | cache: 'yarn'
119 | cache-dependency-path: '**/yarn.lock'
120 |
121 | - name: Setup Yarn from .tool-versions
122 | run: |
123 | npm install -g yarn@${{ steps.tool-versions.outputs.yarn_version }}
124 |
125 | - name: Restore node_modules
126 | uses: actions/cache@v3
127 | id: restore-build
128 | with:
129 | path: node_modules
130 | key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
131 |
132 | - name: Run build
133 | run: yarn build
134 |
135 | deadcode:
136 | name: Find dead code
137 | runs-on: ubuntu-latest
138 | needs: install
139 | steps:
140 | - name: Checkout
141 | uses: actions/checkout@v3
142 | with:
143 | fetch-depth: 1
144 |
145 | - name: Read .tool-versions
146 | id: tool-versions
147 | run: |
148 | NODE_VERSION=$(grep "nodejs" .tool-versions | awk '{print $2}')
149 | YARN_VERSION=$(grep "yarn" .tool-versions | awk '{print $2}')
150 | echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT
151 | echo "yarn_version=$YARN_VERSION" >> $GITHUB_OUTPUT
152 |
153 | - name: Use Node.js from .tool-versions
154 | uses: actions/setup-node@v3
155 | with:
156 | node-version: ${{ steps.tool-versions.outputs.node_version }}
157 | cache: 'yarn'
158 | cache-dependency-path: '**/yarn.lock'
159 |
160 | - name: Setup Yarn from .tool-versions
161 | run: |
162 | npm install -g yarn@${{ steps.tool-versions.outputs.yarn_version }}
163 |
164 | - name: Restore node_modules
165 | uses: actions/cache@v3
166 | id: restore-build
167 | with:
168 | path: node_modules
169 | key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
170 |
171 | - name: Find dead code
172 | run: yarn deadCode
173 |
174 | release:
175 | name: Release version
176 | runs-on: ubuntu-latest
177 | needs: [test, build, deadcode]
178 | steps:
179 | - name: Checkout
180 | uses: actions/checkout@v3
181 | with:
182 | fetch-depth: 1
183 |
184 | - name: Read .tool-versions
185 | id: tool-versions
186 | run: |
187 | NODE_VERSION=$(grep "nodejs" .tool-versions | awk '{print $2}')
188 | YARN_VERSION=$(grep "yarn" .tool-versions | awk '{print $2}')
189 | echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT
190 | echo "yarn_version=$YARN_VERSION" >> $GITHUB_OUTPUT
191 |
192 | - name: Use Node.js from .tool-versions
193 | uses: actions/setup-node@v3
194 | with:
195 | node-version: ${{ steps.tool-versions.outputs.node_version }}
196 | cache: 'yarn'
197 | cache-dependency-path: '**/yarn.lock'
198 | registry-url: 'https://registry.npmjs.org'
199 | scope: '@arthurgeron'
200 |
201 | - name: Setup Yarn from .tool-versions
202 | run: |
203 | npm install -g yarn@${{ steps.tool-versions.outputs.yarn_version }}
204 |
205 | - name: Restore node_modules
206 | uses: actions/cache@v3
207 | id: restore-build
208 | with:
209 | path: node_modules
210 | key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
211 |
212 | - name: Publish Release
213 | uses: LumaKernel/npm-release-pack-action@v1.0.1
214 | id: release
215 | env:
216 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
217 | with:
218 | github_token: ${{ secrets.GITHUB_TOKEN }}
219 | publish_command: npm publish --access public
220 | generate_release_notes: true
221 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | **/node_modules/
2 | /dist/
3 | .vscode
4 | yarn-error.log
5 | .jest-cache
6 | coverage
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | npx --no -- commitlint --edit "${1}"
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | yarn deadCode # find unused exports
2 | yarn test
3 | yarn build
4 |
--------------------------------------------------------------------------------
/.lintstagedrc.json:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arthurgeron/eslint-plugin-react-usememo/d036dad6a27c2eb038a25482bbbd9a4ad0732917/.lintstagedrc.json
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | @arthurgeron:registry=https://registry.npmjs.org/
2 | save-exact = true
--------------------------------------------------------------------------------
/.tool-versions:
--------------------------------------------------------------------------------
1 | nodejs 23.10.0
2 | yarn 1.22.22
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ESLint-Plugin-React-UseMemo
2 |
3 | This plugin enforces the wrapping of complex objects or functions (which might generate unnecessary renders or side-effects) in `useMemo` or `useCallback`. It also allows you to programmatically enforce the wrapping of functional components in `memo`, and that all props and dependencies are wrapped in `useMemo`/`useCallback`.
4 |
5 | ## Purpose
6 | The objective is to ensure that your application's component tree and/or expensive lifecycles (such as React Native's FlatLists, useEffect, useMemo, etc.) only re-calculate or render again when absolutely necessary. By controlling expensive expressions, you can achieve optimal scalability and performance for your application.
7 |
8 | _**Note:**_ Use of memoization everywhere is not advised, as everything comes with a cost. Overusing memoization might slow down your application instead of speeding it up.
9 |
10 | ## Guidelines for Memoization
11 | > For more details, please refer to React's [documentation](https://react.dev/reference/react/useMemo) on hooks, re-rendering and memoization.
12 | ### There are two primary rules for situations where dynamic objects should be memoed:
13 |
14 | 1. Variables or expressions that return non-primitive objects or functions passed as props to other components.
15 |
16 | ***Incorrect***
17 | ```js
18 | function Component({incomingData}) {
19 | const complexData = {
20 | ...incomingData,
21 | checked: true
22 | }; // generated each render, breaks hooks shallow comparison
23 |
24 | return
25 | }
26 | ```
27 | ***Correct***
28 | ```js
29 | function Component({incomingData}) {
30 | const complexData = useMemo(() => ({
31 | ...incomingData,
32 | checked: true
33 | }), [incomingData]); // generated only when incomingData changes
34 |
35 | return
36 | }
37 | ```
38 |
39 | 2. Variables or expressions that return non-primitive objects returned from custom hooks.
40 |
41 | ***Incorrect***
42 | ```js
43 | function useMyData({incomingData}) {
44 | const parsedData = parseData(incomingData); // generated each render
45 |
46 | return parsedData; // Will result in loops passed as a dependency in other hooks(e.g. useMemo, useCallback, useEffect).
47 | }
48 | ```
49 | ***Correct***
50 | ```js
51 | function useMyData({incomingData}) {
52 | const parsedData = useMemo(() => parseData(incomingData), [incomingData]); // generated only when incomingData changes
53 |
54 | return parsedData; // Won't generate loops if used as a dependency in hooks.
55 | }
56 | ```
57 |
58 | ### It is not recommended to use memoization in the following cases:
59 |
60 | - When the resulting value (expression or variable) is primitive (string, number, boolean).
61 |
62 | ***Incorrect***
63 | ```js
64 | function Component() {
65 | const width = useMemo(() => someValue * 10, []); // results in integer, wouldn't break hooks' shallow comparison; Memoizing this would only reduce performance
66 |
67 | return
68 | }
69 | ```
70 | ***Correct***
71 | ```js
72 | function Component() {
73 | const width = someValue * 10;
74 |
75 | return
76 | }
77 | ```
78 |
79 | - If you're passing props to a native component of the framework (e.g. Div, Touchable, etc), except in some instances in react-native (e.g. FlatList).
80 |
81 | ***Incorrect***
82 | ```js
83 | function Component() {
84 | const onClick = useCallback(() => {}, []);
85 |
86 | return
87 | }
88 | ```
89 | ***Correct***
90 | ```js
91 | function Component() {
92 | const onClick = () => {};
93 |
94 | return
95 | }
96 | ```
97 |
98 | - Values that can be a global/context outside the react Context.
99 | ***Incorrect***
100 | ```js
101 | function Component() {
102 | const breakpoints = [100];
103 |
104 | return
105 | }
106 | ```
107 |
108 | ***Correct***
109 | ```js
110 | const breakpoints = [100];
111 |
112 | function Component() {
113 | return
114 | }
115 | ```
116 |
117 |
118 |
119 |
120 | ## Installation
121 |
122 | Install it with yarn:
123 |
124 | ```
125 | yarn add @arthurgeron/eslint-plugin-react-usememo --dev
126 | ```
127 |
128 | or npm:
129 |
130 | ```
131 | npm install @arthurgeron/eslint-plugin-react-usememo --save-dev
132 | ```
133 |
134 | ## Compatibility
135 |
136 | This plugin supports ESLint v8 (traditional configuration) and is preparing support for ESLint v9 (flat configuration).
137 |
138 | > **Important Note**: While the plugin exports the proper structure for ESLint v9, there are some compatibility issues with ESLint v9's new architecture. We're actively working on resolving these issues. For now, please continue using ESLint v8 with the traditional configuration.
139 |
140 | ### ESLint v8 Configuration (.eslintrc)
141 |
142 | Add the plugin and enable the rules in your `.eslintrc` file:
143 |
144 | ```json
145 | {
146 | "plugins": ["@arthurgeron/react-usememo"],
147 | "rules": {
148 | "@arthurgeron/react-usememo/require-usememo": "error",
149 | "@arthurgeron/react-usememo/require-memo": "error",
150 | "@arthurgeron/react-usememo/require-usememo-children": "error"
151 | }
152 | }
153 | ```
154 |
155 | ### ESLint v9 Configuration (eslint.config.js)
156 |
157 | ESLint v9 support is in progress. Once fully compatible, you'll be able to use the plugin in the following ways:
158 |
159 | #### Option 1: Importing the default config
160 |
161 | ```js
162 | import plugin from '@arthurgeron/eslint-plugin-react-usememo';
163 | export default [
164 | {
165 | files: ['**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx'],
166 | languageOptions: {
167 | ecmaVersion: 2020,
168 | sourceType: 'module',
169 | parserOptions: {
170 | ecmaFeatures: {
171 | jsx: true
172 | }
173 | }
174 | },
175 | plugins: {
176 | '@arthurgeron/react-usememo': plugin.flatConfig,
177 | },
178 | rules: {
179 | '@arthurgeron/react-usememo/require-usememo': 'error',
180 | '@arthurgeron/react-usememo/require-memo': 'error',
181 | '@arthurgeron/react-usememo/require-usememo-children': 'error',
182 | },
183 | },
184 | ];
185 | ```
186 |
187 | #### Option 2: Using the recommended config
188 |
189 | ```js
190 | import { flatConfig } from '@arthurgeron/eslint-plugin-react-usememo';
191 |
192 | export default [
193 | // Other configs...
194 | flatConfig.configs.recommended,
195 | ];
196 | ```
197 |
198 | #### Option 3: Using CommonJS syntax
199 |
200 | ```js
201 | const { flatConfig } = require('@arthurgeron/eslint-plugin-react-usememo');
202 |
203 | module.exports = [
204 | // Other configs...
205 | {
206 | plugins: {
207 | '@arthurgeron/react-usememo': flatConfig,
208 | },
209 | rules: {
210 | '@arthurgeron/react-usememo/require-usememo': 'error',
211 | '@arthurgeron/react-usememo/require-memo': 'error',
212 | '@arthurgeron/react-usememo/require-usememo-children': 'error',
213 | },
214 | },
215 | ];
216 | ```
217 |
218 | For detailed migration guidance from ESLint v8 to ESLint v9, please refer to our [ESLint v9 Migration Guide](https://github.com/arthurgeron/eslint-plugin-react-usememo/blob/main/docs/rules/eslint-v9-migration-guide.md).
219 |
220 | ## Examples
221 |
222 | For working examples of both ESLint v8 (traditional config) and ESLint v9 (flat config) implementations, please check the [examples directory](https://github.com/arthurgeron/eslint-plugin-react-usememo/tree/main/examples).
223 |
224 | ## Rule #1: `require-usememo` ***(recommended)***
225 | This rule requires complex values (objects, arrays, functions, and JSX) that get passed props or referenced as a hook dependency to be wrapped in useMemo() or useCallback().
226 |
227 | One of the great features of this rule is its amazing autofix functionality. It intelligently wraps necessary components with useMemo() or useCallback(), making your code more efficient and saving you valuable time.
228 |
229 | For detailed examples, options available for this rule, and information about the autofix functionality, please refer to our [rules documentation](https://github.com/arthurgeron/eslint-plugin-react-usememo/blob/main/docs/rules/require-usememo.md).
230 |
231 | ## Rule #2: `require-memo`
232 | This rule requires all function components to be wrapped in `React.memo()`.
233 |
234 | For detailed examples and usage of this rule, please refer to our [rules documentation](https://github.com/arthurgeron/eslint-plugin-react-usememo/blob/main/docs/rules/require-memo.md)
235 |
236 | ## Rule #3: `require-usememo-children`
237 | This rule requires complex values (objects, arrays, functions, and JSX) that get passed as children to be wrapped in `useMemo()` or `useCallback()`.
238 |
239 | For detailed examples and options available for this rule, please refer to our [rules documentation](https://github.com/arthurgeron/eslint-plugin-react-usememo/blob/main/docs/rules/require-usememo-children.md).
240 |
241 | ## Conclusion
242 | By efficiently using `useMemo`, `useCallback`, and `React.memo()`, we can optimize our React and React Native applications. It allows us to control the re-calculation and re-rendering of components, offering better scalability and performance.
243 |
--------------------------------------------------------------------------------
/__tests__/eslint-v8/ruleTester.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * ESLint V8 Rule Tester Configuration
3 | * Use this for testing rules with ESLint v8
4 | */
5 | import { RuleTester } from "eslint";
6 |
7 | export const ruleTester = new RuleTester({
8 | parser: require.resolve("@typescript-eslint/parser"),
9 | parserOptions: {
10 | ecmaVersion: 2020,
11 | sourceType: "module",
12 | ecmaFeatures: { jsx: true },
13 | },
14 | });
15 |
--------------------------------------------------------------------------------
/__tests__/eslint-v8/rules.test.ts:
--------------------------------------------------------------------------------
1 | import { createRequireMemoTestCases } from "../testcases/requireMemo";
2 | import { createRequireUseCallbackTestCases } from "../testcases/requireUseCallback";
3 | import { createRequireUseMemoTestCases } from "../testcases/requireUseMemo";
4 | import { createRequireUseMemoChildrenTestCases } from "../testcases/requireUseMemoChildren";
5 | import { ruleTester } from "./ruleTester";
6 | import requireMemo from "../../src/require-memo";
7 | import requireUseCallback from "../../src/require-usememo";
8 | import requireUseMemo from "../../src/require-usememo";
9 | import requireUseMemoChildren from "../../src/require-usememo-children";
10 | import type { Rule } from "eslint";
11 |
12 | const {
13 | validTestCases: validMemoTestCases,
14 | invalidTestCases: invalidMemoTestCases,
15 | } = createRequireMemoTestCases();
16 |
17 | const {
18 | validTestCases: validUseCallbackTestCases,
19 | invalidTestCases: invalidUseCallbackTestCases,
20 | } = createRequireUseCallbackTestCases();
21 |
22 | const {
23 | validTestCases: validUseMemoTestCases,
24 | invalidTestCases: invalidUseMemoTestCases,
25 | } = createRequireUseMemoTestCases();
26 |
27 | const {
28 | validTestCases: validUseMemoChildrenTestCases,
29 | invalidTestCases: invalidUseMemoChildrenTestCases,
30 | } = createRequireUseMemoChildrenTestCases();
31 |
32 | describe("ESLint v8", () => {
33 | describe("Require", () => {
34 | ruleTester.run("Memo", requireMemo as unknown as Rule.RuleModule, {
35 | valid: validMemoTestCases as any,
36 | invalid: invalidMemoTestCases as any,
37 | });
38 |
39 | ruleTester.run(
40 | "useCallback",
41 | requireUseCallback as unknown as Rule.RuleModule,
42 | {
43 | valid: validUseCallbackTestCases as any,
44 | invalid: invalidUseCallbackTestCases as any,
45 | },
46 | );
47 |
48 | ruleTester.run("useMemo", requireUseMemo as unknown as Rule.RuleModule, {
49 | valid: validUseMemoTestCases as any,
50 | invalid: invalidUseMemoTestCases as any,
51 | });
52 |
53 | ruleTester.run(
54 | "useMemo children",
55 | requireUseMemoChildren as unknown as Rule.RuleModule,
56 | {
57 | valid: validUseMemoChildrenTestCases as any,
58 | invalid: invalidUseMemoChildrenTestCases as any,
59 | },
60 | );
61 | });
62 | });
63 |
--------------------------------------------------------------------------------
/__tests__/eslint-v9/ruleTester.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * ESLint V9 Rule Tester Configuration
3 | * Use this for testing rules with ESLint v9 flat config format
4 | *
5 | * Note: When running tests with ESLint v9, you need to use ESLint v9 installed
6 | * This file is designed to work when you switch to ESLint v9
7 | */
8 | import { RuleTester } from "eslint-v9";
9 |
10 | export const createRuleTesterV9 = () => {
11 | // For ESLint v9, we need to use the flat config format with jsx support
12 | return new RuleTester({
13 | languageOptions: {
14 | parser: require('@typescript-eslint/parser'),
15 | ecmaVersion: 2020,
16 | sourceType: 'module',
17 | parserOptions: {
18 | ecmaFeatures: {
19 | jsx: true
20 | }
21 | }
22 | }
23 | });
24 | };
25 |
26 |
--------------------------------------------------------------------------------
/__tests__/eslint-v9/rules.test.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 | import { createRequireMemoTestCases } from "../testcases/requireMemo";
3 | import { createRequireUseCallbackTestCases } from "../testcases/requireUseCallback";
4 | import { createRequireUseMemoTestCases } from "../testcases/requireUseMemo";
5 | import { createRequireUseMemoChildrenTestCases } from "../testcases/requireUseMemoChildren";
6 | import { createRuleTesterV9 } from "./ruleTester";
7 | import requireMemo from "../../src/require-memo";
8 | import requireUseCallback from "../../src/require-usememo";
9 | import requireUseMemo from "../../src/require-usememo";
10 | import requireUseMemoChildren from "../../src/require-usememo-children";
11 | import type { Rule } from "eslint-v9";
12 |
13 | const ruleTesterV9 = createRuleTesterV9();
14 |
15 | const {
16 | validTestCases: validMemoTestCases,
17 | invalidTestCases: invalidMemoTestCases,
18 | } = createRequireMemoTestCases();
19 | const {
20 | validTestCases: validUseCallbackTestCases,
21 | invalidTestCases: invalidUseCallbackTestCases,
22 | } = createRequireUseCallbackTestCases();
23 | const {
24 | validTestCases: validUseMemoTestCases,
25 | invalidTestCases: invalidUseMemoTestCases,
26 | } = createRequireUseMemoTestCases();
27 | const {
28 | validTestCases: validUseMemoChildrenTestCases,
29 | invalidTestCases: invalidUseMemoChildrenTestCases,
30 | } = createRequireUseMemoChildrenTestCases();
31 |
32 | describe("ESLint v9", () => {
33 | describe("Require", () => {
34 | ruleTesterV9.run(
35 | "Memo",
36 | requireMemo as unknown as Rule.RuleModule,
37 | {
38 | valid: validMemoTestCases as any,
39 | invalid: invalidMemoTestCases as any,
40 | },
41 | );
42 |
43 | ruleTesterV9.run(
44 | "useCallback",
45 | requireUseCallback as unknown as Rule.RuleModule,
46 | {
47 | valid: validUseCallbackTestCases as any,
48 | invalid: invalidUseCallbackTestCases as any,
49 | },
50 | );
51 |
52 | ruleTesterV9.run("useMemo", requireUseMemo as unknown as Rule.RuleModule, {
53 | valid: validUseMemoTestCases as any,
54 | invalid: invalidUseMemoTestCases as any,
55 | });
56 |
57 | ruleTesterV9.run(
58 | "useMemo children",
59 | requireUseMemoChildren as unknown as Rule.RuleModule,
60 | {
61 | valid: validUseMemoChildrenTestCases as any,
62 | invalid: invalidUseMemoChildrenTestCases as any,
63 | },
64 | );
65 | });
66 | });
67 |
--------------------------------------------------------------------------------
/__tests__/testcases/requireMemo.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Shared test cases for ESLint plugin rules
3 | * This file contains test cases that can be used by both ESLint v8 and v9 test configurations
4 | */
5 |
6 | // Helper to create a standard set of test cases for require-memo
7 | export const createRequireMemoTestCases = () => {
8 | const validTestCases = [
9 | // Valid case: Component wrapped in memo
10 | {
11 | code: `
12 | import React, { memo } from 'react';
13 | const Component = memo(() =>
);
14 | export default Component;
15 | `,
16 | },
17 | // Valid case: Default export wrapped in memo
18 | {
19 | code: `
20 | import React, { memo } from 'react';
21 | export default memo(() =>
);
22 | `,
23 | },
24 | // Valid case: Named export wrapped in memo
25 | {
26 | code: `
27 | import React, { memo } from 'react';
28 | export const Component = memo(() =>
);
29 | `,
30 | },
31 | // Valid case: Expression wrapped in memo
32 | {
33 | code: `
34 | import React, { memo } from 'react';
35 | const Component = memo(() => {
36 | return
;
37 | });
38 | export default Component;
39 | `,
40 | },
41 | // Valid case: Alternative memo import
42 | {
43 | code: `
44 | import { memo } from 'react';
45 | export default memo(() =>
);
46 | `,
47 | },
48 | // Valid case: Full React import with memo
49 | {
50 | code: `
51 | import React from 'react';
52 | export default React.memo(() =>
);
53 | `,
54 | },
55 | // Additional cases from require-memo.test.ts
56 | { code: "export const TestMap = {};" },
57 | { code: "const TestMap = {}; export default TestMap;" },
58 | { code: "export default {};" },
59 | { code: "export const SomethingWeird = func()();" },
60 | { code: "export const Component = useRef(() =>
)" },
61 | { code: "export const Component = useRef(function() { return
; })" },
62 | { code: "export const variable = func()();" },
63 | { code: "export const Component = React.memo(() =>
)" },
64 | { code: "export const Component = memo(() =>
)" },
65 | { code: "const Component = memo(() =>
); export default Component;" },
66 | { code: "export default memo(function Component() { return
; })" },
67 | { code: "export const Component = memo(useRef(() =>
))" },
68 | { code: "const Component = React.useRef(React.memo(() =>
))" },
69 | { code: "export const myFunction = () =>
" },
70 | { code: "export const myFunction = wrapper(() =>
)" },
71 | { code: "export const Component = React.memo(function() { return
; });" },
72 | { code: "export const Component = memo(function Component() { return
; });" },
73 | { code: "function myFunction() { return
; }" },
74 | { code: "const myFunction = wrapper(function() { return
})" },
75 | {
76 | code: "export default function() { return
};",
77 | filename: "dir/myFunction.js",
78 | },
79 | { code: "const Component = () =>
; export default memo(Component);" },
80 | {
81 | code: "export const Component = () =>
",
82 | options: [{
83 | ignoredComponents: {
84 | 'Component': true,
85 | }
86 | }]
87 | },
88 | ];
89 |
90 | const invalidTestCases = [
91 | // Invalid case: Component not wrapped in memo
92 | {
93 | code: `
94 | import React from 'react';
95 | const Component = () =>
;
96 | export default Component;
97 | `,
98 | errors: [{ messageId: 'memo-required' }],
99 | },
100 | // Invalid case: Default export not wrapped in memo
101 | {
102 | code: `
103 | import React from 'react';
104 | export default () =>
;
105 | `,
106 | errors: [{ messageId: 'memo-required' }],
107 | },
108 | // Invalid case: Named export not wrapped in memo
109 | {
110 | code: `
111 | import React from 'react';
112 | export const Component = () =>
;
113 | `,
114 | errors: [{ messageId: 'memo-required' }],
115 | },
116 | // Invalid case: Component with expression not wrapped in memo
117 | {
118 | code: `
119 | import React from 'react';
120 | const Component = () => {
121 | return
;
122 | };
123 | export default Component;
124 | `,
125 | errors: [{ messageId: 'memo-required' }],
126 | },
127 | // Additional cases from require-memo.test.ts
128 | {
129 | code: "export const Component = () =>
",
130 | errors: [{ messageId: "memo-required" }],
131 | },
132 | {
133 | code: "export const ListItem = () =>
",
134 | errors: [{ messageId: "memo-required" }],
135 | options: [{
136 | ignoredComponents: {
137 | '*Item': false,
138 | '*': true
139 | }
140 | }]
141 | },
142 | {
143 | code: "const Component = () =>
; export default Component;",
144 | errors: [{ messageId: "memo-required" }],
145 | },
146 | {
147 | code: "export const Component = function Component() { return
; }",
148 | errors: [{ messageId: "memo-required" }],
149 | },
150 | {
151 | code: "export function Component() { return
; }",
152 | errors: [{ messageId: "memo-required" }],
153 | },
154 | {
155 | code: "export default function Component() { return
; }",
156 | errors: [{ messageId: "memo-required" }],
157 | },
158 | ];
159 |
160 | return { validTestCases, invalidTestCases };
161 | };
162 |
163 |
--------------------------------------------------------------------------------
/__tests__/testcases/requireUseCallback.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Shared test cases for ESLint plugin rules
3 | * This file contains test cases that can be used by both ESLint v8 and v9 test configurations
4 | */
5 |
6 | // Helper to create a standard set of test cases for require-usecallback
7 | export const createRequireUseCallbackTestCases = () => {
8 | const validTestCases = [
9 | {
10 | code: `const Component = () => {
11 | const myFn = useCallback(function() {}, []);
12 | return ;
13 | }`,
14 | },
15 | {
16 | code: `const Component = () => {
17 | const myFn = useCallback(() => {}, []);
18 | return ;
19 | }`,
20 | },
21 | {
22 | code: `const Component = () => {
23 | const myFn = function() {};
24 | return
;
25 | }`,
26 | },
27 | {
28 | code: `class Component {
29 | render() {
30 | const myFn = function() {};
31 | return
;
32 | }
33 | }`,
34 | },
35 | {
36 | code: `
37 | const myFn = function() {};
38 | const Component = () => {
39 | return ;
40 | }`,
41 | },
42 | {
43 | code: `
44 | const myFn = function() {};
45 | class Component {
46 | render() {
47 | return ;
48 | }
49 | }`,
50 | },
51 | {
52 | code: `const Component = () => {
53 | const myFn = () => {};
54 | return
;
55 | }`,
56 | },
57 | {
58 | code: `
59 | const myFn = () => {};
60 | const Component = () => {
61 | return
;
62 | }`,
63 | },
64 | {
65 | code: `const Component = () => {
66 | const myFn1 = useCallback(() => [], []);
67 | const myFn2 = useCallback(() => myFn1, [myFn1]);
68 | return ;
69 | }`,
70 | },
71 | {
72 | code: `
73 | class Component {
74 | myFn() {}
75 | render() {
76 | return ;
77 | }
78 | }`,
79 | },
80 | {
81 | code: `const Component = () => {
82 | const myFn = memoize(() => {});
83 | return ;
84 | }`,
85 | },
86 | {
87 | code: `const Component = () => {
88 | const myFn = lodash.memoize(() => []);
89 | return ;
90 | }`,
91 | },
92 | {
93 | code:`const Component = () => {
94 | const myFn1 = () => [];
95 | const myFn2 = useCallback(() => myFn1, [myFn1]);
96 | return ;
97 | }`,
98 | },
99 | {
100 | code: `const Component = () => {
101 | const myFn = useMemo(() => function() {}, []);
102 | return ;
103 | }`,
104 | }
105 | ];
106 |
107 | const invalidTestCases = [
108 | {
109 | code: `
110 | const Component = () => {
111 | const myFn = function myFn() {};
112 | return ;
113 | }`,
114 | output: `import { useCallback } from 'react';
115 |
116 | const Component = () => {
117 | const myFn = useCallback(() => {}, []);
118 | return ;
119 | }`,
120 | errors: [{ messageId: "function-usecallback-props" }],
121 | },
122 | {
123 | code: `
124 | const Component = () => {
125 | const myFn = () => {};
126 | return ;
127 | }`,
128 | output: `import { useCallback } from 'react';
129 |
130 | const Component = () => {
131 | const myFn = useCallback(() => {}, []);
132 | return ;
133 | }`,
134 | errors: [{ messageId: "function-usecallback-props" }],
135 | },
136 | {
137 | code: `
138 | const Component = () => {
139 | let myFn = useCallback(() => ({}));
140 | myFn = () => ({});
141 | return ;
142 | }`,
143 | output: `
144 | const Component = () => {
145 | const myFn = useCallback(() => ({}));
146 | myFn = () => ({});
147 | return ;
148 | }`,
149 | errors: [{ messageId: "usememo-const" }],
150 | },
151 | {
152 | code: `
153 | const Component = () => {
154 | return {}} />;
155 | }`,
156 | errors: [{ messageId: "function-usecallback-props" }],
157 | output: `import { useCallback } from 'react';
158 |
159 | const Component = () => {
160 | const prop = useCallback(() => {}, []);
161 | return ;
162 | }`,
163 | },
164 | {
165 | code: `
166 | const Component = () => {
167 | return []} />;
168 | }`,
169 | errors: [{ messageId: "function-usecallback-props" }],
170 | output: `import { useCallback } from 'react';
171 |
172 | const Component = () => {
173 | const prop = useCallback(() => [], []);
174 | return ;
175 | }`,
176 | },
177 | {
178 | code: `class Component {
179 | return () {
180 | return []} />;
181 | }
182 | }`,
183 | errors: [{ messageId: "instance-class-memo-props" }],
184 | },
185 | {
186 | code: `
187 | const Component = () => {
188 | const myFn1 = () => [];
189 | const myFn2 = useCallback(() => [], []);
190 | return ;
191 | }`,
192 | output: `import { useCallback } from 'react';
193 |
194 | const Component = () => {
195 | const myFn1 = useCallback(() => [], []);
196 | const myFn2 = useCallback(() => [], []);
197 | return ;
198 | }`,
199 | errors: [{ messageId: "function-usecallback-props" }],
200 | },
201 | {
202 | code: `const Component = () => {
203 | const myFn = memoize(() => {});
204 | return ;
205 | }`,
206 | options: [{ strict: true }],
207 | errors: [{ messageId: "unknown-usememo-props" }],
208 | },
209 | {
210 | code: `const Component = () => {
211 | const myFn = lodash.memoize(() => []);
212 | return ;
213 | }`,
214 | options: [{ strict: true }],
215 | errors: [{ messageId: "unknown-usememo-props" }],
216 | },
217 | {
218 | code: `class Component {
219 | render() {
220 | const myFn = lodash.memoize(() => []);
221 | return ;
222 | }
223 | }`,
224 | options: [{ strict: true }],
225 | errors: [{ messageId: "unknown-class-memo-props" }],
226 | },
227 | {
228 | code: `
229 | const Component = () => {
230 | const myFn = function test() {};
231 | return ;
232 | }`,
233 | output: `import { useCallback } from 'react';
234 |
235 | const Component = () => {
236 | const myFn = useCallback(() => {}, []);
237 | return ;
238 | }`,
239 | errors: [{ messageId: "function-usecallback-props" }],
240 | },
241 | {
242 | code: `
243 | const Component = () => {
244 | const myFn = () => [];
245 | return ;
246 | }`,
247 | output: `import { useCallback } from 'react';
248 |
249 | const Component = () => {
250 | const myFn = useCallback(() => [], []);
251 | return ;
252 | }`,
253 | errors: [{ messageId: "function-usecallback-props" }],
254 | },
255 | {
256 | code: `import { useMemo } from 'react';
257 |
258 | const Component = () => {
259 | const myFn = () => [];
260 | const myFn2 = () => [];
261 | return ;
262 | }`,
263 | output: `import { useMemo, useCallback } from 'react';
264 |
265 | const Component = () => {
266 | const myFn = useCallback(() => [], []);
267 | const myFn2 = useCallback(() => [], []);
268 | return ;
269 | }`,
270 | errors: [{ messageId: "function-usecallback-props" }, { messageId: "function-usecallback-props" }],
271 | },
272 | {
273 | code: `
274 | const Component = () => {
275 | const myFn = async function test() {};
276 | return ;
277 | }`,
278 | output: `import { useCallback } from 'react';
279 |
280 | const Component = () => {
281 | const myFn = useCallback(async () => {}, []);
282 | return ;
283 | }`,
284 | errors: [{ messageId: "function-usecallback-props" }],
285 | },
286 | {
287 | code: `
288 | const Component = () => {
289 | const myFn = async () => [];
290 | return ;
291 | }`,
292 | output: `import { useCallback } from 'react';
293 |
294 | const Component = () => {
295 | const myFn = useCallback(async () => [], []);
296 | return ;
297 | }`,
298 | errors: [{ messageId: "function-usecallback-props" }],
299 | },
300 | {
301 | code: `
302 | const Component = () => {
303 | return []} />;
304 | }`,
305 | output: `import { useCallback } from 'react';
306 |
307 | const Component = () => {
308 | const prop = useCallback(async () => [], []);
309 | return ;
310 | }`,
311 | errors: [{ messageId: "function-usecallback-props" }],
312 | }
313 | ];
314 |
315 | return { validTestCases, invalidTestCases };
316 | };
317 |
--------------------------------------------------------------------------------
/__tests__/testcases/requireUseMemo.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Shared test cases for ESLint plugin rules
3 | * This file contains test cases that can be used by both ESLint v8 and v9 test configurations
4 | */
5 |
6 | // Helper to create a standard set of test cases for require-usememo
7 | export const createRequireUseMemoTestCases = () => {
8 | const validTestCases = [
9 | {
10 | code: `function renderItem({
11 | field,
12 | fieldState,
13 | }) {
14 | return (
15 | );
20 | }`,
21 | },
22 | {
23 | code: `function Component() {
24 | let myObject = 'hi';
25 | return ;
26 | }`,
27 | },
28 | {
29 | code: `function Component({index = 0}) {
30 | const isNotFirst = index > 0;
31 | return ;
32 | }`,
33 | },
34 | {
35 | code: `function Component() {
36 | return {/* Empty Expression Should Not Error :) */} ;
37 | }`,
38 | },
39 | {
40 | code: `const Component = () => {
41 | const myObject = useMemo(() => ({}), []);
42 | return ;
43 | }`,
44 | },
45 | {
46 | code: `const Component = () => {
47 | const myArray = useMemo(() => [], []);
48 | return ;
49 | }`
50 | },
51 | {
52 | code: `
53 | function Component({data}) {
54 | return ;
55 | }`,
56 | },
57 | {
58 | code: `
59 | function x() {}
60 | function Component() {
61 | return ;
62 | }`,
63 | },
64 | {
65 | code: `
66 | function userTest() {
67 | const y = (data) => {};
68 | return y;
69 | }`,
70 | },
71 | {
72 | code: `
73 | function hi() {
74 | const y = (data) => {};
75 | return y;
76 | }`,
77 | },
78 | {
79 | code: `const Component = () => {
80 | const myArray = useMemo(() => new Object(), []);
81 | return ;
82 | }`
83 | },
84 | {
85 | code: `
86 | const labels = ['data']
87 | const Content =
88 | const Component = () => {
89 | const myArray = useMemo(() => new Object(), []);
90 | return ;
91 | }`
92 | },
93 | {
94 | code: `function Component() {
95 | const myArray = useMemo(() => new Object(), []);
96 | return ;
97 | }`
98 | },
99 | {
100 | code: `
101 | const myArray = new Object();
102 | class Component {
103 | render() {
104 | return ;
105 | }
106 | }`,
107 | },
108 | {
109 | code: `
110 | const myArray = new Object();
111 | function Component() {
112 | return ;
113 | }`,
114 | },
115 | {
116 | code: `class Component {
117 | constructor(props){
118 | super(props);
119 | this.state = {
120 | myData: new Object(),
121 | };
122 | }
123 | render() {
124 | const {myData} = this.state;
125 | return ;
126 | }
127 | }`,
128 | },
129 | {
130 | code: `class Component {
131 | constructor(props){
132 | super(props);
133 | this.state = {
134 | myArray: [],
135 | };
136 | }
137 | render() {
138 | const {myArray} = this.state;
139 | return ;
140 | }
141 | }`,
142 | },
143 | {
144 | code: `
145 | const myArray = [];
146 | class Component {
147 | render() {
148 | return ;
149 | }
150 | }`,
151 | },
152 | {
153 | code: `
154 | function test() {}
155 | class Component {
156 | render() {
157 | return ;
158 | }
159 | }`,
160 | },
161 | {
162 | code: `const Component = () => {
163 | const myObject = {};
164 | return
;
165 | }`,
166 | },
167 | {
168 | code: `const Component = () => {
169 | const myArray = [];
170 | return
;
171 | }`,
172 | },
173 | {
174 | code: `class Component {
175 | render() {
176 | const myArray = [];
177 | return
;
178 | }
179 | }`,
180 | },
181 | {
182 | code: `const Component = () => {
183 | const myNumber1 = 123;
184 | const myNumber2 = 123 + 456;
185 | const myString1 = 'abc';
186 | const myString2 = \`abc\`;
187 | return
;
188 | }`,
189 | },
190 | {
191 | code: `const Component = () => {
192 | const myObject = memoize({});
193 | return ;
194 | }`,
195 | },
196 | {
197 | code: `
198 | function test() {}
199 | const Component = () => {
200 | return ;
201 | }`,
202 | },
203 | {
204 | code: `
205 | function test() {}
206 | function Component() {
207 | return ;
208 | }`,
209 | },
210 | {
211 | code: `const Component = () => {
212 | const myArray = lodash.memoize([]);
213 | return ;
214 | }`,
215 | },
216 | {
217 | code: `const Component = () => {
218 | const myBool = false;
219 | return ;
220 | }`,
221 | },
222 | {
223 | code: `const Component = () => {
224 | const myString = 'test';
225 | return ;
226 | }`,
227 | },
228 | {
229 | code: `const Component = () => {
230 | const myComplexString = css\`color: red;\`;
231 | return ;
232 | }`,
233 | },
234 | {
235 | code: `function useTest() {
236 | const myBool = false;
237 | return myBool;
238 | }`,
239 | },
240 | {
241 | code: `
242 | const x = {};
243 | function useTest() {
244 | return {x};
245 | }`,
246 | },
247 | {
248 | code: `function useTesty() {
249 | const myString = '';
250 | return myString;
251 | }`,
252 | },
253 | {
254 | code: `function useTesty() {
255 | const myBool = useMemo(() => !!{}, []);
256 | return myBool;
257 | }`,
258 | },
259 | {
260 | code: `function useTesty() {
261 | const x = {};
262 | const myBool = useMemo(() => x, [x]);
263 | return myBool;
264 | }`,
265 | },
266 | {
267 | code: `
268 | function useTesty() {
269 | const x = {};
270 | return useData(x);
271 | }`,
272 | options: [{ checkHookReturnObject: true, checkHookCalls: false }],
273 | },
274 | {
275 | code: `
276 | function useTesty() {
277 | const x = {};
278 | return use(x);
279 | }`,
280 | },
281 | {
282 | code: `
283 | function useTesty() {
284 | const x = {};
285 | return useDataManager(x);
286 | }`,
287 | options: [{ checkHookReturnObject: true, ignoredHookCallsNames: {"!useDate*": true} }],
288 | },
289 | {
290 | code: `const Component = () => {
291 | const myArray1 = [];
292 | const myArray2 = useMemo(() => myArray1, [myArray1]);
293 | return ;
294 | }`,
295 | },
296 | {
297 | code: `function useTest() {
298 | // @ts-ignore
299 | const y: boolean | undefined = false;
300 | const x = useMemo(() => x, [y]);
301 | return {x};
302 | }`,
303 | },
304 | {
305 | code: `function useTest({data}: {data: boolean | undefined}) {
306 | const x = useMemo(() => !data, [data]);
307 | return {x};
308 | }`,
309 | },
310 | {
311 | code: `function useTest() {
312 | let y = '';
313 | const x = useMemo(() => '', []);
314 | return {x, y};
315 | }`,
316 | },
317 | // ignoredPropNames
318 | {
319 | code: `const Component = () => {
320 | const myObject = {};
321 | return ;
322 | }`,
323 | options: [{ ignoredPropNames: ["ignoreProp"] }],
324 | },
325 | {
326 | code: `const Component = () => {
327 | const myCallback = () => {};
328 | return Click me ;
329 | }`,
330 | options: [{ ignoredPropNames: ["onClick"] }],
331 | },
332 | {
333 | code: `const Component = () => {
334 | return
;
335 | }`,
336 | options: [{ ignoredPropNames: ["style"] }],
337 | },
338 | {
339 | code: `const Component = () => {
340 | return {}}>Click me ;
341 | }`,
342 | options: [{ ignoredPropNames: ["onClick"] }],
343 | },
344 | ];
345 |
346 | const invalidTestCases = [
347 | {
348 | code: `
349 | import React, { useMemo } from 'react';
350 | const MyComponent = () => {
351 | const items = [1, 2];
352 | return (
353 |
354 | {items.map(item => )}
355 | {items.forEach(item => )}
356 | {items.reduce((acc, item) => )}
357 | {items.some((acc, item) => )}
358 | {items.every((acc, item) => )}
359 | {items.find((acc, item) => )}
360 | {items.findIndex((acc, item) => )}
361 |
362 | );
363 | };
364 | `,
365 | errors: [
366 | { messageId: "error-in-invalid-context" },
367 | { messageId: "error-in-invalid-context" },
368 | { messageId: "error-in-invalid-context" },
369 | { messageId: "error-in-invalid-context" },
370 | { messageId: "error-in-invalid-context" },
371 | { messageId: "error-in-invalid-context" },
372 | { messageId: "error-in-invalid-context" }
373 | ],
374 | },
375 | {
376 | code: `
377 | import React, { useMemo } from 'react';
378 | const MyComponent = () => {
379 | const itemsJSX = useMemo(() => , []);
380 | const itemsParsed = useMemo(() => items.map(item => ), []);
381 |
382 | return null;
383 | };
384 | `,
385 | errors: [
386 | { messageId: "error-in-invalid-context" },
387 | { messageId: "error-in-invalid-context" }
388 | ],
389 | },
390 | {
391 | code: `
392 | import React, { useCallback } from 'react';
393 | const MyComponent = () => {
394 | const handleClick = useCallback(() => {
395 | return ;
396 | }, []);
397 | return Click me
;
398 | };
399 | `,
400 | errors: [{ messageId: "error-in-invalid-context" }],
401 | },
402 | {
403 | code: `
404 | const Component = () => {
405 | return } />;
406 | }`,
407 | errors: [{ messageId: "jsx-usememo-props" }],
408 | output: `import { useMemo } from 'react';
409 |
410 | const Component = () => {
411 | const prop = useMemo(() => ( ), []);
412 | return ;
413 | }`,
414 | },
415 | {
416 | code: `
417 | const Component = () => {
418 | const myObject = {};
419 | return ;
420 | }`,
421 | errors: [{ messageId: "object-usememo-props" }],
422 | output: `import { useMemo } from 'react';
423 |
424 | const Component = () => {
425 | const myObject = useMemo(() => ({}), []);
426 | return ;
427 | }`,
428 | },
429 | {
430 | code: `
431 | const Component = () => {
432 | const myArray = [];
433 | return ;
434 | }`,
435 | output: `import { useMemo } from 'react';
436 |
437 | const Component = () => {
438 | const myArray = useMemo(() => [], []);
439 | return ;
440 | }`,
441 | errors: [{ messageId: "array-usememo-props" }],
442 | },
443 | {
444 | code: `
445 | const Component = () => {
446 | const myArray = [];
447 | return ;
448 | }`,
449 | output: `
450 | const Component = () => {
451 | const myArray = useMemo(() => [], []);
452 | return ;
453 | }`,
454 | options: [{ fix: { addImports: false } }],
455 | errors: [{ messageId: "array-usememo-props" }],
456 | },
457 | {
458 | code: `
459 | const Component = () => {
460 | const myInstance = new Object();
461 | return ;
462 | }`,
463 | output: `import { useMemo } from 'react';
464 |
465 | const Component = () => {
466 | const myInstance = useMemo(() => new Object(), []);
467 | return ;
468 | }`,
469 | errors: [{ messageId: "instance-usememo-props" }],
470 | },
471 | {
472 | code: `class Component {
473 | render() {
474 | const myInstance = new Object();
475 | return ;
476 | }
477 | }`,
478 | errors: [{ messageId: "instance-class-memo-props" }],
479 | },
480 | {
481 | code: `import { useCallback } from 'react';
482 |
483 | const Component = () => {
484 | const firstInstance = useMemo(() => new Object(), []);
485 | const second = new Object();
486 | return ;
487 | }`,
488 | output: `import { useCallback, useMemo } from 'react';
489 |
490 | const Component = () => {
491 | const firstInstance = useMemo(() => new Object(), []);
492 | const second = useMemo(() => new Object(), []);
493 | return ;
494 | }`,
495 | errors: [{ messageId: "instance-usememo-props" }],
496 | },
497 | {
498 | code: `
499 | const Component = () => {
500 | const firstInstance = useMemo(() => new Object(), []);
501 | const second = new Object();
502 | return ;
503 | }`,
504 | output: `import { useMemo } from 'react';
505 |
506 | const Component = () => {
507 | const firstInstance = useMemo(() => new Object(), []);
508 | const second = useMemo(() => new Object(), []);
509 | return ;
510 | }`,
511 | errors: [{ messageId: "instance-usememo-props" }],
512 | },
513 | {
514 | code: `
515 | const Component = () => {
516 | let myObject = useMemo(() => ({}), []);
517 | myObject = {a: 'b'};
518 | return ;
519 | }`,
520 | output: `
521 | const Component = () => {
522 | const myObject = useMemo(() => ({}), []);
523 | myObject = {a: 'b'};
524 | return ;
525 | }`,
526 | errors: [{ messageId: "usememo-const" }],
527 | },
528 | {
529 | code: `
530 | import { useCallback } from 'react';
531 |
532 | const Component = () => {
533 | return ;
534 | }`,
535 | output: `
536 | import { useCallback, useMemo } from 'react';
537 |
538 | const Component = () => {
539 | const userData = useMemo(() => ({}), []);
540 | return ;
541 | }`,
542 | errors: [{ messageId: "object-usememo-props" }],
543 | },
544 | {
545 | code: `
546 | const Component = () => {
547 | const userData = undefined;
548 | return ;
549 | }`,
550 | output: `import { useMemo } from 'react';
551 |
552 | const Component = () => {
553 | const userData = undefined;
554 | const _userData = useMemo(() => ({}), []);
555 | return ;
556 | }`,
557 | errors: [{ messageId: "object-usememo-props" }],
558 | },
559 | {
560 | code: `
561 | const Component = () => {
562 | const userData = undefined;
563 | const _userData = undefined;
564 | return ;
565 | }`,
566 | output: `import { useMemo } from 'react';
567 |
568 | const Component = () => {
569 | const userData = undefined;
570 | const _userData = undefined;
571 | const __userData = useMemo(() => ({}), []);
572 | return ;
573 | }`,
574 | errors: [{ messageId: "object-usememo-props" }],
575 | },
576 | {
577 | code: `
578 | const Component = () => {
579 | const userData = undefined;
580 | const _userData = undefined;
581 | const __userData = undefined;
582 | const ___userData = undefined;
583 | const ____userData = undefined;
584 | const _____userData = undefined;
585 | return ;
586 | }`,
587 | output: `import { useMemo } from 'react';
588 |
589 | const Component = () => {
590 | const userData = undefined;
591 | const _userData = undefined;
592 | const __userData = undefined;
593 | const ___userData = undefined;
594 | const ____userData = undefined;
595 | const _____userData = undefined;
596 | const renameMe = useMemo(() => ({}), []);
597 | return ;
598 | }`,
599 | errors: [{ messageId: "object-usememo-props" }],
600 | },
601 | {
602 | code: `
603 | const Component = () => {
604 | const userData = undefined;
605 | const _userData = undefined;
606 | const __userData = undefined;
607 | const ___userData = undefined;
608 | const ____userData = undefined;
609 | const _____userData = undefined;
610 | const renameMe = undefined;
611 | return ;
612 | }`,
613 | output: `import { useMemo } from 'react';
614 |
615 | const Component = () => {
616 | const userData = undefined;
617 | const _userData = undefined;
618 | const __userData = undefined;
619 | const ___userData = undefined;
620 | const ____userData = undefined;
621 | const _____userData = undefined;
622 | const renameMe = undefined;
623 | const renameMe_99c32a94 = useMemo(() => ({}), []);
624 | return ;
625 | }`,
626 | errors: [{ messageId: "object-usememo-props" }],
627 | },
628 | {
629 | code: `
630 | const Component = () => {
631 | let x = {};
632 | return ;
633 | }`,
634 | output: `import { useMemo } from 'react';
635 |
636 | const Component = () => {
637 | const x = useMemo(() => ({}), []);
638 | return ;
639 | }`,
640 | errors: [{ messageId: "object-usememo-props" }],
641 | },
642 | {
643 | code: `import { useMemo } from 'react';
644 | function useTesty() {
645 | const x = {};
646 | return useDataManager(x);
647 | }`,
648 | options: [{ checkHookReturnObject: true, ignoredHookCallsNames: {"useData*": false} }],
649 | output: `import { useMemo } from 'react';
650 | function useTesty() {
651 | const x = useMemo(() => ({}), []);
652 | return useDataManager(x);
653 | }`,
654 | errors: [{ messageId: "object-usememo-deps" }],
655 | },
656 | {
657 | code: `
658 | const useTest = () => {
659 | // @ts-ignore
660 | const x: boolean | undefined = false;
661 | function y() {}
662 | return {x, y};
663 | }`,
664 | output: `import { useCallback } from 'react';
665 |
666 | const useTest = () => {
667 | // @ts-ignore
668 | const x: boolean | undefined = false;
669 | const y = useCallback(() => {}, []);
670 | return {x, y};
671 | }`,
672 | errors: [{ messageId: "function-usecallback-hook" }],
673 | },
674 | {
675 | code: `
676 | function Component() {
677 | const component = ;
678 | return ;
679 | }`,
680 | errors: [{messageId: "jsx-usememo-props"}],
681 | output: `import { useMemo } from 'react';
682 |
683 | function Component() {
684 | const component = useMemo(() => ( ), []);
685 | return ;
686 | }`
687 | },
688 | {
689 | code: `
690 | import {} from 'react';
691 | function useTesty() {
692 | const user = {};
693 | const x = {
694 | renderCell: (user) => ,
695 | };
696 | return useData(x);
697 | }`,
698 | output: `
699 | import { useMemo } from 'react';
700 | function useTesty() {
701 | const user = {};
702 | const x = {
703 | renderCell: (user) => ['role'], [])} user={user} />,
704 | };
705 | return useData(x);
706 | }`,
707 | errors: [{ messageId: "object-usememo-deps" }, { messageId: "array-usememo-props" }, { messageId: "unknown-usememo-hook" }],
708 | options: [{ strict: true,
709 | checkHookReturnObject: true,
710 | fix: { addImports: true },
711 | checkHookCalls: true,
712 | ignoredHookCallsNames: {},
713 | }],
714 | },
715 | {
716 | code: `import type { ComponentProps } from 'react';
717 | import React from 'react';
718 |
719 | const Component = () => {
720 | const myArray = [];
721 | return ;
722 | }`,
723 | output: `import type { ComponentProps } from 'react';
724 | import React, { useMemo } from 'react';
725 |
726 | const Component = () => {
727 | const myArray = useMemo(() => [], []);
728 | return ;
729 | }`,
730 | errors: [{ messageId: "array-usememo-props" }],
731 | },
732 | {
733 | code: `import type { ComponentProps } from 'react';
734 |
735 | const Component = () => {
736 | const myArray = [];
737 | return ;
738 | }`,
739 | output: `import { useMemo } from 'react';
740 | import type { ComponentProps } from 'react';
741 |
742 | const Component = () => {
743 | const myArray = useMemo(() => [], []);
744 | return ;
745 | }`,
746 | errors: [{ messageId: "array-usememo-props" }],
747 | },
748 | {
749 | code: `import type { ComponentProps } from 'react';
750 | import React, { useRef } from 'react';
751 |
752 | const Component = () => {
753 | const myRef = useRef();
754 | const myArray = [];
755 | return ;
756 | }`,
757 | output: `import type { ComponentProps } from 'react';
758 | import React, { useRef, useMemo } from 'react';
759 |
760 | const Component = () => {
761 | const myRef = useRef();
762 | const myArray = useMemo(() => [], []);
763 | return ;
764 | }`,
765 | errors: [{ messageId: "array-usememo-props" }],
766 | },
767 | {
768 | code: `
769 | function useTest() {
770 | return { x: 1 };
771 | }`,
772 | output: `import { useMemo } from 'react';
773 |
774 | function useTest() {
775 | return useMemo(() => ({ x: 1 }), []);
776 | }`,
777 | errors: [{ messageId: "object-usememo-hook" }],
778 | options: [{ checkHookReturnObject: true, strict: true }],
779 | },
780 | {
781 | code: `
782 | function useTest() {
783 | return (data) => {};
784 | }`,
785 | output: `import { useCallback } from 'react';
786 |
787 | function useTest() {
788 | return useCallback((data) => {}, []);
789 | }`,
790 | errors: [{ messageId: "function-usecallback-hook" }],
791 | options: [{ checkHookReturnObject: true, strict: true }],
792 | },
793 | {
794 | code: `
795 | function useTest() {
796 | const y = { x: 1 };
797 | return y;
798 | }`,
799 | output: `import { useMemo } from 'react';
800 |
801 | function useTest() {
802 | const y = useMemo(() => ({ x: 1 }), []);
803 | return y;
804 | }`,
805 | errors: [{ messageId: "object-usememo-hook" }],
806 | options: [{ checkHookReturnObject: true, strict: true }],
807 | },
808 | {
809 | code: `
810 | function useTest() {
811 | const y = (data) => {};
812 | return y;
813 | }`,
814 | output: `import { useCallback } from 'react';
815 |
816 | function useTest() {
817 | const y = useCallback((data) => {}, []);
818 | return y;
819 | }`,
820 | errors: [{ messageId: "function-usecallback-hook" }],
821 | options: [{ checkHookReturnObject: true, strict: true }],
822 | },
823 | {
824 | code: `import { anything } from 'react'
825 |
826 | const useMyHook = () => {
827 | useAnotherHook(
828 | () => {},
829 | []
830 | );
831 | };`,
832 | output: `import { anything, useCallback } from 'react'
833 |
834 | const useMyHook = () => {
835 | useAnotherHook(
836 | useCallback(() => {}, []),
837 | []
838 | );
839 | };`,
840 | errors: [{ messageId: "function-usecallback-deps" }, { messageId: "array-usememo-deps" }],
841 | },
842 | ];
843 |
844 | return { validTestCases, invalidTestCases };
845 | };
846 |
--------------------------------------------------------------------------------
/__tests__/testcases/requireUseMemoChildren.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Shared test cases for ESLint plugin rules
3 | * This file contains test cases that can be used by both ESLint v8 and v9 test configurations
4 | */
5 |
6 | // Helper to create a standard set of test cases for require-usememo-children
7 | export const createRequireUseMemoChildrenTestCases = () => {
8 | const validTestCases = [
9 | {
10 | code: `const Component = () => {
11 | const children = React.useMemo(() =>
, []);
12 | return {children} ;
13 | }`,
14 | },
15 | {
16 | code: `const Component = () => {
17 | return
;
18 | }`,
19 | },
20 | {
21 | code: `const Component = () => {
22 | const renderFn = React.useCallback(() =>
, []);
23 | return {renderFn} ;
24 | }`,
25 | }
26 | ];
27 |
28 | const invalidTestCases = [
29 | {
30 | code: `const Component = () => {
31 | const children = React.useMemo(() =>
, []);
32 | return
33 | <>
34 | {children}
35 | >
36 | ;
37 | }`,
38 | errors: [{ messageId: "jsx-usememo-children" }],
39 | },
40 | {
41 | code: `const Component = () => {
42 | const children =
;
43 | return {children}
44 | }`,
45 | errors: [{ messageId: "jsx-usememo-children" }],
46 | },
47 | {
48 | code: `const Component = () => {
49 | const children = [
, , ];
50 | return {children}
51 | }`,
52 | errors: [{ messageId: "array-usememo-children" }],
53 | },
54 | {
55 | code: `const Component = () => {
56 | return
57 | {() =>
}
58 |
59 | }`,
60 | errors: [{ messageId: "function-usecallback-children" }],
61 | }
62 | ];
63 |
64 | return { validTestCases, invalidTestCases };
65 | };
66 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = { extends: ['@commitlint/config-conventional'] };
2 |
--------------------------------------------------------------------------------
/docs/rules/eslint-v9-migration-guide.md:
--------------------------------------------------------------------------------
1 | # ESLint v9 Migration Guide
2 |
3 | Starting from version 2.5.0, `@arthurgeron/eslint-plugin-react-usememo` exports configurations compatible with both ESLint v8 (traditional config) and ESLint v9 (flat config). This guide will help you migrate to ESLint v9 once full compatibility is achieved.
4 |
5 | ## Current Status
6 |
7 | **Important Note:** While the plugin exports the proper structure for ESLint v9 compatibility, there are some underlying compatibility issues with the current rule implementations and ESLint v9's new architecture. We're actively working on resolving these issues for complete ESLint v9 compatibility.
8 |
9 | For now, please continue using ESLint v8 with the traditional configuration.
10 |
11 | ## Prerequisites
12 |
13 | 1. Install ESLint v9 (once full compatibility is released):
14 | ```bash
15 | npm install eslint@^9.0.0 --save-dev
16 | ```
17 |
18 | 2. Ensure your `@arthurgeron/eslint-plugin-react-usememo` version is at least 2.5.0:
19 | ```bash
20 | npm install @arthurgeron/eslint-plugin-react-usememo@^2.5.0 --save-dev
21 | ```
22 |
23 | ## Migration Steps
24 |
25 | ### 1. Create a New Configuration File
26 |
27 | Once full compatibility is achieved, create a new `eslint.config.js` file in your project root:
28 |
29 | ```js
30 | // eslint.config.js
31 | import { flatConfig } from '@arthurgeron/eslint-plugin-react-usememo';
32 | import js from '@eslint/js';
33 | // Import other plugins as needed
34 | import reactPlugin from 'eslint-plugin-react';
35 | import reactHooksPlugin from 'eslint-plugin-react-hooks';
36 |
37 | export default [
38 | js.configs.recommended,
39 | {
40 | files: ['**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx'],
41 | plugins: {
42 | 'react': reactPlugin,
43 | 'react-hooks': reactHooksPlugin,
44 | '@arthurgeron/react-usememo': flatConfig
45 | },
46 | languageOptions: {
47 | ecmaVersion: 2022,
48 | sourceType: 'module',
49 | parserOptions: {
50 | ecmaFeatures: {
51 | jsx: true
52 | }
53 | }
54 | },
55 | rules: {
56 | // React rules
57 | 'react/react-in-jsx-scope': 'off',
58 | 'react-hooks/rules-of-hooks': 'error',
59 | 'react-hooks/exhaustive-deps': 'warn',
60 |
61 | // React useMemo plugin rules
62 | '@arthurgeron/react-usememo/require-usememo': 'error',
63 | '@arthurgeron/react-usememo/require-memo': 'error',
64 | '@arthurgeron/react-usememo/require-usememo-children': 'error'
65 | }
66 | }
67 | ];
68 | ```
69 |
70 | ### 2. Using the Recommended Configuration
71 |
72 | If you prefer to use the recommended configuration, you can simplify your config:
73 |
74 | ```js
75 | // eslint.config.js
76 | import { flatConfig } from '@arthurgeron/eslint-plugin-react-usememo';
77 | import js from '@eslint/js';
78 | import reactPlugin from 'eslint-plugin-react';
79 |
80 | export default [
81 | js.configs.recommended,
82 | // Your other configs...
83 | flatConfig.configs.recommended
84 | ];
85 | ```
86 |
87 | ### 3. CommonJS Support
88 |
89 | If you're using CommonJS modules:
90 |
91 | ```js
92 | // eslint.config.js
93 | const { flatConfig } = require('@arthurgeron/eslint-plugin-react-usememo');
94 | const js = require('@eslint/js');
95 |
96 | module.exports = [
97 | js.configs.recommended,
98 | // Your other configs...
99 | {
100 | plugins: {
101 | '@arthurgeron/react-usememo': flatConfig
102 | },
103 | rules: {
104 | '@arthurgeron/react-usememo/require-usememo': 'error',
105 | '@arthurgeron/react-usememo/require-memo': 'error',
106 | '@arthurgeron/react-usememo/require-usememo-children': 'error'
107 | }
108 | }
109 | ];
110 | ```
111 |
112 | ### 4. Remove the Old Configuration
113 |
114 | After confirming that the new configuration works correctly, you can remove the old `.eslintrc.*` file.
115 |
116 | ## Key Differences
117 |
118 | ### Traditional Config (v8)
119 |
120 | ```json
121 | {
122 | "plugins": ["@arthurgeron/react-usememo"],
123 | "rules": {
124 | "@arthurgeron/react-usememo/require-usememo": "error",
125 | "@arthurgeron/react-usememo/require-memo": "error",
126 | "@arthurgeron/react-usememo/require-usememo-children": "error"
127 | }
128 | }
129 | ```
130 |
131 | ### Flat Config (v9)
132 |
133 | ```js
134 | import { flatConfig } from '@arthurgeron/eslint-plugin-react-usememo';
135 |
136 | export default [
137 | {
138 | plugins: {
139 | '@arthurgeron/react-usememo': flatConfig
140 | },
141 | rules: {
142 | '@arthurgeron/react-usememo/require-usememo': 'error',
143 | '@arthurgeron/react-usememo/require-memo': 'error',
144 | '@arthurgeron/react-usememo/require-usememo-children': 'error'
145 | }
146 | }
147 | ];
148 | ```
149 |
150 | ## Example Projects
151 |
152 | For complete working examples of both configurations, check out the example projects in the [examples directory](https://github.com/arthurgeron/eslint-plugin-react-usememo/tree/main/examples):
153 |
154 | 1. **v8-traditional** - Example with ESLint v8 traditional config
155 | 2. **v9-flat** - Example with ESLint v9 flat config structure
156 |
157 | ## Additional Resources
158 |
159 | - [ESLint v9 Flat Config Documentation](https://eslint.org/docs/latest/use/configure/configuration-files-new)
160 | - [ESLint v9 Migration Guide](https://eslint.org/docs/latest/use/migrate-to-9.0.0)
--------------------------------------------------------------------------------
/docs/rules/require-memo.md:
--------------------------------------------------------------------------------
1 | # Rule: require-memo
2 |
3 | This rule enforces the use of `memo()` on function components. The objective is to optimize your component re-renders and avoid unnecessary render cycles when your component's props do not change.
4 |
5 | ## Rationale
6 |
7 | React’s rendering behavior ensures that whenever the parent component renders, the child component instances are re-rendered as well. When dealing with expensive computations or components, this could lead to performance issues. `memo()` is a higher order component which tells React to skip rendering the component if its props have not changed.
8 |
9 | When `memo()` wraps an exported component, then it will only re-render if the current and next props are not shallowly equal.
10 |
11 | ```jsx
12 | function MyComponent(props) { /* ... */ }
13 |
14 | export default memo(MyComponent);
15 | ```
16 |
17 | This rule applies to function components, not class-based components as they should extend `React.PureComponent` or must implement `shouldComponentUpdate` lifecycle method for similar optimization.
18 |
19 | ## Rule Details
20 | This rule will enforce that all function components are wrapped in `memo()`.
21 | Only exported components are validated.
22 |
23 | ## Incorrect Code Examples
24 |
25 | Here are examples of incorrect code:
26 |
27 | ```js
28 | // Not using memo on function component
29 | function ComponentA(props) {
30 | return {props.name}
;
31 | }
32 |
33 | export default ComponentA;
34 | ```
35 |
36 | ## Correct Code Examples
37 |
38 | Here is an example of correct code pattern:
39 |
40 | ```js
41 | // Using memo on function component
42 | function ComponentB(props) {
43 | return {props.name}
;
44 | }
45 |
46 | export default memo(ComponentB);
47 | ```
48 | ## Options
49 |
50 | The rule takes an optional object:
51 |
52 | ```json
53 | "rules": {
54 | "@arthurgeron/react-usememo/require-memo": [2, {
55 | "ignoredComponents": {
56 | "IgnoreMe": true,
57 | "DontIgnoreMe": false,
58 | "!IgnoreEverythingButMe": true,
59 | }
60 | }]
61 | }
62 | ```
63 | - `{ignoredComponents: Record}`: This allows you to add specific Component Names, thereby individually disabling or enabling them to be checked when used. Matching names with a `true` value will cause the checks to be ignored.
64 | You can use strict 1:1 comparisons (e.g., `"ComponentName"`) or employ Minimatch's Glob Pattern (e.g., `"*Item"`).
65 | > For more information on Minimatch, refer to its README [here](https://www.npmjs.com/package/minimatch). You may also find this [cheatsheet](https://github.com/motemen/minimatch-cheat-sheet) useful.
66 |
67 |
68 | ## When Not To Use It
69 |
70 | If the component always re-renders with different props or is not expensive in terms of performance, there is no real benefit to using `memo()`. In fact, using `memo()` for a large number of simple components could negatively impact performance as memoizing small components may cost more than re-rendering them.
71 |
72 | > For more examples and detailed explanation, refer to the eslint-plugin-react-memo [readme](https://github.com/myorg/eslint-plugin-react-memo).
--------------------------------------------------------------------------------
/docs/rules/require-usememo-children.md:
--------------------------------------------------------------------------------
1 | # Rule: require-usememo-children
2 |
3 | This rule enforces the use of `useMemo()` around children in render methods.
4 |
5 | ## Rationale
6 |
7 | The React `useMemo` hook is used to memorize expensive computations, but it can also be beneficial when passing complex objects or large arrays as props to child components.
8 |
9 | By wrapping such data with useMemo, you ensure that the child component will not re-render unless the data has changed. It compares the previous result with the new one and if they're the same, it avoids the re-render.
10 |
11 | ```jsx
12 | function ParentComponent({ list }) {
13 | const memoizedList = React.useMemo(() => list, [list]);
14 |
15 | return ;
16 | }
17 | ```
18 | This rule applies to any type of component (functional or class-based) as long as it involves passing complex children (objects, arrays) to another component.
19 |
20 | ## Rule Details
21 | This rule will enforce that all complex children are wrapped in `useMemo()`.
22 |
23 | ## Incorrect
24 | ```JavaScript
25 | function Component() {
26 |
27 |
28 | <>
29 |
30 | >
31 |
32 | }
33 | ```
34 |
35 | ## Correct
36 | ```JavaScript
37 | function Component() {
38 |
39 | const children = useMemo(() => ( ), []);
40 |
41 | {children}
42 |
43 | }
44 | ```
45 |
46 | ## Options
47 |
48 | - `{strict: true}`: Fails even in cases where it is difficult to determine if the value in question is a primitive (string or number) or a complex value (object, array, etc.).
49 |
50 | ## When Not To Use It
51 |
52 | If the child elements are simple types (strings, numbers) or the parent doesn't often re-render, then using `useMemo()` might be overkill and could even lead to worse performance. Therefore, it's better to turn off this rule in such cases.
53 |
54 | > For more examples and detailed explanation, refer to the eslint-plugin-react-memo-children [readme](https://github.com/myorg/eslint-plugin-react-memo-children).
--------------------------------------------------------------------------------
/docs/rules/require-usememo.md:
--------------------------------------------------------------------------------
1 | # Rule: require-usememo
2 |
3 | This rule requires complex values (objects, arrays, functions, and JSX) that get passed as props or referenced as a hook dependency to be wrapped in `useMemo()` or `useCallback()`.
4 |
5 | Not only does this rule enforce performance optimization practices, but it also comes with an **amazing autofix functionality**. It intelligently wraps necessary components with `useMemo()` or `useCallback()`, which results in a more efficient code and saves developers valuable time.
6 |
7 | This rule aims to ensure that calculations are not performed more often than necessary and a component's tree only recalculates or renders again when absolutely required. This controls expensive expressions and enhances your application's scalability and performance.
8 |
9 | ## Options
10 |
11 | The rule takes an optional object:
12 |
13 | ```json
14 | "rules": {
15 | "@arthurgeron/react-usememo/require-usememo": [2, {
16 | "strict": true,
17 | "checkHookReturnObject": true,
18 | "fix": { "addImports": true },
19 | "checkHookCalls": true,
20 | "ignoredHookCallsNames": { "useStateManagement": false },
21 | "ignoredPropNames": ["style"]
22 | }],
23 | }
24 | ```
25 |
26 | Here is what each option means:
27 |
28 | - `{strict: true}`: Fails even in cases where it is difficult to determine if the value in question is a primitive (string or number) or a complex value (object, array, etc.).
29 |
30 | - `{checkHookReturnObject: true}`: Requires Object Expressions passed in return statements.
31 |
32 | - `{checkHookCalls: true}`: Requires objects/data passed to a non-native/Custom hook to be memoized.
33 |
34 | - `{ignoredHookCallsNames: Record}`: This allows you to add specific hook names, thereby individually disabling or enabling them to be checked when used. Matching names with a `true` value will cause the checks to be ignored.
35 | You can use strict 1:1 comparisons (e.g., `"useCustomHook"`) or employ Minimatch's Glob Pattern (e.g., `"useState*"`).
36 | > React's [use](https://react.dev/reference/react/use) hook is ignored here by default, while triggering async calls dinamically losely within a component's render cycle is bad, it does not affect the overall "performance" or behavior of the hook itself. This can be disabled with a `"use": true` entry.
37 |
38 | > For more information on Minimatch, refer to its README [here](https://www.npmjs.com/package/minimatch). You may also find this [cheatsheet](https://github.com/motemen/minimatch-cheat-sheet) useful.
39 |
40 | > If no strict names match and you have entries with Glob syntax, the algorithm will stop at the first match.
41 |
42 |
43 | - `fix`: Contains rules that only apply during eslint's fix routine.
44 |
45 | - `addImports`: Creates imports for useMemo and useCallback when one or both are added by this rule. Will increment to a existing import declaration or create a new one. Setting this to false disables it, defaults to true.
46 |
47 | - `ignoredPropNames`: This allows you to add specific prop name, thereby disabling them to be checked when used.
48 |
49 | ## Autofix Examples (Function Components & Hooks only)
50 |
51 | To illustrate the autofix feature in action, below are some examples with input code and the corresponding fixed output:
52 |
53 | #### Example 1
54 | Input:
55 | ```jsx
56 | const Component = () => {
57 | return {}} />;
58 | }
59 | ```
60 | Fixed output:
61 | ```jsx
62 | import { useCallback } from 'react';
63 |
64 | const Component = () => {
65 | const userData = useCallback(() => ({}), []);
66 | return ;
67 | }
68 | ```
69 |
70 | > If an expression is unnamed, it adopts the name of the property. Plus, it scans the scope to make sure the naming doesn't conflict with any existing variables. You can find more examples in our [tests](https://github.com/arthurgeron/eslint-plugin-react-usememo/__tests__/require-usememo.test.ts).
71 |
72 | > Imports for useMemo and useCallback are created, if necessary. Can be disabled with option `{ fix: { addImports: false } }`
73 |
74 | #### Example 2
75 | Input:
76 | ```jsx
77 | const Component = () => {
78 | const myObject = {};
79 | return ;
80 | }
81 | ```
82 | Fixed output:
83 | ```jsx
84 | import { useMemo } from 'react';
85 |
86 | const Component = () => {
87 | const myObject = useMemo(() => ({}), []);
88 | return ;
89 | }
90 | ```
91 |
92 | #### Example 3
93 | Input:
94 | ```jsx
95 | const Component = () => {
96 | const myArray = [];
97 | return ;
98 | }
99 | ```
100 | Fixed output:
101 | ```jsx
102 | import { useMemo } from 'react';
103 |
104 | const Component = () => {
105 | const myArray = useMemo(() => [], []);
106 | return ;
107 | }
108 | ```
109 |
110 | #### Example 4
111 | Input:
112 | ```jsx
113 | const Component = () => {
114 | const myInstance = new Object();
115 | return ;
116 | }
117 | ```
118 | Fixed output:
119 | ```jsx
120 | import { useMemo } from 'react';
121 |
122 | const Component = () => {
123 | const myInstance = useMemo(() => new Object(), []);
124 | return ;
125 | }
126 | ```
127 |
128 | > For all the example scenarios provided above, please check [`this guide`](https://github.com/arthurgeron/eslint-plugin-react-usememo/blob/main/docs/rules/require-usememo.md).
129 |
130 |
131 | Absolutely! Here are examples illustrating incorrect and correct application of the `require-usememo` rule.
132 |
133 | ## Incorrect Code Examples
134 |
135 | ### Function Components
136 |
137 | The function defined within a component is re-declared on each render which can lead to undesirable side effects or unnecessary render cycles.
138 |
139 | ```js
140 | function MyComponent() {
141 | const myObject = {}; // This object is re-created on each render
142 | const myArray = []; // This array is re-created on each render
143 |
144 | return ;
145 | }
146 | ```
147 |
148 | ### Hooks
149 |
150 | Objects defined within a hook will be re-declared on each render, which will cause undesirable side effects for other hooks and components using these objects.
151 |
152 | ```js
153 | function useData() {
154 | const myObject = {}; // This object is re-created on each render
155 | const myArray = []; // This array is re-created on each render
156 |
157 | return return {myObject, myArray};
158 | }
159 | ```
160 |
161 | ### Class Components
162 |
163 | Each time `render()` is called, a new object (or array or class instance) is created. A better approach would be to have this object created once and reused.
164 |
165 | ```js
166 | class MyClassComponent extends React.Component {
167 | render() {
168 | const myObject = {}; // This object is re-created on each render
169 | return ;
170 | }
171 | }
172 | ```
173 |
174 | ## Correct Code Examples
175 |
176 | ### Function Components
177 |
178 | Below, all the potentially mutable objects are wrapped with `useMemo`, so they aren't re-created on each render.
179 |
180 | ```js
181 | import { useMemo } from 'react';
182 |
183 | function MyComponent() {
184 | const myObject = useMemo(() => ({}), []);
185 | const myArray = useMemo(() => [], []);
186 |
187 | return ;
188 | }
189 | ```
190 |
191 | ### Hooks
192 |
193 | Below, all the potentially mutable objects are wrapped with `useMemo`, so they aren't re-created on each render.
194 |
195 | ```js
196 | function useData() {
197 | const myObject = useMemo(() => ({}), []);
198 | const myArray = useMemo(() => [], []);
199 |
200 | return return {myObject, myArray};
201 | }
202 | ```
203 |
204 | ### Class Components
205 |
206 | Here, we're storing our object in the component's state. This ensures the new object isn't re-created every time the component is rendered.
207 |
208 | ```js
209 | class MyClassComponent extends React.Component {
210 | constructor() {
211 | this.state = {
212 | myObject: {}
213 | }
214 | }
215 | render() {
216 | return ;
217 | }
218 | }
219 | ```
220 |
221 | Another example showing how to properly modify a incoming prop only when the reference changes, instead of changing it every render.
222 |
223 | ```js
224 | function getUserIdsInfo (users) {
225 | return users.reduce((acc, userData, index) => {
226 | const id = userData.id;
227 | const isFirstPosition = !index;
228 | acc.ids.push(id);
229 | acc.joinedKeys += isFirstPosition ? id : `,${id}`;
230 | return acc;
231 | }, { joinedKeys: '', ids: [] });
232 | }
233 |
234 | class MyClassComponent extends React.Component {
235 | constructor(props) {
236 | super(props);
237 | const { joinedKeys, ids: userIds } = getUserIdsInfo(props.users);
238 |
239 | this.state = {
240 | userIds,
241 | joinedKeys,
242 | };
243 | }
244 |
245 | static getDerivedStateFromProps(nextProps, prevState) {
246 |
247 | // Not optimal to do reduce every prop/state change
248 | // Optimally there'd be an "if" statement here to check if the prop we care about has changed, avoiding the reduce.
249 | // This is only an example
250 | const { joinedKeys, ids: userIds } = getUserIdsInfo(nextProps.users);
251 |
252 | if (joinedKeys !== prevState.joinedKeys) {
253 | return {
254 | userIds,
255 | joinedKeys,
256 | };
257 | }
258 | // Return null to indicate no change to state
259 | return null;
260 | }
261 |
262 | render() {
263 | // Reference to object wont change between renders, will only update when incoming prop 'users' changes.
264 | const { userIds } = this.state;
265 | return ;
266 | }
267 | }
268 | ```
269 |
270 | > For more examples and detailed explanation, refer to the eslint-plugin-react-usememo [readme](https://github.com/arthurgeron/eslint-plugin-react-usememo).
--------------------------------------------------------------------------------
/examples/README.md:
--------------------------------------------------------------------------------
1 | # ESLint Plugin React UseMemo Examples
2 |
3 | This directory contains example React projects demonstrating how to use the `@arthurgeron/eslint-plugin-react-usememo` plugin with different ESLint configurations.
4 |
5 | ## Examples
6 |
7 | 1. **[v8-traditional](./v8-traditional/)** - Demonstrates the plugin with ESLint v8 using traditional configuration format (`.eslintrc.json`)
8 |
9 | 2. **[v9-flat](./v9-flat/)** - Demonstrates the plugin with ESLint v9 using the new flat configuration format (`eslint.config.js`)
10 |
11 | ## Testing Results
12 |
13 | ### ESLint v8 Traditional Config
14 |
15 | The v8 traditional config example has been successfully tested. The plugin correctly identifies and reports issues when:
16 | - Objects aren't wrapped in `useMemo()`
17 | - Functions aren't wrapped in `useCallback()`
18 | - Components aren't wrapped in `React.memo()`
19 |
20 | You can run the linter in this example with:
21 | ```
22 | cd v8-traditional
23 | yarn install
24 | yarn lint
25 | ```
26 |
27 | ### ESLint v9 Flat Config
28 |
29 | While the plugin exports the correct flat config structure for ESLint v9, there are some compatibility issues with the current implementation and ESLint v9's new architecture. The example tests verify that the plugin's structure is correctly exported but the full integration with ESLint v9 requires further updates to the rule implementations.
30 |
31 | The test script confirms that the plugin correctly exports:
32 | - All three rules: `require-memo`, `require-usememo`, and `require-usememo-children`
33 | - The recommended configuration
34 |
35 | You can run the basic structure test with:
36 | ```
37 | cd v9-flat
38 | node test-plugin.js
39 | ```
40 |
41 | ## Key Differences
42 |
43 | ### ESLint v8 (Traditional Config)
44 |
45 | In the traditional config (`v8-traditional` example), the plugin is configured in `.eslintrc.json` like this:
46 |
47 | ```json
48 | {
49 | "plugins": ["@arthurgeron/react-usememo"],
50 | "rules": {
51 | "@arthurgeron/react-usememo/require-usememo": "error",
52 | "@arthurgeron/react-usememo/require-memo": "error",
53 | "@arthurgeron/react-usememo/require-usememo-children": "error"
54 | }
55 | }
56 | ```
57 |
58 | ### ESLint v9 (Flat Config)
59 |
60 | In the flat config (`v9-flat` example), the plugin is configured in `eslint.config.js` like this:
61 |
62 | ```js
63 | import { flatConfig } from '@arthurgeron/eslint-plugin-react-usememo';
64 |
65 | export default [
66 | {
67 | // ...
68 | plugins: {
69 | '@arthurgeron/react-usememo': flatConfig
70 | },
71 | rules: {
72 | '@arthurgeron/react-usememo/require-usememo': 'error',
73 | '@arthurgeron/react-usememo/require-memo': 'error',
74 | '@arthurgeron/react-usememo/require-usememo-children': 'error'
75 | }
76 | }
77 | ];
78 | ```
79 |
80 | Alternatively, you can use the plugin's recommended configuration:
81 |
82 | ```js
83 | import { flatConfig } from '@arthurgeron/eslint-plugin-react-usememo';
84 |
85 | export default [
86 | // Other configs...
87 | flatConfig.configs.recommended,
88 | ];
89 | ```
90 |
91 | ## Functionality
92 |
93 | Both examples demonstrate proper usage of:
94 | - `useMemo` for complex objects
95 | - `useCallback` for function references
96 | - `React.memo` for component memoization
97 |
98 | The examples show how the ESLint plugin enforces these patterns to improve React application performance.
--------------------------------------------------------------------------------
/examples/v8-traditional/.eslintrc.js:
--------------------------------------------------------------------------------
1 | const path = require('node:path');
2 |
3 | module.exports = {
4 | "env": {
5 | "browser": true,
6 | "es2021": true,
7 | "node": true
8 | },
9 | "extends": [
10 | "eslint:recommended",
11 | // "plugin:react/recommended"
12 | ],
13 | "parserOptions": {
14 | "ecmaFeatures": {
15 | "jsx": true
16 | },
17 | "ecmaVersion": "latest",
18 | "sourceType": "module"
19 | },
20 | "plugins": [
21 | // "react",
22 | // "react-hooks",
23 | "@arthurgeron/react-usememo"
24 | ],
25 | "rules": {
26 | // "react/react-in-jsx-scope": "off",
27 | // "react-hooks/rules-of-hooks": "error",
28 | // "react-hooks/exhaustive-deps": "warn",
29 |
30 | // require-usememo rule with custom options
31 | "@arthurgeron/react-usememo/require-usememo": ["error", {
32 | strict: true,
33 | checkHookReturnObject: true,
34 | fix: { addImports: true },
35 | checkHookCalls: true,
36 | ignoredHookCallsNames: { "useStateManagement": true },
37 | ignoredPropNames: ["style", "className"]
38 | }],
39 |
40 | // require-memo rule with custom options
41 | "@arthurgeron/react-usememo/require-memo": ["error", {
42 | ignoredComponents: {
43 | "Header": true,
44 | "Footer": true,
45 | "SimpleText": true
46 | }
47 | }],
48 |
49 | // require-usememo-children rule with custom options
50 | "@arthurgeron/react-usememo/require-usememo-children": ["error", {
51 | strict: true
52 | }]
53 | },
54 | "settings": {
55 | "react": {
56 | "version": "detect"
57 | }
58 | }
59 | }
--------------------------------------------------------------------------------
/examples/v8-traditional/README.md:
--------------------------------------------------------------------------------
1 | # ESLint Plugin React UseMemo - v8 Traditional Config Example
2 |
3 | This example project demonstrates how to use the `@arthurgeron/eslint-plugin-react-usememo` plugin with ESLint v8 using the traditional configuration format.
4 |
5 | ## Setup Details
6 |
7 | This project uses:
8 | - React 18
9 | - ESLint v8
10 | - Traditional ESLint configuration (`.eslintrc.json`)
11 |
12 | ## Key Files
13 |
14 | - `.eslintrc.json`: Contains the ESLint configuration that enables the react-usememo plugin rules
15 | - `src/App.js`: Demonstrates proper usage of `useMemo`, `useCallback`, and `React.memo`
16 |
17 | ## How to Use
18 |
19 | 1. Install dependencies:
20 | ```
21 | npm install
22 | ```
23 |
24 | 2. Run the linter:
25 | ```
26 | npm run lint
27 | ```
28 |
29 | 3. Start the development server:
30 | ```
31 | npm start
32 | ```
33 |
34 | ## ESLint Plugin Configuration
35 |
36 | The plugin is configured in `.eslintrc.json` with the following rules enabled:
37 |
38 | ```json
39 | {
40 | "@arthurgeron/react-usememo/require-usememo": "error",
41 | "@arthurgeron/react-usememo/require-memo": "error",
42 | "@arthurgeron/react-usememo/require-usememo-children": "error"
43 | }
44 | ```
45 |
46 | This configuration ensures that:
47 | 1. Complex values (objects, arrays, functions) passed as props are wrapped in `useMemo`/`useCallback`
48 | 2. Function components are wrapped in `React.memo()`
49 | 3. Complex values passed as children are wrapped in `useMemo`/`useCallback`
--------------------------------------------------------------------------------
/examples/v8-traditional/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "eslint-plugin-react-usememo-v8-example",
3 | "version": "1.0.0",
4 | "description": "Example React project with ESLint v8 traditional config",
5 | "main": "src/index.js",
6 | "scripts": {
7 | "start": "react-scripts start",
8 | "build": "react-scripts build",
9 | "test": "react-scripts test",
10 | "lint": "eslint src --resolve-plugins-relative-to=../../"
11 | },
12 | "dependencies": {
13 | "react": "^18.2.0",
14 | "react-dom": "^18.2.0",
15 | "react-scripts": "5.0.1"
16 | },
17 | "devDependencies": {
18 | "@arthurgeron/eslint-plugin-react-usememo": "../../",
19 | "eslint": "^8.56.0",
20 | "eslint-plugin-react": "^7.33.2",
21 | "eslint-plugin-react-hooks": "^4.6.0"
22 | },
23 | "browserslist": {
24 | "production": [
25 | ">0.2%",
26 | "not dead",
27 | "not op_mini all"
28 | ],
29 | "development": [
30 | "last 1 chrome version",
31 | "last 1 firefox version",
32 | "last 1 safari version"
33 | ]
34 | }
35 | }
--------------------------------------------------------------------------------
/examples/v8-traditional/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useMemo, useCallback } from 'react';
2 | import { Custom } from './Custom';
3 |
4 | // This component demonstrates a component that would benefit from useMemo and memo
5 | function CountDisplay({ count, label, onClick }) {
6 | console.log(`Rendering CountDisplay with count: ${count}`);
7 |
8 | // This complex object should be memoized to prevent unnecessary re-renders
9 | const styles = useMemo(() => ({
10 | container: {
11 | padding: '20px',
12 | margin: '10px',
13 | border: '1px solid #ccc',
14 | borderRadius: '5px',
15 | backgroundColor: count % 2 === 0 ? '#f0f0f0' : '#e0e0e0'
16 | },
17 | text: {
18 | color: count > 5 ? 'red' : 'blue',
19 | fontWeight: 'bold'
20 | }
21 | }), [count]);
22 | // This should trigger a lint error without useMemo because it creates a new object each render
23 | const data = {
24 | name: 'John',
25 | age: 30
26 | };
27 |
28 | return (
29 |
30 |
{label}: {count}
31 |
32 |
Increment
33 |
34 | );
35 | }
36 |
37 | // Wrap with React.memo to prevent re-renders when props don't change
38 | const MemoizedCountDisplay = React.memo(CountDisplay);
39 |
40 | // This component is not wrapped in React.memo, which should trigger the require-memo rule
41 | export function UnmemoizedComponent({ value }) {
42 | // Add some complexity to make sure it triggers the require-memo rule
43 | const formattedValue = `Value: ${value}`;
44 | const styles = {
45 | container: {
46 | padding: '10px',
47 | margin: '5px',
48 | border: '1px solid #ddd',
49 | borderRadius: '3px'
50 | }
51 | };
52 |
53 | return (
54 |
55 |
{formattedValue}
56 |
This component should be memoized
57 |
58 | );
59 | }
60 |
61 | // Won't trigger ESLint errors because it's in ignoredComponents
62 | export function Header({ title }) {
63 | return (
64 |
67 | );
68 | }
69 |
70 | // Won't trigger ESLint errors because it's in ignoredComponents
71 | export function Footer() {
72 | return (
73 |
74 | © 2023 My Application
75 |
76 | );
77 | }
78 |
79 | // Won't trigger ESLint errors because it's in ignoredComponents
80 | export function SimpleText({ text }) {
81 | return {text}
;
82 | }
83 |
84 | // A component with complex children that should trigger the require-usememo-children rule
85 | function ComplexChildContainer({ children }) {
86 | return (
87 |
88 | {children}
89 |
90 | );
91 | }
92 |
93 | function App() {
94 | const [count1, setCount1] = useState(0);
95 | const [count2, setCount2] = useState(0);
96 |
97 | // Using useCallback to memoize function references
98 | const incrementCount1 = useCallback(() => {
99 | setCount1(prevCount => prevCount + 1);
100 | }, []);
101 |
102 | const incrementCount2 = useCallback(() => {
103 | setCount2(prevCount => prevCount + 1);
104 | }, []);
105 |
106 | // Example of a complex calculation that should be memoized
107 | const total = useMemo(() => {
108 | console.log('Calculating total...');
109 | // Simulating expensive calculation
110 | return count1 + count2;
111 | }, [count1, count2]);
112 |
113 | // This could trigger a lint error without useMemo because it creates a new object each render
114 | const userData = useMemo(() => ({
115 | counts: {
116 | first: count1,
117 | second: count2,
118 | total
119 | },
120 | lastUpdated: new Date().toISOString()
121 | }), [count1, count2, total]);
122 |
123 | // These objects would normally require useMemo, but won't throw errors because their props are in ignoredPropNames
124 | const customStyle = { color: 'blue', fontWeight: 'bold' };
125 | const customClassName = { primary: true, secondary: false };
126 |
127 | // Calling a hook that would normally require its arguments to be memoized
128 | const useStateManagementResult = useStateManagement({ count1, count2 });
129 |
130 | // This complex JSX structure should be memoized when used as children
131 | const complexChildren = (
132 |
133 |
Complex Children
134 |
These children should be memoized with useMemo
135 |
136 | Item 1
137 | Item 2
138 |
139 |
140 | );
141 |
142 | // This function should use useCallback
143 | const handleClick = () => {
144 | console.log('Clicked!');
145 | };
146 |
147 | return (
148 |
149 |
ESLint React useMemo Plugin Example (v8)
150 |
151 |
Total: {total}
152 |
Last Updated: {userData.lastUpdated}
153 |
154 |
159 |
160 |
165 |
166 |
167 |
168 |
169 | This div has a style prop that would normally require useMemo
170 |
171 |
172 |
173 | This div has a className prop that would normally require useMemo
174 |
175 |
176 |
177 |
178 |
{useStateManagementResult}
179 |
180 |
181 | {complexChildren}
182 |
183 |
184 |
Click me
185 |
186 |
187 |
188 | );
189 | }
190 |
191 | // Mock hook for demonstration
192 | function useStateManagement(state) {
193 | return JSON.stringify(state);
194 | }
195 |
196 | export { MemoizedCountDisplay, ComplexChildContainer };
197 | export default App;
--------------------------------------------------------------------------------
/examples/v8-traditional/src/Custom.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const Custom = React.memo(({ data }) => {
4 | return (
5 |
6 |
Custom Component
7 |
Name: {data.name}
8 |
Age: {data.age}
9 |
10 | );
11 | });
12 |
--------------------------------------------------------------------------------
/examples/v8-traditional/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import App from './App';
4 |
5 | const root = ReactDOM.createRoot(document.getElementById('root'));
6 | root.render(
7 |
8 |
9 |
10 | );
--------------------------------------------------------------------------------
/examples/v9-flat/README.md:
--------------------------------------------------------------------------------
1 | # ESLint Plugin React UseMemo - v9 Flat Config Example
2 |
3 | This example project demonstrates how to use the `@arthurgeron/eslint-plugin-react-usememo` plugin with ESLint v9 using the new flat configuration format.
4 |
5 | ## Setup Details
6 |
7 | This project uses:
8 | - React 18
9 | - ESLint v9
10 | - Flat ESLint configuration (`eslint.config.js`)
11 |
12 | ## Key Files
13 |
14 | - `eslint.config.js`: Contains the ESLint flat configuration that enables the react-usememo plugin rules
15 | - `src/App.js`: Demonstrates proper usage of `useMemo`, `useCallback`, and `React.memo`
16 |
17 | ## How to Use
18 |
19 | 1. Install dependencies:
20 | ```
21 | npm install
22 | ```
23 |
24 | 2. Run the linter:
25 | ```
26 | npm run lint
27 | ```
28 |
29 | 3. Start the development server:
30 | ```
31 | npm start
32 | ```
33 |
34 | ## ESLint Plugin Configuration
35 |
36 | The plugin is configured in `eslint.config.js` using the flat configuration format:
37 |
38 | ```js
39 | import { flatConfig } from '@arthurgeron/eslint-plugin-react-usememo';
40 |
41 | export default [
42 | // Other configs...
43 | {
44 | // ...
45 | plugins: {
46 | '@arthurgeron/react-usememo': flatConfig
47 | },
48 | rules: {
49 | '@arthurgeron/react-usememo/require-usememo': 'error',
50 | '@arthurgeron/react-usememo/require-memo': 'error',
51 | '@arthurgeron/react-usememo/require-usememo-children': 'error',
52 | }
53 | }
54 | ];
55 | ```
56 |
57 | Alternatively, you can use the plugin's recommended configuration:
58 |
59 | ```js
60 | import { flatConfig } from '@arthurgeron/eslint-plugin-react-usememo';
61 |
62 | export default [
63 | // Other configs...
64 | flatConfig.configs.recommended,
65 | ];
66 | ```
67 |
68 | These configurations ensure that:
69 | 1. Complex values (objects, arrays, functions) passed as props are wrapped in `useMemo`/`useCallback`
70 | 2. Function components are wrapped in `React.memo()`
71 | 3. Complex values passed as children are wrapped in `useMemo`/`useCallback`
--------------------------------------------------------------------------------
/examples/v9-flat/eslint.config.js:
--------------------------------------------------------------------------------
1 | import { flatConfig } from '@arthurgeron/eslint-plugin-react-usememo';
2 | import js from '@eslint/js';
3 | import reactPlugin from 'eslint-plugin-react';
4 | import reactHooksPlugin from 'eslint-plugin-react-hooks';
5 |
6 | export default [
7 | js.configs.recommended,
8 | // React configuration
9 | {
10 | files: ['**/*.js', '**/*.jsx'],
11 | plugins: {
12 | react: reactPlugin,
13 | 'react-hooks': reactHooksPlugin
14 | },
15 | languageOptions: {
16 | ecmaVersion: 2022,
17 | sourceType: 'module',
18 | globals: {
19 | // Common browser globals
20 | document: 'readonly',
21 | window: 'readonly',
22 | console: 'readonly'
23 | },
24 | // Parser options for JSX
25 | parserOptions: {
26 | ecmaFeatures: {
27 | jsx: true
28 | }
29 | },
30 | },
31 | settings: {
32 | react: {
33 | version: 'detect'
34 | }
35 | },
36 | rules: {
37 | // React rules
38 | 'react/react-in-jsx-scope': 'off',
39 | 'react-hooks/rules-of-hooks': 'error',
40 | 'react-hooks/exhaustive-deps': 'warn'
41 | }
42 | },
43 | // The plugin's flat config recommended settings
44 | {
45 | files: ['**/*.js', '**/*.jsx'],
46 | plugins: {
47 | '@arthurgeron/react-usememo': flatConfig
48 | },
49 | rules: {
50 | // require-usememo rule with custom options
51 | '@arthurgeron/react-usememo/require-usememo': ['error', {
52 | strict: true,
53 | checkHookReturnObject: true,
54 | fix: { addImports: true },
55 | checkHookCalls: true,
56 | ignoredHookCallsNames: { 'useStateManagement': true },
57 | ignoredPropNames: ['style', 'className']
58 | }],
59 |
60 | // require-memo rule with custom options
61 | '@arthurgeron/react-usememo/require-memo': ['warn', {
62 | ignoredComponents: {
63 | 'Header': true,
64 | 'Footer': true,
65 | 'SimpleText': true
66 | }
67 | }],
68 |
69 | // require-usememo-children rule with custom options
70 | '@arthurgeron/react-usememo/require-usememo-children': ['warn', {
71 | strict: true
72 | }]
73 | }
74 | }
75 | ];
--------------------------------------------------------------------------------
/examples/v9-flat/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "eslint-plugin-react-usememo-v9-example",
3 | "version": "1.0.0",
4 | "description": "Example React project with ESLint v9 flat config",
5 | "main": "src/index.js",
6 | "type": "module",
7 | "scripts": {
8 | "start": "react-scripts start",
9 | "build": "react-scripts build",
10 | "test": "react-scripts test",
11 | "lint": "eslint src"
12 | },
13 | "dependencies": {
14 | "react": "^18.2.0",
15 | "react-dom": "^18.2.0",
16 | "react-scripts": "5.0.1"
17 | },
18 | "devDependencies": {
19 | "@arthurgeron/eslint-plugin-react-usememo": "../../",
20 | "@eslint/js": "^9.0.0",
21 | "eslint": "^9.0.0",
22 | "eslint-plugin-react": "^7.33.2",
23 | "eslint-plugin-react-hooks": "^4.6.0"
24 | },
25 | "browserslist": {
26 | "production": [
27 | ">0.2%",
28 | "not dead",
29 | "not op_mini all"
30 | ],
31 | "development": [
32 | "last 1 chrome version",
33 | "last 1 firefox version",
34 | "last 1 safari version"
35 | ]
36 | }
37 | }
--------------------------------------------------------------------------------
/examples/v9-flat/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useMemo, useCallback } from 'react';
2 | import { Custom } from './Custom';
3 | // This component demonstrates a component that would benefit from useMemo and memo
4 | function CountDisplay({ count, label, onClick }) {
5 | console.log(`Rendering CountDisplay with count: ${count}`);
6 |
7 | // This complex object should be memoized to prevent unnecessary re-renders
8 | const styles = useMemo(() => ({
9 | container: {
10 | padding: '20px',
11 | margin: '10px',
12 | border: '1px solid #ccc',
13 | borderRadius: '5px',
14 | backgroundColor: count % 2 === 0 ? '#f0f0f0' : '#e0e0e0'
15 | },
16 | text: {
17 | color: count > 5 ? 'red' : 'blue',
18 | fontWeight: 'bold'
19 | }
20 | }), [count]);
21 |
22 | // This should trigger a lint error without useMemo because it creates a new object each render
23 | const data = {
24 | name: 'John',
25 | age: 30
26 | };
27 |
28 | // This should trigger a lint error - a function that should use useCallback
29 | const handleClick = () => {
30 | console.log('Clicked!', count);
31 | onClick();
32 | };
33 |
34 | // This should trigger a lint error - an array that should use useMemo
35 | const items = ['item1', 'item2', 'item3'];
36 |
37 | return (
38 |
39 |
{label}: {count}
40 |
41 |
Increment
42 |
43 | {items.map(item => {item} )}
44 |
45 |
46 | );
47 | }
48 |
49 | // Wrap with React.memo to prevent re-renders when props don't change
50 | const MemoizedCountDisplay = React.memo(CountDisplay);
51 |
52 | // This component should trigger errors because it's not wrapped in React.memo
53 | export function UnmemoizedComponent({ value }) {
54 | // Add some complexity to make sure it triggers the require-memo rule
55 | const formattedValue = `Value: ${value}`;
56 | const styles = {
57 | container: {
58 | padding: '10px',
59 | margin: '5px',
60 | border: '1px solid #ddd',
61 | borderRadius: '3px'
62 | }
63 | };
64 |
65 | return (
66 |
67 |
{formattedValue}
68 |
This component should be memoized
69 |
70 | );
71 | }
72 |
73 | // Won't trigger ESLint errors because it's in ignoredComponents
74 | export function Header({ title }) {
75 | return (
76 |
79 | );
80 | }
81 | // Won't trigger ESLint errors because it's in ignoredComponents
82 | export function Footer() {
83 | return (
84 |
85 | © 2023 My Application
86 |
87 | );
88 | }
89 |
90 | export function SimpleText({ text }) {
91 | return {text}
;
92 | }
93 |
94 | // A component with complex children that should trigger the require-usememo-children rule
95 | function ComplexChildContainer({ children }) {
96 | return (
97 |
98 | {children}
99 |
100 | );
101 | }
102 |
103 | function App() {
104 | const [count1, setCount1] = useState(0);
105 | const [count2, setCount2] = useState(0);
106 |
107 | // Using useCallback to memoize function references
108 | const incrementCount1 = useCallback(() => {
109 | setCount1(prevCount => prevCount + 1);
110 | }, []);
111 |
112 | const incrementCount2 = useCallback(() => {
113 | setCount2(prevCount => prevCount + 1);
114 | }, []);
115 |
116 | // Example of a complex calculation that should be memoized
117 | const total = useMemo(() => {
118 | console.log('Calculating total...');
119 | // Simulating expensive calculation
120 | return count1 + count2;
121 | }, [count1, count2]);
122 |
123 | // This could trigger a lint error without useMemo because it creates a new object each render
124 | const userData = useMemo(() => ({
125 | counts: {
126 | first: count1,
127 | second: count2,
128 | total
129 | },
130 | lastUpdated: new Date().toISOString()
131 | }), [count1, count2, total]);
132 |
133 | // These objects would normally require useMemo, but won't throw errors because their props are in ignoredPropNames
134 | const customStyle = { color: 'blue', fontWeight: 'bold' };
135 | const customClassName = { primary: true, secondary: false };
136 |
137 | // Calling a hook that would normally require its arguments to be memoized
138 | const useStateManagementResult = useStateManagement({ count1, count2 });
139 |
140 | // This complex JSX structure should be memoized when used as children
141 | const complexChildren = (
142 |
143 |
Complex Children
144 |
These children should be memoized with useMemo
145 |
146 | Item 1
147 | Item 2
148 |
149 |
150 | );
151 |
152 | return (
153 |
154 |
ESLint React useMemo Plugin Example (v9 Flat Config)
155 |
156 |
Total: {total}
157 |
Last Updated: {userData.lastUpdated}
158 |
159 |
164 |
165 |
170 |
171 |
172 |
173 |
174 | This div has a style prop that would normally require useMemo
175 |
176 |
177 |
178 | This div has a className prop that would normally require useMemo
179 |
180 |
181 |
182 |
183 |
{useStateManagementResult}
184 |
185 |
186 | {complexChildren}
187 |
188 |
189 |
190 |
191 | );
192 | }
193 |
194 | // Mock hook for demonstration
195 | function useStateManagement(state) {
196 | return JSON.stringify(state);
197 | }
198 |
199 | export { MemoizedCountDisplay, ComplexChildContainer };
200 | export default App;
--------------------------------------------------------------------------------
/examples/v9-flat/src/Custom.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const Custom = React.memo(({ data }) => {
4 | return (
5 |
6 |
Custom Component
7 |
Name: {data.name}
8 |
Age: {data.age}
9 |
10 | );
11 | });
12 |
--------------------------------------------------------------------------------
/examples/v9-flat/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import App from './App';
4 |
5 | const root = ReactDOM.createRoot(document.getElementById('root'));
6 | root.render(
7 |
8 |
9 |
10 | );
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest',
3 | testEnvironment: 'node',
4 | globals: {
5 | 'ts-jest': {
6 | diagnostics: false,
7 | },
8 | },
9 | moduleNameMapper: {
10 | "src/(.*)": "/src/$1"
11 | },
12 | testPathIgnorePatterns : [
13 | "/src/index.ts",
14 | "/__tests__/(.*)/ruleTester.ts",
15 | "/__tests__/testcases/(.*)",
16 | ],
17 | collectCoverage: true,
18 | collectCoverageFrom: [
19 | '/src/*',
20 | '/src/**/*',
21 | '!/src/index.{js,ts}',
22 | '!/src/**/constants.{js,ts}',
23 | '!/src/**/flat-config.{js,ts}',
24 | '!/src/**/traditional-config.{js,ts}',
25 | ],
26 | cacheDirectory: '.jest-cache',
27 | coverageThreshold: {
28 | global: {
29 | branches: 75,
30 | functions: 80,
31 | lines: 80,
32 | statements: 75,
33 | },
34 | },
35 | };
--------------------------------------------------------------------------------
/knip.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://github.com/webpro/knip/raw/main/schema.json",
3 | "entry": ["src/index.ts"],
4 | "project": "**/*.ts",
5 | "ignoreUnusedDependencies": ["estree"],
6 | "ignoreFiles": [
7 | "src/flat-config.ts",
8 | "src/traditional-config.ts",
9 | "__tests__/testcases/*.ts"
10 | ],
11 | "rules": {
12 | "exports": "error"
13 | },
14 | "ignoreExportsUsedInFile": true
15 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@arthurgeron/eslint-plugin-react-usememo",
3 | "version": "2.5.0",
4 | "description": "",
5 | "main": "dist/index.js",
6 | "author": "Stefano J. Attardi & Arthur Geron =3.0.3"
53 | },
54 | "exports": {
55 | ".": {
56 | "import": "./dist/index.js",
57 | "require": "./dist/index.cjs"
58 | }
59 | },
60 | "eslintConfig": {
61 | "plugins": [
62 | "@arthurgeron/react-usememo"
63 | ],
64 | "rules": {
65 | "@arthurgeron/react-usememo/require-usememo": "error",
66 | "@arthurgeron/react-usememo/require-memo": "error",
67 | "@arthurgeron/react-usememo/require-usememo-children": "error"
68 | }
69 | },
70 | "peerDependencies": {
71 | "eslint": "^8.0.0 || ^9.0.0"
72 | }
73 | }
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import typescript from '@rollup/plugin-typescript';
2 | import commonjs from '@rollup/plugin-commonjs';
3 | import resolve from '@rollup/plugin-node-resolve';
4 |
5 | export default [
6 | {
7 | input: 'src/index.ts',
8 | output: [
9 | {
10 | file: 'dist/index.js',
11 | format: 'esm',
12 | sourcemap: true,
13 | },
14 | {
15 | file: 'dist/index.cjs',
16 | format: 'cjs',
17 | sourcemap: true,
18 | },
19 | ],
20 | plugins: [
21 | resolve(),
22 | commonjs(),
23 | typescript({
24 | exclude: ['**/__tests__/**', '**/*.test.ts', '**/testcases/**']
25 | })
26 | ],
27 | external: ['eslint', '@typescript-eslint/types'],
28 | },
29 | ];
30 |
--------------------------------------------------------------------------------
/src/constants/messages.ts:
--------------------------------------------------------------------------------
1 | export const MessagesRequireUseMemo = {
2 | "error-in-invalid-context": "An error was identified inside this expression, but it can't be fixed because it'd break the rule of hooks. JSX logic should be extracted into a separate component.",
3 | "object-usememo-props":
4 | "Object literal should be wrapped in useMemo() or be static when used as a prop",
5 | "object-class-memo-props":
6 | "Object literal should come from state or be static when used as a prop",
7 | "object-usememo-hook":
8 | "Object literal should come from state or be static when returned from a hook",
9 | "object-usememo-deps":
10 | "Object literal should be wrapped in useMemo() or be static when used as a hook dependency",
11 | "array-usememo-props":
12 | "Array literal should be wrapped in useMemo() or be static when used as a prop",
13 | "array-usememo-hook":
14 | "Array literal should be wrapped in useMemo() or be static when returned from a hook",
15 | "array-class-memo-props":
16 | "Array literal should be from state and declared in state, constructor, getDerivedStateFromProps or statically when used as a prop",
17 | "array-usememo-deps":
18 | "Array literal should be wrapped in useMemo() or be static when used as a hook dependency",
19 | "instance-usememo-props":
20 | "Object instantiation should be wrapped in useMemo() or be static when used as a prop",
21 | "instance-usememo-hook":
22 | "Object instantiation should be wrapped in useMemo() or be static when returned from a hook",
23 | "instance-class-memo-props":
24 | "Object instantiation should be done in state, constructor, getDerivedStateFromProps or statically when used as a prop",
25 | "instance-usememo-deps":
26 | "Object instantiation should be wrapped in useMemo() or be static when used as a hook dependency",
27 | "jsx-usememo-props":
28 | "JSX should be wrapped in useMemo() when used as a prop",
29 | "jsx-usememo-hook":
30 | "JSX should be wrapped in useMemo() when returned from a hook",
31 | "jsx-usememo-deps":
32 | "JSX should be wrapped in useMemo() when used as a hook dependency",
33 | "function-usecallback-props":
34 | "Function definition should be wrapped in useCallback() or be static when used as a prop",
35 | "function-usecallback-hook":
36 | "Function definition should be wrapped in useCallback() or be static when returned from a hook",
37 | "function-class-props":
38 | "Function definition should declared as a class property or statically when used as a prop",
39 | "function-usecallback-deps":
40 | "Function definition should be wrapped in useCallback() or be static when used as a hook dependency",
41 | "unknown-usememo-props":
42 | "Unknown value may need to be wrapped in useMemo() when used as a prop",
43 | "unknown-usememo-hook":
44 | "Unknown value may need to be wrapped in useMemo() when returned from a hook",
45 | "unknown-class-memo-props":
46 | "Unknown value should be declared in state, constructor, getDerivedStateFromProps or statically when used as a prop",
47 | "unknown-usememo-deps":
48 | "Unknown value may need to be wrapped in useMemo() when used as a hook dependency",
49 | "usememo-const":
50 | "useMemo/useCallback return value should be assigned to a `const` to prevent reassignment",
51 | } as const;
52 |
53 | export const MessagesRequireUseMemoChildren = {
54 | "object-usememo-children":
55 | "Object literal should be wrapped in React's useMemo() when used as children",
56 | "array-usememo-children":
57 | "Array literal should be wrapped in React's useMemo() when used as children",
58 | "instance-usememo-children":
59 | "Object instantiation should be wrapped in React's useMemo() when used as children",
60 | "jsx-usememo-children":
61 | "JSX should be wrapped in React's useMemo() when used as children",
62 | "function-usecallback-children":
63 | "Function definition should be wrapped in useCallback() when used as children",
64 | "unknown-usememo-children":
65 | "Unknown value may need to be wrapped in React's useMemo() when used as children",
66 | "usememo-const":
67 | "useMemo/useCallback return value should be assigned to a `const` to prevent reassignment",
68 | };
69 |
70 |
--------------------------------------------------------------------------------
/src/flat-config.ts:
--------------------------------------------------------------------------------
1 | import requireMemoRule from "./require-memo";
2 | import requireUseMemoRule from "./require-usememo/index";
3 | import requireUseMemoChildrenRule from "./require-usememo-children";
4 |
5 | /**
6 | * Plugin definition for ESLint flat config format (ESLint V9)
7 | */
8 | export const flatConfig = {
9 | meta: {
10 | name: '@arthurgeron/eslint-plugin-react-usememo',
11 | version: '2.5.0'
12 | },
13 | rules: {
14 | 'require-memo': requireMemoRule,
15 | 'require-usememo': requireUseMemoRule,
16 | 'require-usememo-children': requireUseMemoChildrenRule,
17 | }
18 | };
19 |
20 | // Add configs after defining the plugin to reference the plugin itself
21 | Object.defineProperty(flatConfig, 'configs', {
22 | value: {
23 | recommended: {
24 | plugins: {
25 | '@arthurgeron/react-usememo': flatConfig
26 | },
27 | rules: {
28 | '@arthurgeron/react-usememo/require-usememo': 'error',
29 | '@arthurgeron/react-usememo/require-memo': 'error',
30 | '@arthurgeron/react-usememo/require-usememo-children': 'error',
31 | },
32 | },
33 | }
34 | });
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { configs, rules } from "./traditional-config";
2 | import { flatConfig } from "./flat-config";
3 |
4 | export { configs, rules } from "./traditional-config";
5 | export { flatConfig } from "./flat-config";
6 |
7 | export default {
8 | rules,
9 | configs,
10 | flatConfig,
11 | };
--------------------------------------------------------------------------------
/src/require-memo/index.ts:
--------------------------------------------------------------------------------
1 | import type { Rule } from "eslint-v9";
2 | import type { Rule as RuleV8 } from "eslint";
3 | import { findVariable, isComponentName } from "../utils";
4 | import { checkVariableDeclaration, checkFunction } from "./utils";
5 | import type { MemoFunctionDeclaration, MemoFunctionExpression } from "./types";
6 | import type { TSESTree } from "@typescript-eslint/types";
7 | import { getCompatibleScope, type CompatibleNode } from "../utils/compatibility";
8 | import type { CompatibleRuleModule } from "../utils/compatibility";
9 |
10 | const rule: CompatibleRuleModule = {
11 | meta: {
12 | messages: {
13 | "memo-required": "Component definition not wrapped in React.memo()",
14 | },
15 | schema: [
16 | {
17 | type: "object",
18 | properties: { ignoredComponents: { type: "object" } },
19 | additionalProperties: false,
20 | },
21 | ],
22 | },
23 | create: (context: RuleV8.RuleContext | Rule.RuleContext) => {
24 | return {
25 | ExportNamedDeclaration(node: CompatibleNode) {
26 | // Use type assertions to help TypeScript understand the node structure
27 | const tsNode = node as TSESTree.ExportNamedDeclaration;
28 | // Check if the node has declaration and is a function
29 | if (
30 | tsNode.declaration &&
31 | tsNode.declaration.type === "VariableDeclaration"
32 | ) {
33 | const declarations = tsNode.declaration.declarations;
34 | for (const declaration of declarations) {
35 | if (
36 | isComponentName((declaration?.id as TSESTree.Identifier)?.name)
37 | ) {
38 | checkVariableDeclaration(
39 | context,
40 | declaration,
41 | );
42 | }
43 | }
44 | return;
45 | }
46 | if (tsNode.declaration?.type === "FunctionDeclaration") {
47 | checkFunction(context, tsNode.declaration as MemoFunctionDeclaration);
48 | }
49 | },
50 | ExportDefaultDeclaration(node: CompatibleNode) {
51 | // Use type assertions to help TypeScript understand the node structure
52 | const tsNode = node as TSESTree.ExportDefaultDeclaration;
53 |
54 | // Handle default export of arrow functions and function expressions directly
55 | if (
56 | tsNode.declaration.type === "ArrowFunctionExpression" ||
57 | tsNode.declaration.type === "FunctionExpression"
58 | ) {
59 | // Direct export default of a function should use memo
60 | context.report({
61 | node: tsNode.declaration as any,
62 | messageId: "memo-required",
63 | });
64 | return;
65 | }
66 |
67 | // It was exported in one place but declared elsewhere
68 | if (tsNode.declaration.type === "Identifier") {
69 | const scope = getCompatibleScope(context, node);
70 | if (scope) {
71 | const variable = findVariable(scope, tsNode.declaration);
72 |
73 | if (variable?.defs[0]?.type === "Variable") {
74 | const variableNode = variable.defs[0].node;
75 |
76 | if (
77 | isComponentName((variableNode.id as TSESTree.Identifier).name)
78 | ) {
79 | checkVariableDeclaration(
80 | context,
81 | variableNode,
82 | );
83 | }
84 | }
85 | }
86 | return;
87 | }
88 | if (tsNode.declaration.type === "FunctionDeclaration") {
89 | checkFunction(context, tsNode.declaration as MemoFunctionDeclaration);
90 | }
91 | },
92 | };
93 | },
94 | };
95 |
96 | export default rule;
97 |
--------------------------------------------------------------------------------
/src/require-memo/types.ts:
--------------------------------------------------------------------------------
1 | import type * as ESTree from "estree";
2 | import type { Rule } from "eslint";
3 |
4 | export type MemoVariableIdentifier = ESTree.Identifier & Rule.NodeParentExtension;
5 | export type MemoFunctionExpression = (ESTree.FunctionExpression | ESTree.ArrowFunctionExpression) & Rule.NodeParentExtension;
6 | export type MemoFunctionDeclaration = ESTree.FunctionDeclaration & Rule.NodeParentExtension;
--------------------------------------------------------------------------------
/src/require-memo/utils.ts:
--------------------------------------------------------------------------------
1 | import type { Rule } from "eslint";
2 | import type { Rule as RuleV9 } from "eslint-v9";
3 | import type * as ESTree from "estree";
4 | import { isComponentName, shouldIgnoreNode } from "../utils";
5 | import * as path from "node:path";
6 | import { getCompatibleScope, getCompatibleFilename } from "../utils/compatibility";
7 |
8 | import type { MemoFunctionExpression, MemoVariableIdentifier } from "./types";
9 | import type { ESNode } from "src/types";
10 |
11 | function isMemoCallExpression(node: Rule.Node) {
12 | if (node.type !== "CallExpression") return false;
13 | if (node.callee?.type === "MemberExpression") {
14 | const {
15 | callee: { object, property },
16 | } = node;
17 | if (
18 | object.type === "Identifier" &&
19 | property.type === "Identifier" &&
20 | object.name === "React" &&
21 | property.name === "memo"
22 | ) {
23 | return true;
24 | }
25 | } else if (
26 | node.callee?.type === "Identifier" &&
27 | node.callee?.name === "memo"
28 | ) {
29 | return true;
30 | }
31 |
32 | return false;
33 | }
34 |
35 | export function checkFunction(
36 | context: Rule.RuleContext | RuleV9.RuleContext,
37 | node: (
38 | | ESTree.ArrowFunctionExpression
39 | | ESTree.FunctionExpression
40 | | ESTree.FunctionDeclaration
41 | | ESTree.Identifier
42 | ) &
43 | (Rule.NodeParentExtension | RuleV9.NodeParentExtension),
44 | ) {
45 | const ignoredNames = context.options?.[0]?.ignoredComponents;
46 | let currentNode = node.type === "FunctionDeclaration" ? node : node.parent;
47 | while (currentNode.type === "CallExpression") {
48 | if (isMemoCallExpression(currentNode)) {
49 | return;
50 | }
51 |
52 | currentNode = currentNode.parent;
53 | }
54 |
55 | if (
56 | currentNode.type === "VariableDeclarator" ||
57 | currentNode.type === "FunctionDeclaration"
58 | ) {
59 | const { id } = currentNode;
60 | if (id?.type === "Identifier") {
61 | if (
62 | isComponentName(id?.name) &&
63 | (!ignoredNames ||
64 | !shouldIgnoreNode(id as unknown as ESNode, ignoredNames))
65 | ) {
66 | context.report({ node, messageId: "memo-required" });
67 | }
68 | }
69 | } else if (
70 | node.type === "FunctionDeclaration" &&
71 | currentNode.type === "Program"
72 | ) {
73 | if (
74 | ignoredNames &&
75 | !shouldIgnoreNode(node as unknown as ESNode, ignoredNames)
76 | ) {
77 | return;
78 | }
79 | if (node.id !== null && isComponentName(node.id?.name)) {
80 | context.report({ node, messageId: "memo-required" });
81 | } else {
82 | if (getCompatibleFilename(context) === " ") return;
83 | const filename = path.basename(getCompatibleFilename(context));
84 | if (isComponentName(filename)) {
85 | context.report({ node, messageId: "memo-required" });
86 | }
87 | }
88 | }
89 | }
90 |
91 | export function checkVariableDeclaration(
92 | context: Rule.RuleContext | RuleV9.RuleContext,
93 | declaration: any, // Using any to bypass type incompatibilities between ESLint v8 and v9
94 | ) {
95 | if (declaration.init) {
96 | if (declaration.init.type === "CallExpression") {
97 | const declarationProperties = (
98 | (declaration.init.callee as MemoVariableIdentifier).name
99 | ? declaration.init.callee
100 | : (declaration.init.callee as ESTree.MemberExpression).property
101 | ) as MemoVariableIdentifier;
102 | if (declarationProperties?.name === "memo") {
103 | checkFunction(
104 | context,
105 | declaration.init.arguments[0] as MemoVariableIdentifier,
106 | );
107 | return;
108 | }
109 | } else if (
110 | declaration.init.type === "ArrowFunctionExpression" ||
111 | declaration.init.type === "FunctionExpression"
112 | ) {
113 | checkFunction(context, declaration.init as MemoFunctionExpression);
114 | return;
115 | }
116 | }
117 | }
118 |
119 |
120 |
--------------------------------------------------------------------------------
/src/require-usememo-children.ts:
--------------------------------------------------------------------------------
1 | import type { Rule } from "eslint";
2 | import type { Rule as RuleV9 } from "eslint-v9";
3 | import type * as ESTree from "estree";
4 | import type { TSESTree } from "@typescript-eslint/types";
5 | import {
6 | getExpressionMemoStatus,
7 | isComplexComponent,
8 | } from "./utils";
9 | import { MessagesRequireUseMemoChildren } from "./constants/messages";
10 | import { MemoStatus } from "src/types";
11 | import type { CompatibleContext } from "./require-usememo/utils";
12 | import type { CompatibleNode, CompatibleRuleModule } from "./utils/compatibility";
13 |
14 | const rule: CompatibleRuleModule = {
15 | meta: {
16 | messages: MessagesRequireUseMemoChildren,
17 | schema: [
18 | {
19 | type: "object",
20 | properties: { strict: { type: "boolean" } },
21 | additionalProperties: false,
22 | },
23 | ],
24 | },
25 | create: (context: CompatibleContext) => {
26 | let isClass = false;
27 | function report(node: CompatibleNode, messageId: keyof typeof MessagesRequireUseMemoChildren) {
28 | context.report({ node: node as any, messageId: messageId as string });
29 | }
30 |
31 | return {
32 | ClassDeclaration: () => {
33 | isClass = true;
34 | },
35 |
36 | JSXElement: (node: CompatibleNode) => {
37 | const tsNode = node as unknown as TSESTree.JSXElement;
38 | const {
39 | children,
40 | openingElement,
41 | } = tsNode;
42 | if (isClass || !isComplexComponent(openingElement)) return;
43 |
44 | for (const child of children) {
45 | if (child.type === "JSXElement" || child.type === "JSXFragment") {
46 | report(node, "jsx-usememo-children");
47 | return;
48 | }
49 | if (child.type === "JSXExpressionContainer") {
50 | const { expression } = child;
51 | if (expression.type !== "JSXEmptyExpression") {
52 | const statusData = getExpressionMemoStatus(context, expression);
53 | switch (statusData?.status) {
54 | case MemoStatus.UnmemoizedObject:
55 | report(node, "object-usememo-children");
56 | break;
57 | case MemoStatus.UnmemoizedArray:
58 | report(node, "array-usememo-children");
59 | break;
60 | case MemoStatus.UnmemoizedNew:
61 | report(node, "instance-usememo-children");
62 | break;
63 | case MemoStatus.UnmemoizedFunction:
64 | report(node, "function-usecallback-children");
65 | break;
66 | case MemoStatus.UnmemoizedFunctionCall:
67 | case MemoStatus.UnmemoizedOther:
68 | if (context.options?.[0]?.strict) {
69 | report(node, "unknown-usememo-children");
70 | }
71 | break;
72 | case MemoStatus.UnmemoizedJSX:
73 | report(node, "jsx-usememo-children");
74 | break;
75 | }
76 | }
77 | }
78 | }
79 | },
80 | };
81 | },
82 | };
83 |
84 | export default rule;
85 |
--------------------------------------------------------------------------------
/src/require-usememo/constants.ts:
--------------------------------------------------------------------------------
1 | import { MemoStatus } from 'src/types';
2 | import type { ExpressionData, MemoErrorHookDictionary } from './types';
3 |
4 | export const defaultImportRangeStart = `import { `;
5 | export const nameGeneratorUUID = 'b32a4d70-4f64-11eb-89d5-33e6ce8a6c99';
6 | export const jsxEmptyExpressionClassData: ExpressionData = {
7 | [MemoStatus.UnmemoizedObject.toString()]: "object-class-memo-props",
8 | [MemoStatus.UnmemoizedArray.toString()]: "array-class-memo-props",
9 | [MemoStatus.UnmemoizedNew.toString()]: "instance-class-memo-props",
10 | [MemoStatus.UnmemoizedFunction.toString()]: 'instance-class-memo-props',
11 | [MemoStatus.UnmemoizedFunctionCall.toString()]: "unknown-class-memo-props",
12 | [MemoStatus.UnmemoizedOther.toString()]: "unknown-class-memo-props",
13 | [MemoStatus.UnsafeLet.toString()]: "usememo-const",
14 | }
15 |
16 | export const jsxEmptyExpressionData: ExpressionData = {
17 | [MemoStatus.UnmemoizedObject.toString()]: "object-usememo-props",
18 | [MemoStatus.UnmemoizedArray.toString()]: "array-usememo-props",
19 | [MemoStatus.UnmemoizedNew.toString()]: "instance-usememo-props",
20 | [MemoStatus.UnmemoizedFunction.toString()]: "function-usecallback-props",
21 | [MemoStatus.UnmemoizedFunctionCall.toString()]: "unknown-usememo-props",
22 | [MemoStatus.UnmemoizedOther.toString()]: "unknown-usememo-props",
23 | [MemoStatus.UnmemoizedJSX.toString()]: "jsx-usememo-props",
24 | [MemoStatus.UnsafeLet.toString()]: "usememo-const",
25 | }
26 |
27 | export const hookReturnExpressionData: ExpressionData = {
28 | [MemoStatus.UnmemoizedObject.toString()]: "object-usememo-hook",
29 | [MemoStatus.UnmemoizedArray.toString()]: "array-usememo-hook",
30 | [MemoStatus.UnmemoizedNew.toString()]: "instance-usememo-hook",
31 | [MemoStatus.UnmemoizedFunction.toString()]: "function-usecallback-hook",
32 | [MemoStatus.UnmemoizedFunctionCall.toString()]: "unknown-usememo-hook",
33 | [MemoStatus.UnmemoizedOther.toString()]: "unknown-usememo-hook",
34 | [MemoStatus.UnmemoizedJSX.toString()]: "jsx-usememo-hook",
35 | [MemoStatus.UnsafeLet.toString()]: "usememo-const",
36 | }
37 |
38 | export const callExpressionData: ExpressionData = {
39 | [MemoStatus.UnmemoizedObject.toString()]: "object-usememo-deps",
40 | [MemoStatus.UnmemoizedArray.toString()]: "array-usememo-deps",
41 | [MemoStatus.UnmemoizedNew.toString()]: "instance-usememo-deps",
42 | [MemoStatus.UnmemoizedFunction.toString()]: "function-usecallback-deps",
43 | [MemoStatus.UnmemoizedFunctionCall.toString()]: "unknown-usememo-deps",
44 | [MemoStatus.UnmemoizedOther.toString()]: "unknown-usememo-deps",
45 | [MemoStatus.UnmemoizedJSX.toString()]: "jsx-usememo-deps",
46 | [MemoStatus.UnsafeLet.toString()]: "usememo-const",
47 | }
48 |
49 | export const defaultReactHookNames: Record = {
50 | "useContext": true,
51 | "useState": true,
52 | "useReducer": true,
53 | "useRef": true,
54 | "useLayoutEffect": true,
55 | "useEffect": true,
56 | "useImperativeHandle": true,
57 | "useCallback": true,
58 | "useMemo": true,
59 | "useDebugValue": true,
60 | "useDeferredValue": true,
61 | "useTransition": true,
62 | "useId": true,
63 | "useInsertionEffect": true,
64 | "useSyncExternalStore": true,
65 | "useQuery": true,
66 | "useMutation": true,
67 | "useQueryClient": true,
68 | "useInfiniteQuery": true,
69 | }
70 |
71 | export const messageIdToHookDict: MemoErrorHookDictionary = {
72 | 'function-usecallback-props': 'useCallback',
73 | 'function-usecallback-hook': 'useCallback',
74 | 'function-usecallback-deps': 'useCallback',
75 | 'object-usememo-props': 'useMemo',
76 | 'usememo-const': 'useMemo',
77 | };
78 |
--------------------------------------------------------------------------------
/src/require-usememo/index.ts:
--------------------------------------------------------------------------------
1 | import type { Rule } from "eslint-v9";
2 | import type { Rule as RuleV8 } from "eslint";
3 | import type { TSESTree } from "@typescript-eslint/types";
4 | import { defaultReactHookNames, jsxEmptyExpressionClassData, jsxEmptyExpressionData, callExpressionData, hookReturnExpressionData } from './constants';
5 | import { MessagesRequireUseMemo } from '../constants/messages';
6 | import {
7 | getExpressionMemoStatus,
8 | isComplexComponent,
9 | shouldIgnoreNode,
10 | } from "../utils";
11 | import type {ExpressionTypes, NodeType, ExpressionData, ReactImportInformation, ImportNode} from './types';
12 | import { checkForErrors, fixBasedOnMessageId, getIsHook, type CompatibleContext } from './utils';
13 | import { type ESNode, MemoStatus } from "src/types";
14 | import { getCompatibleScope, type CompatibleNode } from "../utils/compatibility";
15 | import type { CompatibleRuleModule } from "../utils/compatibility";
16 |
17 | const rule: CompatibleRuleModule = {
18 | meta: {
19 | type: 'problem',
20 | messages: MessagesRequireUseMemo,
21 | docs: {
22 | description: 'Detects shallow comparison fails in React',
23 | recommended: true,
24 | },
25 | fixable: 'code',
26 | schema: [
27 | {
28 | type: "object",
29 | properties: { strict: { type: "boolean" }, checkHookReturnObject: { type: "boolean" }, checkHookCalls: { type: "boolean"}, ignoredHookCallsNames: {type: "object"}, fix: {
30 | addImports: "boolean",
31 | }, ignoredPropNames: { type: "array" } },
32 | additionalProperties: false,
33 | },
34 | ],
35 | },
36 | create: (context: CompatibleContext) => {
37 | let isClass = false;
38 | const importData: ReactImportInformation = {
39 | reactImported: false,
40 | useMemoImported: false,
41 | useCallbackImported: false,
42 | }
43 |
44 | function report(node: T, messageId: keyof typeof MessagesRequireUseMemo) {
45 | context.report({
46 | node: node as unknown as Rule.Node,
47 | messageId,
48 | fix(fixer) {
49 | const disableFixer = isClass || messageId === MemoStatus.ErrorInvalidContext;
50 | return disableFixer ? null : fixBasedOnMessageId(node as Rule.Node, messageId, fixer, context, importData);
51 | }
52 | });
53 | }
54 |
55 | function process(node: NodeType, _expression?: ExpressionTypes, expressionData?: ExpressionData, checkContext = false) {
56 | const isGlobalScope = getCompatibleScope(context, node)?.block.type === 'Program';
57 |
58 | if (checkContext && isGlobalScope) {
59 | return;
60 | }
61 |
62 | const expression = _expression ?? (node.value && Object.prototype.hasOwnProperty.call(node.value, 'expression') ? (node.value as unknown as TSESTree.JSXExpressionContainer).expression : node.value) ;
63 | switch(expression?.type) {
64 | case 'LogicalExpression':
65 | !expression.left ? true : process(node, (expression as TSESTree.LogicalExpression).left);
66 | !expression.right ? true : process(node, (expression as TSESTree.LogicalExpression).right);
67 | return;
68 | case 'JSXEmptyExpression':
69 | return;
70 | default:
71 | checkForErrors(
72 | expressionData || (isClass ? jsxEmptyExpressionClassData : jsxEmptyExpressionData),
73 | getExpressionMemoStatus(context as Rule.RuleContext, expression as TSESTree.Expression, checkContext),
74 | context,
75 | node,
76 | report
77 | );
78 | return;
79 | }
80 | }
81 |
82 | function JSXAttribute(node: CompatibleNode) {
83 | const ignoredPropNames = context.options?.[0]?.ignoredPropNames ?? [];
84 | const tsNode = node as unknown as TSESTree.JSXAttribute;
85 |
86 | const { parent, value } = tsNode;
87 | if (value === null) return null;
88 | if (parent && !isComplexComponent(parent as TSESTree.JSXIdentifier)) return null;
89 | if ((value.type as string) === "JSXExpressionContainer") {
90 | if (ignoredPropNames.includes(tsNode.name?.name)) {
91 | return null;
92 | }
93 | process(node as unknown as NodeType, undefined, undefined, true);
94 | }
95 | return null;
96 | }
97 |
98 | return {
99 | JSXAttribute,
100 |
101 | ClassDeclaration: () => {
102 | isClass = true;
103 | },
104 |
105 | ImportDeclaration(node: CompatibleNode) {
106 | const tsNode = node as TSESTree.ImportDeclaration;
107 | if (tsNode.source.value === 'react' && tsNode.importKind !== 'type') {
108 | importData.reactImported = true;
109 | const specifiers = Array.isArray(tsNode.specifiers) ? tsNode.specifiers.slice() : tsNode.specifiers;
110 | importData.importDeclaration = Object.assign({}, tsNode, {specifiers}) as TSESTree.ImportDeclaration;
111 | importData.useMemoImported = specifiers.some(specifier => specifier.local.name === 'useMemo');
112 | importData.useCallbackImported = specifiers.some(specifier => specifier.local.name === 'useCallback');
113 | }
114 | },
115 |
116 | ReturnStatement(node: CompatibleNode) {
117 | const tsNode = node as TSESTree.ReturnStatement;
118 | const functionDeclarationNode = tsNode.parent?.parent?.type === 'FunctionDeclaration' && tsNode?.parent?.parent?.id;
119 | const anonFuncVariableDeclarationNode = tsNode.parent?.parent?.type === 'ArrowFunctionExpression' && tsNode?.parent?.parent?.parent?.type === 'VariableDeclarator' && tsNode?.parent?.parent?.parent?.id;
120 | const validNode = functionDeclarationNode || anonFuncVariableDeclarationNode;
121 |
122 | if (validNode && getIsHook(validNode as TSESTree.Identifier) && tsNode.argument) {
123 | if (tsNode.argument.type === 'ObjectExpression') {
124 | if (context.options?.[0]?.checkHookReturnObject) {
125 | report(node as unknown as Rule.NodeParentExtension, "object-usememo-hook");
126 | return;
127 | }
128 | const objExp = (tsNode.argument as TSESTree.ObjectExpression);
129 |
130 | // Replace forEach with for...of to fix linting error
131 | for (const _node of objExp.properties) {
132 | process(
133 | _node as unknown as NodeType,
134 | (_node as TSESTree.Property).value as unknown as ExpressionTypes,
135 | hookReturnExpressionData
136 | );
137 | }
138 | return;
139 | }
140 | process(
141 | node as unknown as NodeType,
142 | tsNode.argument as unknown as ExpressionTypes,
143 | hookReturnExpressionData
144 | );
145 | }
146 | },
147 |
148 | CallExpression: (node: CompatibleNode) => {
149 | const tsNode = node as TSESTree.CallExpression;
150 | const { callee } = tsNode;
151 | const ignoredNames = context.options?.[0]?.ignoredHookCallsNames ?? {};
152 |
153 | if (
154 | context.options?.[0]?.checkHookCalls === false
155 | || !getIsHook(callee as ESNode)
156 | ) {
157 | return;
158 | }
159 |
160 | if(!shouldIgnoreNode(node as ESNode, {...defaultReactHookNames, ...ignoredNames})) {
161 | for (const argument of tsNode.arguments) {
162 | if (argument.type !== 'SpreadElement') {
163 | checkForErrors(
164 | callExpressionData,
165 | getExpressionMemoStatus(context as Rule.RuleContext, (argument as TSESTree.Expression)),
166 | context,
167 | node as Rule.Node,
168 | report
169 | );
170 | }
171 | }
172 | }
173 | },
174 | };
175 | },
176 | };
177 |
178 | export default rule;
--------------------------------------------------------------------------------
/src/require-usememo/types.ts:
--------------------------------------------------------------------------------
1 | import type { Rule } from "eslint-v9";
2 | import type { TSESTree } from "@typescript-eslint/types";
3 | import type { MessagesRequireUseMemo} from '../constants/messages';
4 |
5 | export type ExpressionTypes = TSESTree.ArrowFunctionExpression | TSESTree.JSXExpressionContainer | TSESTree.Expression | TSESTree.ObjectExpression | TSESTree.ArrayExpression | TSESTree.Identifier | TSESTree.LogicalExpression | TSESTree.JSXEmptyExpression;
6 |
7 | export type NodeType = TSESTree.MethodDefinitionComputedName;
8 | export type ExpressionData = Record;
9 | type OptionalRecord = {
10 | [P in K]?: T;
11 | };
12 | type PartialKeyOfMessages = keyof typeof MessagesRequireUseMemo;
13 | export type MemoErrorHookDictionary = OptionalRecord;
14 | export type ImportNode = TSESTree.ImportDeclaration & Rule.NodeParentExtension;
15 | export type ReactImportInformation = {
16 | reactImported: boolean;
17 | useMemoImported: boolean;
18 | useCallbackImported: boolean;
19 | importDeclaration?: TSESTree.ImportDeclaration;
20 | };
--------------------------------------------------------------------------------
/src/require-usememo/utils.ts:
--------------------------------------------------------------------------------
1 | import type { Rule } from "eslint";
2 | import type { Rule as RuleV9 } from "eslint-v9";
3 | import type { TSESTree } from "@typescript-eslint/types";
4 | import type * as ESTree from "estree";
5 | import type { MessagesRequireUseMemo } from "../constants/messages";
6 | import type { ExpressionData, ReactImportInformation } from "./types";
7 | import { MemoStatus, type MemoStatusToReport } from "src/types";
8 | import {
9 | messageIdToHookDict,
10 | nameGeneratorUUID,
11 | defaultImportRangeStart,
12 | } from "./constants";
13 | import getVariableInScope from "src/utils/getVariableInScope";
14 | import { v5 as uuidV5 } from "uuid";
15 | import type { CompatibleNode } from "../utils/compatibility";
16 |
17 | export type CompatibleContext = Rule.RuleContext | RuleV9.RuleContext;
18 |
19 | export function isImpossibleToFix(
20 | node: Rule.NodeParentExtension | RuleV9.NodeParentExtension,
21 | context: CompatibleContext,
22 | ) {
23 | let current: TSESTree.Node | undefined = node as TSESTree.Node;
24 |
25 | while (current) {
26 | if (current.type === "CallExpression") {
27 | const callee = current.callee;
28 | const isInsideIteration =
29 | callee.type === "MemberExpression" &&
30 | callee.property.type === "Identifier" &&
31 | callee.property.name in Array.prototype;
32 | const isInsideOtherHook =
33 | callee.type === "Identifier" &&
34 | (callee.name === "useMemo" || callee.name === "useCallback");
35 | return { result: isInsideIteration || isInsideOtherHook, node: callee };
36 | }
37 | current = current.parent;
38 | }
39 |
40 | return { result: false };
41 | }
42 |
43 | export function checkForErrors<
44 | T,
45 | Y extends
46 | | Rule.NodeParentExtension
47 | | RuleV9.NodeParentExtension
48 | | TSESTree.MethodDefinitionComputedName,
49 | >(
50 | data: ExpressionData,
51 | statusData: MemoStatusToReport,
52 | context: CompatibleContext,
53 | node: Y | undefined,
54 | report: (node: Y, error: keyof typeof MessagesRequireUseMemo) => void,
55 | ) {
56 | if (!statusData) {
57 | return;
58 | }
59 | if (statusData.status === MemoStatus.ErrorInvalidContext) {
60 | report((statusData.node ?? node) as Y, MemoStatus.ErrorInvalidContext);
61 | }
62 | const errorName = data?.[statusData.status.toString()];
63 | if (errorName) {
64 | const strict = errorName.includes("unknown");
65 | if (!strict || (strict && context.options?.[0]?.strict)) {
66 | report((statusData.node ?? node) as Y, errorName);
67 | }
68 | }
69 | }
70 |
71 | function addReactImports(
72 | context: CompatibleContext,
73 | kind: "useMemo" | "useCallback",
74 | reactImportData: ReactImportInformation,
75 | fixer: Rule.RuleFixer | RuleV9.RuleFixer,
76 | ) {
77 | const importsDisabled = context.options?.[0]?.fix?.addImports === false;
78 | let specifier: TSESTree.ImportClause | undefined = undefined;
79 |
80 | if (importsDisabled) {
81 | return;
82 | }
83 |
84 | if (!reactImportData[`${kind}Imported`]) {
85 | // Create a new ImportSpecifier for useMemo/useCallback hook.
86 | specifier = {
87 | type: "ImportSpecifier",
88 | imported: { type: "Identifier", name: kind },
89 | local: { type: "Identifier", name: kind },
90 | } as TSESTree.ImportSpecifier;
91 |
92 | if (reactImportData.importDeclaration?.specifiers) {
93 | const specifiers = reactImportData.importDeclaration.specifiers;
94 | const hasDefaultExport =
95 | specifiers?.[0]?.type === "ImportDefaultSpecifier";
96 | const isEmpty = !specifiers.length;
97 | // Default export counts as a specifier too
98 | const shouldCreateSpecifiersBracket =
99 | specifiers.length <= 1 && hasDefaultExport;
100 | const hasCurrentSpecifier =
101 | !isEmpty &&
102 | !shouldCreateSpecifiersBracket &&
103 | specifiers.find((x) => x.local.name === kind);
104 |
105 | if (shouldCreateSpecifiersBracket) {
106 | specifiers.push(specifier);
107 | return fixer.insertTextAfter(specifiers[0], `, { ${kind} }`);
108 | }
109 |
110 | if (isEmpty) {
111 | const importDeclaration =
112 | reactImportData.importDeclaration as TSESTree.ImportDeclaration;
113 | const fixRange =
114 | importDeclaration.range[0] + defaultImportRangeStart.length - 1;
115 |
116 | return fixer.insertTextAfterRange([fixRange, fixRange], ` ${kind} `);
117 | }
118 |
119 | if (!hasCurrentSpecifier) {
120 | specifiers.push(specifier);
121 | const insertPosition = specifiers.find(
122 | (specifier) =>
123 | !!specifier.range &&
124 | (!hasDefaultExport || specifier.type !== "ImportDefaultSpecifier"),
125 | );
126 |
127 | if (insertPosition) {
128 | return fixer.insertTextAfter(insertPosition, `, ${kind}`);
129 | }
130 | return;
131 | }
132 | }
133 | }
134 |
135 | // If React is not imported, create a new ImportDeclaration for it.
136 | if (!reactImportData.reactImported && !reactImportData.importDeclaration) {
137 | reactImportData.importDeclaration = {
138 | type: "ImportDeclaration",
139 | specifiers: [
140 | {
141 | ...specifier,
142 | range: [
143 | defaultImportRangeStart.length,
144 | defaultImportRangeStart.length + kind.length,
145 | ],
146 | },
147 | ],
148 | source: { type: "Literal", value: "react" },
149 | } as TSESTree.ImportDeclaration;
150 | reactImportData.reactImported = true;
151 | reactImportData[`${kind}Imported`] = true;
152 |
153 | // Add an extra new line before const component and use indentSpace for proper spacing.
154 | return fixer.insertTextBeforeRange(
155 | [0, 0],
156 | `${defaultImportRangeStart}${kind} } from 'react';\n`,
157 | );
158 | }
159 | return;
160 | }
161 |
162 | export function getIsHook(node: TSESTree.Node | TSESTree.Identifier) {
163 | if (node.type === "Identifier") {
164 | const { name } = node;
165 | return (
166 | name === "use" ||
167 | ((name?.length ?? 0) >= 4 &&
168 | name[0] === "u" &&
169 | name[1] === "s" &&
170 | name[2] === "e" &&
171 | name[3] === name[3]?.toUpperCase?.())
172 | );
173 | }
174 |
175 | if (
176 | node.type === "MemberExpression" &&
177 | !node.computed &&
178 | getIsHook(node.property)
179 | ) {
180 | const { object: obj } = node; // Utilizing Object destructuring
181 | return obj.type === "Identifier" && obj.name === "React";
182 | }
183 |
184 | return false;
185 | }
186 |
187 | // Helper function to find parent of a specified type.
188 | export function findParentType(
189 | node: Rule.Node,
190 | type: string,
191 | ): Rule.Node | undefined {
192 | let parent = node.parent;
193 |
194 | while (parent) {
195 | if (parent.type === type) return parent;
196 |
197 | parent = parent.parent;
198 | }
199 |
200 | return undefined;
201 | }
202 |
203 | function fixFunction(
204 | node:
205 | | TSESTree.FunctionDeclaration
206 | | TSESTree.FunctionExpression
207 | | TSESTree.ArrowFunctionExpression,
208 | context: Rule.RuleContext,
209 | shouldSetName?: boolean,
210 | ) {
211 | const sourceCode = context.getSourceCode();
212 | const { body, params = [] } = node;
213 | const funcBody = sourceCode.getText(body as unknown as ESTree.Node);
214 | const funcParams = (params as Array).map((node) =>
215 | sourceCode.getText(node),
216 | );
217 | let fixedCode = `useCallback(${node.async ? "async " : ""}(${funcParams.join(", ")}) => ${funcBody}, [])${shouldSetName ? ";" : ""}`;
218 | if (shouldSetName && node?.id?.name) {
219 | const name = node?.id?.name;
220 | fixedCode = `const ${name} = ${fixedCode}`;
221 | }
222 | return fixedCode;
223 | }
224 |
225 | function getSafeVariableName(
226 | context: Rule.RuleContext,
227 | node: CompatibleNode | undefined,
228 | name: string,
229 | attempts = 0,
230 | ): string {
231 | const tempVarPlaceholder = "renameMe";
232 |
233 | if (node && !getVariableInScope(context, node, name)) {
234 | return name;
235 | }
236 | if (attempts >= 5) {
237 | const nameExtensionIfExists =
238 | node && getVariableInScope(context, node, tempVarPlaceholder)
239 | ? uuidV5(name, nameGeneratorUUID).split("-")[0]
240 | : "";
241 | return `${tempVarPlaceholder}${nameExtensionIfExists ? `_${nameExtensionIfExists}` : ""}`;
242 | }
243 | return getSafeVariableName(context, node, `_${name}`, ++attempts);
244 | }
245 |
246 | // Eslint Auto-fix logic, functional components/hooks only
247 | export function fixBasedOnMessageId(
248 | node: CompatibleNode,
249 | messageId: keyof typeof MessagesRequireUseMemo,
250 | fixer: Rule.RuleFixer | RuleV9.RuleFixer,
251 | context: CompatibleContext,
252 | reactImportData: ReactImportInformation,
253 | ) {
254 | // Get source code in a way that works with both v8 and v9
255 | const sourceCode =
256 | "getSourceCode" in context ? context.getSourceCode() : (context as RuleV9.RuleContext)?.sourceCode;
257 |
258 | const hook = messageIdToHookDict[messageId] || "useMemo";
259 | const isObjExpression = node.type === "ObjectExpression";
260 | const isJSXElement =
261 | (node as unknown as TSESTree.JSXElement).type === "JSXElement";
262 | const isArrowFunctionExpression = node.type === "ArrowFunctionExpression";
263 | const isFunctionExpression = node.type === "FunctionExpression";
264 | const isCorrectableFunctionExpression =
265 | isFunctionExpression || isArrowFunctionExpression;
266 | const fixes: Array = [];
267 |
268 | // Determine what type of behavior to follow according to the error message
269 | switch (messageId) {
270 | case "function-usecallback-hook":
271 | if (
272 | node.type === "FunctionExpression" ||
273 | node.type === "ArrowFunctionExpression"
274 | ) {
275 | const importStatementFixes = addReactImports(
276 | context,
277 | "useCallback",
278 | reactImportData,
279 | fixer,
280 | );
281 | const fixed = fixFunction(
282 | node as TSESTree.FunctionExpression,
283 | context as any,
284 | );
285 | importStatementFixes && fixes.push(importStatementFixes);
286 | // Use any cast to bypass type checking
287 | fixes.push(fixer.replaceText(node as any, fixed));
288 | return fixes;
289 | }
290 | break;
291 | case "object-usememo-hook": {
292 | const _returnNode = node as TSESTree.ReturnStatement;
293 | // An undefined node.argument means returned value is not an expression, but most probably a variable which should not be handled here, which falls under default, simpler fix logic.
294 | if (_returnNode.argument) {
295 | const importStatementFixes = addReactImports(
296 | context,
297 | "useMemo",
298 | reactImportData,
299 | fixer,
300 | );
301 | const fixed = `useMemo(() => (${sourceCode.getText(_returnNode.argument as any)}), [])`;
302 | importStatementFixes && fixes.push(importStatementFixes);
303 | // Use any cast to bypass type checking
304 | fixes.push(fixer.replaceText(_returnNode.argument as any, fixed));
305 | return fixes;
306 | }
307 | break;
308 | }
309 | case "function-usecallback-props":
310 | case "object-usememo-props":
311 | case "jsx-usememo-props":
312 | case "usememo-const": {
313 | const variableDeclaration =
314 | node.type === "VariableDeclaration"
315 | ? node
316 | : (findParentType(
317 | node as any,
318 | "VariableDeclaration",
319 | ) as TSESTree.VariableDeclaration);
320 |
321 | // Check if it is a hook being stored in let/var, change to const if so
322 | if (variableDeclaration && variableDeclaration.kind !== "const") {
323 | const tokens = sourceCode.getTokens(variableDeclaration as any);
324 | const letKeywordToken = tokens?.[0];
325 | if (letKeywordToken?.value !== "const") {
326 | fixes.push(fixer.replaceTextRange(letKeywordToken.range, "const"));
327 | }
328 | }
329 | // If it's an dynamic object - Add useMemo/Callback
330 | if (isObjExpression || isJSXElement || isCorrectableFunctionExpression) {
331 | const importStatementFixes = addReactImports(
332 | context,
333 | isCorrectableFunctionExpression ? "useCallback" : "useMemo",
334 | reactImportData,
335 | fixer,
336 | );
337 | importStatementFixes && fixes.push(importStatementFixes);
338 | const fixed = isCorrectableFunctionExpression
339 | ? fixFunction(node as any, context as any)
340 | : `useMemo(() => (${sourceCode.getText(node as any)}), [])`;
341 | const parent =
342 | node.parent as unknown as TSESTree.JSXExpressionContainer;
343 | // Means we have a object expression declared directly in jsx
344 | if (parent.type === "JSXExpressionContainer") {
345 | const parentPropName = (
346 | parent?.parent as TSESTree.JSXAttribute
347 | )?.name?.name.toString();
348 | const newVarName = getSafeVariableName(
349 | context as any,
350 | parent.parent,
351 | parentPropName,
352 | );
353 | const returnStatement = findParentType(
354 | node as any,
355 | "ReturnStatement",
356 | ) as TSESTree.ReturnStatement;
357 |
358 | if (returnStatement) {
359 | const indentationLevel =
360 | sourceCode.lines[returnStatement.loc.start.line - 1].search(/\S/);
361 | const indentation = " ".repeat(indentationLevel);
362 | // Creates a declaration for the variable and inserts it before the return statement
363 | fixes.push(
364 | fixer.insertTextBeforeRange(
365 | returnStatement.range,
366 | `const ${newVarName} = ${fixed};\n${indentation}`,
367 | ),
368 | );
369 | // Replaces the old inline object expression with the variable name
370 | fixes.push(fixer.replaceText(node as any, newVarName));
371 | }
372 | } else {
373 | fixes.push(fixer.replaceText(node as any, fixed));
374 | }
375 | }
376 |
377 | return !fixes.length ? null : fixes;
378 | }
379 | // Unknown cases are usually complex issues or false positives, so we ignore them
380 | case "unknown-class-memo-props":
381 | case "unknown-usememo-hook":
382 | case "unknown-usememo-deps":
383 | case "unknown-usememo-props":
384 | case "error-in-invalid-context":
385 | return null;
386 | }
387 |
388 | // Simpler cases bellow, all of them are just adding useMemo/Callback
389 | const functionPrefix = isArrowFunctionExpression ? "" : "() => ";
390 | const expressionPrefix = isObjExpression || isJSXElement ? "(" : "";
391 | const coreExpression = sourceCode.getText(node as any);
392 | const expressionSuffix = isObjExpression ? ")" : "";
393 |
394 | let fixed = `${hook}(${functionPrefix}${expressionPrefix}${coreExpression}${expressionSuffix}, [])`;
395 | const importStatementFixes = addReactImports(
396 | context,
397 | hook,
398 | reactImportData,
399 | fixer,
400 | );
401 | importStatementFixes && fixes.push(importStatementFixes);
402 |
403 | if (node.type === "FunctionDeclaration") {
404 | const _node = node as TSESTree.FunctionDeclaration;
405 | if (_node && _node?.id?.type === "Identifier") {
406 | fixed = fixFunction(_node, context as any, true);
407 | }
408 | }
409 |
410 | if (
411 | "computed" in node &&
412 | (node as any)?.computed?.type === "ArrowFunctionExpression"
413 | ) {
414 | fixes.push(fixer.replaceText((node as any).computed, fixed));
415 | } else {
416 | fixes.push(fixer.replaceText(node as any, fixed));
417 | }
418 | return fixes;
419 | }
420 |
--------------------------------------------------------------------------------
/src/traditional-config.ts:
--------------------------------------------------------------------------------
1 | import requireMemoRule from "./require-memo";
2 | import requireUseMemoRule from "./require-usememo";
3 | import requireUseMemoChildrenRule from "./require-usememo-children";
4 | import type { Rule } from "eslint";
5 | import type { CompatibleRuleModule } from "./utils/compatibility";
6 |
7 | export const rules: Record = {
8 | "require-memo": requireMemoRule,
9 | "require-usememo": requireUseMemoRule,
10 | "require-usememo-children": requireUseMemoChildrenRule,
11 | };
12 |
13 | // Legacy configs export (for ESLint v8 and below)
14 | export const configs = {
15 | recommended: {
16 | plugins: ["@arthurgeron/react-usememo"],
17 | rules: {
18 | "@arthurgeron/react-usememo/require-usememo": "error",
19 | "@arthurgeron/react-usememo/require-memo": "error",
20 | "@arthurgeron/react-usememo/require-usememo-children": "error",
21 | },
22 | },
23 | };
24 |
25 | // Add meta information to each rule
26 | for (const ruleName of Object.keys(rules)) {
27 | const rule = rules[ruleName] as CompatibleRuleModule;
28 | rule.meta = {
29 | ...rule.meta,
30 | docs: {
31 | ...rule.meta.docs,
32 | url: `https://github.com/arthurgeron/eslint-plugin-react-usememo/blob/main/docs/rules/${ruleName}.md`
33 | }
34 | };
35 | }
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import type { Rule } from "eslint";
2 | import type { TSESTree } from "@typescript-eslint/types";
3 |
4 | export type MemoStatusToReport = {
5 | node?: Rule.RuleContext | TSESTree.Node,
6 | status: MemoStatus
7 | } | undefined;
8 |
9 | export enum MemoStatus {
10 | Memoized,
11 | UnmemoizedObject,
12 | UnmemoizedArray,
13 | UnmemoizedNew,
14 | UnmemoizedFunction,
15 | UnmemoizedFunctionCall,
16 | UnmemoizedJSX,
17 | UnmemoizedOther,
18 | UnsafeLet,
19 | ErrorInvalidContext = 'error-in-invalid-context'
20 | }
21 |
22 | export type ESNode = TSESTree.CallExpression & Rule.NodeParentExtension
--------------------------------------------------------------------------------
/src/utils/compatibility.ts:
--------------------------------------------------------------------------------
1 | import type { Rule } from "eslint";
2 | import type { Rule as RuleV9 } from "eslint-v9";
3 | import type { TSESTree } from "@typescript-eslint/types";
4 |
5 | // Type that represents a node from either ESLint v8 or v9
6 | export type CompatibleNode = Rule.Node | RuleV9.Node | TSESTree.Node;
7 |
8 | /**
9 | * Utility to safely get scope in both ESLint v8 and v9
10 | */
11 | export function getCompatibleScope(
12 | context: Rule.RuleContext | RuleV9.RuleContext,
13 | node?: CompatibleNode,
14 | ) {
15 | if (typeof context.getScope === "function") {
16 | // ESLint v8 approach
17 | return context.getScope();
18 | }
19 |
20 | const v9Context = context as unknown as RuleV9.RuleContext;
21 | if (
22 | v9Context.sourceCode &&
23 | typeof v9Context.sourceCode.getScope === "function"
24 | ) {
25 | // ESLint v9 approach
26 |
27 | if (!node) {
28 | throw new Error("Node is required for ESLint v9");
29 | }
30 | return v9Context.sourceCode.getScope(node as RuleV9.Node);
31 | }
32 |
33 | throw new Error("Failed to fetch scope method");
34 | }
35 |
36 | /**
37 | * Safely get the filename in a way that works with both ESLint v8 and v9
38 | */
39 | export function getCompatibleFilename(
40 | context: Rule.RuleContext | RuleV9.RuleContext,
41 | ): string {
42 | if (typeof context.getFilename === "function") {
43 | // ESLint v8 approach
44 | return context.getFilename();
45 | }
46 |
47 | const v9Context = context as RuleV9.RuleContext;
48 | return v9Context.filename || " ";
49 | }
50 |
51 | /**
52 | * Create a type that works for both ESLint v8 and v9 rule modules
53 | * This relaxes the typing to allow both versions to work together
54 | */
55 | export type CompatibleRuleModule = {
56 | meta: {
57 | messages?: Record;
58 | type?: string;
59 | docs?: {
60 | description?: string;
61 | category?: string;
62 | recommended?: boolean;
63 | url?: string;
64 | };
65 | fixable?: "code" | "whitespace";
66 | schema?: unknown[];
67 | };
68 | create: (
69 | context: Rule.RuleContext | RuleV9.RuleContext,
70 | ) => Record unknown>;
71 | };
72 |
--------------------------------------------------------------------------------
/src/utils/getVariableInScope.ts:
--------------------------------------------------------------------------------
1 | import type { Rule } from "eslint-v9";
2 | import type { Rule as RuleV8 } from "eslint";
3 | import { CompatibleNode, getCompatibleScope } from "../utils/compatibility";
4 | import type { CompatibleContext } from "../require-usememo/utils";
5 |
6 | /**
7 | * Get a variable in the current scope by name
8 | * This is a wrapper around context.getScope().variables.find()
9 | * that uses getCompatibleScope to handle both ESLint v8 and v9
10 | */
11 | export default function getVariableInScope(context: CompatibleContext, node: CompatibleNode, name: string) {
12 | try {
13 | // Try to get scope without a node parameter (ESLint v8 style)
14 | return getCompatibleScope(context, node)?.variables.find((variable) => {
15 | return variable.name === name;
16 | });
17 | } catch (error) {
18 | // If that fails, we might not be able to get a global scope directly
19 | return undefined;
20 | }
21 | }
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | import type { Rule, Scope } from "eslint";
2 | import type { Rule as RuleV9 } from "eslint-v9";
3 | import type { TSESTree } from "@typescript-eslint/types";
4 | import type * as ESTree from "estree";
5 | import { type ESNode, MemoStatus, type MemoStatusToReport } from "src/types";
6 | import { getIsHook, isImpossibleToFix } from "src/require-usememo/utils";
7 | import getVariableInScope from "src/utils/getVariableInScope";
8 | import { Minimatch } from "minimatch";
9 | import type { CompatibleContext } from "src/require-usememo/utils";
10 |
11 | export function isComplexComponent(
12 | node: TSESTree.JSXOpeningElement | TSESTree.JSXIdentifier,
13 | ) {
14 | if (node?.type !== "JSXOpeningElement") return false;
15 | if (node?.name?.type !== "JSXIdentifier") return false;
16 | const firstCharacterLowerCase = node?.name?.name?.[0]?.toLowerCase();
17 | return (
18 | !!firstCharacterLowerCase &&
19 | firstCharacterLowerCase !== node?.name?.name?.[0]
20 | );
21 | }
22 |
23 | export function isComponentName(name: string | undefined) {
24 | return (
25 | typeof name === "string" && !!name && name?.[0] === name?.[0]?.toUpperCase()
26 | );
27 | }
28 |
29 | function isCallExpression(
30 | node: TSESTree.CallExpression,
31 | name: "useMemo" | "useCallback",
32 | ) {
33 | if (node?.callee?.type === "MemberExpression") {
34 | const {
35 | callee: { object, property },
36 | } = node;
37 | if (
38 | object.type === "Identifier" &&
39 | property.type === "Identifier" &&
40 | object.name === "React" &&
41 | property.name === name
42 | ) {
43 | return true;
44 | }
45 | } else if (node?.callee?.type === "Identifier" && node.callee.name === name) {
46 | return true;
47 | }
48 |
49 | return false;
50 | }
51 |
52 | function getIdentifierMemoStatus(
53 | context: CompatibleContext,
54 | identifierNode: TSESTree.Identifier,
55 | ): MemoStatusToReport {
56 | const { name } = identifierNode;
57 | const variableInScope = getVariableInScope(context, identifierNode, name);
58 | if (variableInScope === undefined) return { status: MemoStatus.Memoized };
59 | const [{ node }] = variableInScope.defs;
60 | const isProps =
61 | node?.id?.type === "Identifier" &&
62 | (isComponentName(node.id.name) || getIsHook(node.id));
63 | if (isProps) {
64 | return;
65 | }
66 |
67 | const isFunctionParameter = node?.id?.name !== name;
68 | if (node.type === "FunctionDeclaration")
69 | return {
70 | node: node,
71 | status: isFunctionParameter
72 | ? MemoStatus.Memoized
73 | : MemoStatus.UnmemoizedFunction,
74 | };
75 | if (node.type !== "VariableDeclarator")
76 | return { node: node, status: MemoStatus.Memoized };
77 | if (
78 | node?.parent?.kind === "let" &&
79 | node?.init?.type === "CallExpression" &&
80 | getIsHook(node?.init?.callee)
81 | ) {
82 | return { node: node.parent, status: MemoStatus.UnsafeLet };
83 | }
84 | return getExpressionMemoStatus(context, node.init);
85 | }
86 |
87 | function getInvalidContextReport(
88 | context: CompatibleContext,
89 | expression: TSESTree.Expression,
90 | ) {
91 | const impossibleFix = isImpossibleToFix(
92 | expression as Rule.NodeParentExtension,
93 | context,
94 | );
95 | if (impossibleFix?.result) {
96 | return { node: impossibleFix.node, status: MemoStatus.ErrorInvalidContext };
97 | }
98 | return false;
99 | }
100 |
101 | export function getExpressionMemoStatus(
102 | context: CompatibleContext,
103 | expression: TSESTree.Expression,
104 | checkContext = false,
105 | ): MemoStatusToReport {
106 | switch (expression?.type) {
107 | case undefined:
108 | case "ObjectExpression":
109 | return (
110 | (checkContext && getInvalidContextReport(context, expression)) || {
111 | node: expression,
112 | status: MemoStatus.UnmemoizedObject,
113 | }
114 | );
115 | case "ArrayExpression":
116 | return (
117 | (checkContext && getInvalidContextReport(context, expression)) || {
118 | node: expression,
119 | status: MemoStatus.UnmemoizedArray,
120 | }
121 | );
122 | case "NewExpression":
123 | return (
124 | (checkContext && getInvalidContextReport(context, expression)) || {
125 | node: expression,
126 | status: MemoStatus.UnmemoizedNew,
127 | }
128 | );
129 | case "FunctionExpression":
130 | case "ArrowFunctionExpression": {
131 | return (
132 | (checkContext && getInvalidContextReport(context, expression)) || {
133 | node: expression,
134 | status: MemoStatus.UnmemoizedFunction,
135 | }
136 | );
137 | }
138 | case "JSXElement":
139 | return (
140 | (checkContext && getInvalidContextReport(context, expression)) || {
141 | node: expression,
142 | status: MemoStatus.UnmemoizedJSX,
143 | }
144 | );
145 | case "CallExpression": {
146 | const validCallExpression =
147 | isCallExpression(expression, "useMemo") ||
148 | isCallExpression(expression, "useCallback");
149 |
150 | return (
151 | (validCallExpression &&
152 | checkContext &&
153 | getInvalidContextReport(context, expression)) || {
154 | node: expression,
155 | status: validCallExpression
156 | ? MemoStatus.Memoized
157 | : MemoStatus.UnmemoizedFunctionCall,
158 | }
159 | );
160 | }
161 | case "Identifier":
162 | return getIdentifierMemoStatus(context, expression);
163 | case "BinaryExpression":
164 | return { node: expression, status: MemoStatus.Memoized };
165 | default:
166 | return { node: expression, status: MemoStatus.UnmemoizedOther };
167 | }
168 | }
169 |
170 | export function findVariable(
171 | scope: Scope.Scope,
172 | node: ESTree.Identifier,
173 | ): Scope.Variable | undefined {
174 | if (scope.variables.some((variable) => variable.name === node.name)) {
175 | return scope.variables.find((variable) => variable.name === node.name);
176 | }
177 |
178 | if (scope.upper) {
179 | return findVariable(scope.upper, node);
180 | }
181 |
182 | return undefined;
183 | }
184 |
185 | export function shouldIgnoreNode(
186 | node: ESNode,
187 | ignoredNames: Record,
188 | ) {
189 | const nodeName = (node as TSESTree.Node as TSESTree.Identifier)?.name;
190 | const nodeCalleeName = (node?.callee as TSESTree.Identifier)?.name;
191 | const nodeCalleePropertyName = (
192 | (node?.callee as TSESTree.MemberExpression)?.property as TSESTree.Identifier
193 | )?.name;
194 | const nameToCheck = nodeName || nodeCalleeName || nodeCalleePropertyName;
195 |
196 | const matchedValue = ignoredNames[nameToCheck];
197 |
198 | if (matchedValue !== undefined) {
199 | return matchedValue;
200 | }
201 |
202 | if (nameToCheck === "use") {
203 | return true;
204 | }
205 |
206 | let shouldIgnore: boolean | undefined;
207 |
208 | Object.keys(ignoredNames).find((key) => {
209 | const value = ignoredNames[key];
210 | const miniMatch = new Minimatch(key);
211 | if (miniMatch.hasMagic()) {
212 | const isMatch = nameToCheck && miniMatch.match(nameToCheck);
213 | if (isMatch) {
214 | shouldIgnore = !!value;
215 | return true;
216 | }
217 | }
218 | return false;
219 | });
220 |
221 | return !!shouldIgnore;
222 | }
223 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "outDir": "dist",
5 | "moduleResolution": "node",
6 | "esModuleInterop": true,
7 | "baseUrl": "./",
8 | "noEmitOnError": false
9 | },
10 | }
11 |
--------------------------------------------------------------------------------