eslint-plugin-smarthr 6.19.0 → 6.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,27 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ## [6.21.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v6.20.0...eslint-plugin-smarthr-v6.21.0) (2026-06-22)
6
+
7
+
8
+ ### Features
9
+
10
+ * require-i18n-translation-sync ESLintルールを追加 ([#1406](https://github.com/kufu/tamatebako/issues/1406)) ([5286bbb](https://github.com/kufu/tamatebako/commit/5286bbb94c6b774f9132f6d40ef36c216cff0399))
11
+
12
+ ## [6.20.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v6.19.0...eslint-plugin-smarthr-v6.20.0) (2026-06-16)
13
+
14
+
15
+ ### Features
16
+
17
+ * **best-practice-for-no-unnecessary-variable:** export { xxx as yyy } パターンで適切なメッセージを表示 ([#1375](https://github.com/kufu/tamatebako/issues/1375)) ([52d90fa](https://github.com/kufu/tamatebako/commit/52d90fa6eb682c956d71a12269d2cc2c4173d3dd))
18
+ * 依存配列に不安定な参照を含めることを禁止するルールを追加 ([#1376](https://github.com/kufu/tamatebako/issues/1376)) ([af7c18e](https://github.com/kufu/tamatebako/commit/af7c18e74ce38391078a0bd14bf815529e852f8a))
19
+
20
+
21
+ ### Bug Fixes
22
+
23
+ * best-practice-for-rest-parametersでin演算子も内部属性アクセスとして検知 ([#1372](https://github.com/kufu/tamatebako/issues/1372)) ([086b414](https://github.com/kufu/tamatebako/commit/086b414ef731ebd27aee935f2d31aa5d303dd9fd))
24
+ * lazy-variable/no-unnecessary-variableでjsxタグ名を正しく扱う ([#1382](https://github.com/kufu/tamatebako/issues/1382)) ([391fe7e](https://github.com/kufu/tamatebako/commit/391fe7ea655e030ded3d2a3d45ae511c2da75dd1))
25
+
5
26
  ## [6.19.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v6.18.0...eslint-plugin-smarthr-v6.19.0) (2026-06-10)
6
27
 
7
28
 
package/README.md CHANGED
@@ -40,6 +40,7 @@
40
40
  - [best-practice-for-tailwind-variants](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-tailwind-variants)
41
41
  - [best-practice-for-text-component](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-text-component)
42
42
  - [best-practice-for-unnesessary-early-return](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-unnesessary-early-return)
43
+ - [best-practice-for-unstable-dependencies](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-unstable-dependencies)
43
44
  - [component-name](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/component-name)
44
45
  - [design-system-guideline-bulk-action-row-button](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/design-system-guideline-bulk-action-row-button)
45
46
  - [design-system-guideline-prohibit-dialog-button-icon](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/design-system-guideline-prohibit-dialog-button-icon)
@@ -56,5 +57,6 @@
56
57
  - [require-declaration](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/require-declaration)
57
58
  - [require-export](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/require-export)
58
59
  - [require-i18n-text](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/require-i18n-text)
60
+ - [require-i18n-translation-sync](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/require-i18n-translation-sync)
59
61
  - [require-import](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/require-import)
60
62
  - [trim-props](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/trim-props)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-smarthr",
3
- "version": "6.19.0",
3
+ "version": "6.21.0",
4
4
  "author": "SmartHR",
5
5
  "license": "MIT",
6
6
  "description": "A sharable ESLint plugin for SmartHR",
@@ -84,7 +84,8 @@ function getVariableUsages(sourceCode, varName, declarationNode) {
84
84
  if (!node || typeof node !== 'object') return
85
85
 
86
86
  switch (node.type) {
87
- case 'Identifier': {
87
+ case 'Identifier':
88
+ case 'JSXIdentifier': {
88
89
  // 変数名が一致し、変数参照である場合のみ収集(宣言自体は除外)
89
90
  if (node.name === varName && node !== declarationNode.id && isVariableReference(node)) {
90
91
  usages.push(node)
@@ -50,9 +50,7 @@ function shouldSkipVariableExceptComplexity(node) {
50
50
  // UPPER_SNAKE_CASE形式の定数は除外(慣習的な定数命名)
51
51
  UPPER_SNAKE_CASE_PATTERN.test(node.id.name) ||
52
52
  // 関数式、TaggedTemplateExpressionは除外
53
- EXCLUDED_INIT_TYPES.includes(node.init.type) ||
54
- // React Hooks(useXxxで始まる関数)で初期化される変数は対象外
55
- (node.init.type === 'CallExpression' && node.init.callee.type === 'Identifier' && node.init.callee.name.startsWith('use'))
53
+ EXCLUDED_INIT_TYPES.includes(node.init.type)
56
54
  ) {
57
55
  return true
58
56
  }
@@ -69,6 +67,16 @@ function shouldSkipVariableExceptComplexity(node) {
69
67
  return false
70
68
  }
71
69
 
70
+ /**
71
+ * React Hooksで初期化されているかを判定
72
+ */
73
+ function isReactHookCall(node) {
74
+ return node.init &&
75
+ node.init.type === 'CallExpression' &&
76
+ node.init.callee.type === 'Identifier' &&
77
+ node.init.callee.name.startsWith('use')
78
+ }
79
+
72
80
  /**
73
81
  * 型注釈のテキストを取得
74
82
  * @param {object} sourceCode - SourceCode
@@ -165,7 +173,7 @@ function getVariableUsagesInScope(sourceCode, varName, declarationNode) {
165
173
  * 変数の使用箇所として収集すべきIdentifierかチェック
166
174
  */
167
175
  function isTargetIdentifier(node) {
168
- return node.type === 'Identifier' &&
176
+ return (node.type === 'Identifier' || node.type === 'JSXIdentifier') &&
169
177
  node.name === varName &&
170
178
  node !== declarationNode.id &&
171
179
  !isInsideInit(node)
@@ -195,7 +203,11 @@ function getVariableUsagesInScope(sourceCode, varName, declarationNode) {
195
203
 
196
204
  // 変数名が一致するIdentifierを収集(宣言自体と初期化式内は除外)
197
205
  if (isTargetIdentifier(node)) {
198
- usages.push(node)
206
+ // 同じノードオブジェクトを重複してカウントしないようにする
207
+ // (export { foo } の場合、localとexportedが同じノードを指すため)
208
+ if (!usages.includes(node)) {
209
+ usages.push(node)
210
+ }
199
211
  return
200
212
  }
201
213
 
@@ -371,6 +383,24 @@ function isAtStartOfExpressionStatement(usageNode) {
371
383
  return false
372
384
  }
373
385
 
386
+ /**
387
+ * 使用箇所がexport { xxx as yyy }のExportSpecifierの中にあるかチェック
388
+ */
389
+ function isInExportSpecifier(usageNode) {
390
+ let current = usageNode.parent
391
+ while (current) {
392
+ if (current.type === 'ExportSpecifier') {
393
+ return true
394
+ }
395
+ // ExportNamedDeclarationまで到達したら終了
396
+ if (current.type === 'ExportNamedDeclaration') {
397
+ return false
398
+ }
399
+ current = current.parent
400
+ }
401
+ return false
402
+ }
403
+
374
404
  /**
375
405
  * 複雑さをチェックしてインライン化可能かを判定
376
406
  */
@@ -413,6 +443,19 @@ function analyzeVariable(sourceCode, node, options = {}) {
413
443
  }
414
444
 
415
445
  const usage = usages[0]
446
+
447
+ // JSXIdentifierとして使用されている場合は対象外(JSXタグ名はインライン化できない)
448
+ if (usage.type === 'JSXIdentifier') {
449
+ return null
450
+ }
451
+
452
+ const isExportSpec = isInExportSpecifier(usage)
453
+
454
+ // React Hooks呼び出しは対象外(ただしexport { xxx as yyy }パターンは対象)
455
+ if (!isExportSpec && isReactHookCall(node)) {
456
+ return null
457
+ }
458
+
416
459
  const typeAnnotation = getTypeAnnotationText(sourceCode, node)
417
460
 
418
461
  // 複雑さチェック
@@ -425,6 +468,7 @@ function analyzeVariable(sourceCode, node, options = {}) {
425
468
  varName,
426
469
  usage,
427
470
  typeAnnotation,
471
+ isExportSpecifier: isExportSpec,
428
472
  }
429
473
  }
430
474
 
@@ -438,6 +482,7 @@ module.exports = {
438
482
  schema: SCHEMA,
439
483
  messages: {
440
484
  inlineVariable: '変数"{{name}}"は一度しか使用されていません。直接使用してください。',
485
+ exportDirectly: '変数"{{name}}"は一度しか使用されていません。export const {{exportedName}} = ... の形式で直接エクスポートしてください。',
441
486
  },
442
487
  },
443
488
  create(context) {
@@ -450,12 +495,35 @@ module.exports = {
450
495
  const analysis = analyzeVariable(sourceCode, node, options)
451
496
 
452
497
  if (analysis) {
453
- context.report({
454
- node: analysis.node,
455
- messageId: 'inlineVariable',
456
- data: { name: analysis.varName },
457
- fix: fix ? createInlineFixer(sourceCode, analysis.node, analysis.usage, analysis.typeAnnotation) : null,
458
- })
498
+ // export { xxx as yyy } パターンの場合は異なるメッセージを使用
499
+ if (analysis.isExportSpecifier) {
500
+ // ExportSpecifierからエクスポート名を取得
501
+ let exportedName = analysis.varName
502
+ let current = analysis.usage.parent
503
+ while (current && current.type !== 'ExportSpecifier') {
504
+ current = current.parent
505
+ }
506
+ if (current && current.type === 'ExportSpecifier' && current.exported) {
507
+ exportedName = current.exported.name
508
+ }
509
+
510
+ context.report({
511
+ node: analysis.node,
512
+ messageId: 'exportDirectly',
513
+ data: {
514
+ name: analysis.varName,
515
+ exportedName,
516
+ },
517
+ fix: fix ? createInlineFixer(sourceCode, analysis.node, analysis.usage, analysis.typeAnnotation) : null,
518
+ })
519
+ } else {
520
+ context.report({
521
+ node: analysis.node,
522
+ messageId: 'inlineVariable',
523
+ data: { name: analysis.varName },
524
+ fix: fix ? createInlineFixer(sourceCode, analysis.node, analysis.usage, analysis.typeAnnotation) : null,
525
+ })
526
+ }
459
527
  }
460
528
  },
461
529
  }
@@ -2,6 +2,15 @@ const SCHEMA = []
2
2
 
3
3
  const REST_REGEX = /(^r|R)est$/
4
4
  const MEMBER_EXPRESSION_REST_REGEX = /^(r|[a-zA-Z0-9_]+R)est\./
5
+
6
+ const UNFORMAT_NAME__REST_ELEMENT_SELECTOR = `RestElement:not([argument.name=${REST_REGEX}])`
7
+ const CONFUSING_NAME_VARIABLE_SELECTOR_1 = `:not(:matches(RestElement,JSXSpreadAttribute,JSXSpreadAttribute > TSAsExpression,SpreadElement,SpreadElement > TSAsExpression,MemberExpression,BinaryExpression,VariableDeclarator,ArrayExpression,CallExpression,ObjectPattern > Property,ObjectExpression > Property,ReturnStatement,ArrowFunctionExpression)) > Identifier[name=${REST_REGEX}]`
8
+ const CONFUSING_NAME_VARIABLE_SELECTOR_2 = `:matches(VariableDeclarator[id.name=${REST_REGEX}],ObjectPattern > Property[value.name=${REST_REGEX}],ObjectExpression > Property[key.name=${REST_REGEX}])`
9
+ const ARROW_FUNCTION_PARAMS_SELECTOR = `ArrowFunctionExpression > Identifier[name=${REST_REGEX}]`
10
+ const USE_REST_VIORATION_SELECTOR_MEMBER_EXPRESSION = `MemberExpression[object.name=${REST_REGEX}]`
11
+ const USE_REST_VIORATION_SELECTOR_IN_OPERATOR = `BinaryExpression[operator="in"][right.name=${REST_REGEX}]`
12
+ const USE_REST_VIORATION_OBJECT_PATTERN = `VariableDeclarator[id.type='ObjectPattern'][init.name=${REST_REGEX}]`
13
+
5
14
  const DETAIL_LINK = `
6
15
  - 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-rest-parameters`
7
16
 
@@ -21,46 +30,47 @@ module.exports = {
21
30
  - 残余引数(rest parameters)と混同する可能性があるため別の名称に修正してください`,
22
31
  })
23
32
  }
33
+ const actionUseRestVioration = (node) => {
34
+ context.report({
35
+ node,
36
+ message: `残余引数内の属性を参照しないでください${DETAIL_LINK}`,
37
+ })
38
+ }
24
39
  const actionMemberExpressionName = (node) => {
25
40
  if (node.parent.type === 'MemberExpression') {
26
41
  return actionMemberExpressionName(node.parent)
27
42
  }
28
43
 
29
44
  if (MEMBER_EXPRESSION_REST_REGEX.test(context.sourceCode.getText(node))){
30
- context.report({
31
- node,
32
- message: `残余引数内の属性を参照しないでください${DETAIL_LINK}`,
33
- })
45
+ actionUseRestVioration(node)
34
46
  }
35
47
  }
36
48
 
37
49
  return {
38
- [`ObjectPattern[properties.length=1]>RestElement`]: (node) => {
50
+ 'ObjectPattern[properties.length=1]>RestElement': (node) => {
39
51
  context.report({
40
52
  node,
41
53
  message: `意味のない残余引数のため、単一の引数に変更してください${DETAIL_LINK}`,
42
54
  })
43
55
  },
44
- [`RestElement:not([argument.name=${REST_REGEX}])`]: (node) => {
56
+ [UNFORMAT_NAME__REST_ELEMENT_SELECTOR]: (node) => {
45
57
  context.report({
46
58
  node,
47
59
  message: `残余引数には ${REST_REGEX} とマッチする名称を指定してください${DETAIL_LINK}`,
48
60
  })
49
61
  },
50
- [`:not(:matches(RestElement,JSXSpreadAttribute,JSXSpreadAttribute > TSAsExpression,SpreadElement,SpreadElement > TSAsExpression,MemberExpression,VariableDeclarator,ArrayExpression,CallExpression,ObjectPattern > Property,ObjectExpression > Property,ReturnStatement,ArrowFunctionExpression)) > Identifier[name=${REST_REGEX}]`]: actionNotRest,
51
- [`:matches(VariableDeclarator[id.name=${REST_REGEX}],ObjectPattern > Property[value.name=${REST_REGEX}],ObjectExpression > Property[key.name=${REST_REGEX}])`]: actionNotRest,
52
- [`MemberExpression[object.name=${REST_REGEX}]`]: actionMemberExpressionName,
53
- [`ArrowFunctionExpression > Identifier[name=${REST_REGEX}]`]: (node) => {
62
+ [CONFUSING_NAME_VARIABLE_SELECTOR_1]: actionNotRest,
63
+ [CONFUSING_NAME_VARIABLE_SELECTOR_2]: actionNotRest,
64
+ [ARROW_FUNCTION_PARAMS_SELECTOR]: (node) => {
54
65
  if (node !== node.parent.body) {
55
66
  actionNotRest(node)
56
67
  }
57
68
  },
58
- [`VariableDeclarator[id.type='ObjectPattern'][init.name=${REST_REGEX}]`]: (node) => {
59
- context.report({
60
- node,
61
- message: `残余引数内の属性を参照しないでください${DETAIL_LINK}`,
62
- })
69
+ [USE_REST_VIORATION_SELECTOR_MEMBER_EXPRESSION]: actionMemberExpressionName,
70
+ [USE_REST_VIORATION_SELECTOR_IN_OPERATOR]: (node) => {
71
+ actionUseRestVioration(node.right)
63
72
  },
73
+ [USE_REST_VIORATION_OBJECT_PATTERN]: actionUseRestVioration,
64
74
  }
65
75
  },
66
76
  }
@@ -0,0 +1,339 @@
1
+ # best-practice-for-unstable-dependencies
2
+
3
+ React Hooksの依存配列に不安定な参照(オブジェクト、配列、関数、ReactNodeなど)を含めることを禁止します。
4
+
5
+ これらの値は参照が頻繁に変わるため、依存配列に含めると不要な再実行や無限ループの原因となります。
6
+
7
+ **デフォルトでは `children` のみを検出します。**他の変数名を検出したい場合は、オプションで追加してください。
8
+
9
+ ## 不安定な参照の種類(オプションで追加可能)
10
+
11
+ - **ReactNode**: `children`(デフォルト)、`icon`、`prefix`、`suffix`など
12
+ - **オブジェクト**: `object`、`config`、`options`、`settings`など
13
+ - **配列**: `items`、`list`、`data`、`records`など
14
+ - **関数**: `callback`、`handler`、`onClick`、`onChange`、`onSubmit`など
15
+
16
+ ## ❌ NG
17
+
18
+ ### children(デフォルトで検出)
19
+
20
+ ```javascript
21
+ useEffect(() => {
22
+ console.log(children)
23
+ }, [children])
24
+ ```
25
+
26
+ ```javascript
27
+ const memoized = useMemo(() => {
28
+ return children.length
29
+ }, [children])
30
+ ```
31
+
32
+ ### オプション追加時の例
33
+
34
+ 以下は `additionalUnstableNames` オプションで変数名を追加した場合の検出例です。
35
+
36
+ **オブジェクト:**
37
+
38
+ ```javascript
39
+ // additionalUnstableNames: ["object"] を設定した場合
40
+ useEffect(() => {
41
+ console.log(object.key)
42
+ }, [object])
43
+ ```
44
+
45
+ **配列:**
46
+
47
+ ```javascript
48
+ // additionalUnstableNames: ["items"] を設定した場合
49
+ const memoized = useMemo(() => {
50
+ return items.map(i => i.value)
51
+ }, [items])
52
+ ```
53
+
54
+ **関数:**
55
+
56
+ ```javascript
57
+ // additionalUnstableNames: ["callback"] を設定した場合
58
+ useEffect(() => {
59
+ callback()
60
+ }, [callback])
61
+ ```
62
+
63
+ ## ✅ OK
64
+
65
+ ### 方法1: refを使用する(最も推奨)
66
+
67
+ 依存配列で再実行する必要がない変数は、refに保存して依存配列から除外します。
68
+
69
+ **children(DOM要素のrefを使う場合):**
70
+
71
+ ```javascript
72
+ const childrenRef = useRef()
73
+
74
+ useEffect(() => {
75
+ console.log(childrenRef.current)
76
+ }, [/* childrenを含めない */])
77
+
78
+ ...
79
+
80
+ return (
81
+ <Any ref={childrenRef}>{children}</Any>
82
+ )
83
+ ```
84
+
85
+ refは任意の値を保持でき、更新してもコンポーネントの再レンダリングをトリガーしません。
86
+
87
+ ```javascript
88
+ const valueRef = useRef()
89
+ valueRef.current = value // レンダリングごとに最新の値に更新
90
+
91
+ useEffect(() => {
92
+ // valueRef.currentを使用
93
+ console.log(valueRef.current)
94
+ }, [/* valueを含めない */])
95
+ ```
96
+
97
+ ### 方法2: MutationObserverを使用する
98
+
99
+ `children` の変更を検知する必要がある場合は、DOM要素にrefを設定してMutationObserverで監視します。
100
+
101
+ ```javascript
102
+ const containerRef = useRef()
103
+
104
+ useEffect(() => {
105
+ const observer = new MutationObserver(() => {
106
+ // children内のDOM要素が追加/削除されたときの処理
107
+ console.log('子要素が変更されました')
108
+ })
109
+
110
+ if (containerRef.current) {
111
+ // containerRef内の子要素の変更を監視
112
+ observer.observe(containerRef.current, {
113
+ childList: true, // 直接の子要素の追加/削除を監視
114
+ subtree: true, // 子孫要素の変更も監視(必要に応じて)
115
+ })
116
+ }
117
+
118
+ return () => observer.disconnect()
119
+ }, []) // childrenは依存配列に含めない
120
+
121
+ ...
122
+
123
+ return (
124
+ <div ref={containerRef}>
125
+ {children}
126
+ </div>
127
+ )
128
+ ```
129
+
130
+ ### 方法3: プリミティブ値のみを依存配列に含める
131
+
132
+ オブジェクトや配列の場合は、必要なプリミティブ値のみを依存配列に含めます。
133
+
134
+ ```javascript
135
+ // ❌ 配列全体を依存配列に含める
136
+ useEffect(() => {
137
+ console.log(items.length)
138
+ }, [items])
139
+
140
+ // ✅ 必要な値のみを依存配列に含める
141
+ useEffect(() => {
142
+ console.log(items.length)
143
+ }, [items.length])
144
+ ```
145
+
146
+ **スプレッド構文を使っている場合:**
147
+
148
+ スプレッド構文(`...config`など)で展開している箇所は、実際に必要な値のみをベタ書きできないか検証してください。
149
+
150
+ ```javascript
151
+ // ❌ オブジェクト全体を展開
152
+ useEffect(() => {
153
+ const newConfig = { ...config, newProp: value }
154
+ doSomething(newConfig)
155
+ }, [config, value])
156
+
157
+ // ✅ 必要な値のみをベタ書き
158
+ useEffect(() => {
159
+ const newConfig = {
160
+ apiUrl: config.apiUrl,
161
+ timeout: config.timeout,
162
+ newProp: value
163
+ }
164
+ doSomething(newConfig)
165
+ }, [config.apiUrl, config.timeout, value])
166
+ ```
167
+
168
+ **ヒント:** 多くの場合、スプレッド構文で展開しているオブジェクトは、実際には一部のプロパティしか使っていないことがあります。使用箇所を確認して、必要な値のみを明示的に指定することで、依存配列を最小限に抑えられます。配列の場合も、実際に必要なのは`length`や特定のインデックスの値だけかもしれません。
169
+
170
+ ### 方法4: useCallbackやuseMemoでメモ化する
171
+
172
+ 不安定な参照を安定化させるために、useCallbackやuseMemoでラップします。
173
+
174
+ **関数の場合(useCallback):**
175
+
176
+ ```javascript
177
+ // ❌ 関数を直接依存配列に含める
178
+ useEffect(() => {
179
+ callback()
180
+ }, [callback])
181
+
182
+ // ✅ useCallbackでメモ化
183
+ const memoizedCallback = useCallback(callback, [/* callbackの依存関係 */])
184
+
185
+ useEffect(() => {
186
+ memoizedCallback()
187
+ }, [memoizedCallback])
188
+ ```
189
+
190
+ **オブジェクトの場合(useMemo):**
191
+
192
+ ```javascript
193
+ // ❌ オブジェクトを直接依存配列に含める
194
+ useEffect(() => {
195
+ console.log(config.apiUrl)
196
+ }, [config])
197
+
198
+ // ✅ useMemoでメモ化
199
+ const memoizedConfig = useMemo(() => config, [/* configの依存関係 */])
200
+
201
+ useEffect(() => {
202
+ console.log(memoizedConfig.apiUrl)
203
+ }, [memoizedConfig])
204
+ ```
205
+
206
+ **注意:** メモ化は依存関係が明確な場合に有効です。依存関係が不明確な場合は、方法1のrefを使用することを推奨します。
207
+
208
+ ### 方法5: イベントオブジェクトから値を取得する(useCallbackの場合)
209
+
210
+ useCallbackでイベントハンドラーを定義する場合、値を依存配列に含めずに、イベントオブジェクトから取得できます。
211
+
212
+ **button要素の`value`属性を使う:**
213
+
214
+ ```javascript
215
+ // ❌ 値を依存配列に含める
216
+ const handleClick = useCallback(() => {
217
+ doSomething(itemId)
218
+ }, [itemId]) // itemIdが変わるたびにコールバックが再生成される
219
+
220
+ <button onClick={handleClick}>削除</button>
221
+ ```
222
+
223
+ ```javascript
224
+ // ✅ イベントから値を取得
225
+ const handleClick = useCallback((e) => {
226
+ const itemId = e.currentTarget.value
227
+ doSomething(itemId)
228
+ }, []) // 依存配列が空になり、コールバックが安定する
229
+
230
+ <button value={itemId} onClick={handleClick}>削除</button>
231
+ ```
232
+
233
+ **data-*属性も同様に使える:**
234
+
235
+ ```javascript
236
+ const handleClick = useCallback((e) => {
237
+ const itemId = e.currentTarget.getAttribute('data-id')
238
+ const itemType = e.currentTarget.getAttribute('data-type')
239
+ doSomething(itemId, itemType)
240
+ }, [])
241
+
242
+ <button
243
+ data-id={itemId}
244
+ data-type={itemType}
245
+ onClick={handleClick}
246
+ >
247
+ 削除
248
+ </button>
249
+ ```
250
+
251
+ **メリット:**
252
+ - 依存配列が空になり、コールバックが安定する
253
+ - コンポーネントの再レンダリングが減る
254
+ - リスト内の各アイテムで異なるコールバック関数を生成する必要がない
255
+
256
+ **注意:**
257
+ - この方法はイベントハンドラー(onClick、onChange等)でのみ使用できます。useEffect内など、イベントオブジェクトがない場合は他の方法を使用してください。
258
+ - `value`属性や`getAttribute()`で取得した値は**文字列**になります。数値や真偽値が必要な場合は適切に変換してください(例: `Number(e.currentTarget.value)`、`e.currentTarget.value === 'true'`)。
259
+
260
+ ## オプション
261
+
262
+ ### additionalUnstableNames
263
+
264
+ デフォルトでは `children` のみをチェックしますが、他の変数名を追加できます。
265
+
266
+ ```javascript
267
+ {
268
+ "smarthr/best-practice-for-unstable-dependencies": ["error", {
269
+ "additionalUnstableNames": ["icon", "prefix", "object", "items", "callback"]
270
+ }]
271
+ }
272
+ ```
273
+
274
+ ### additionalTargetHooks
275
+
276
+ デフォルトでは `useEffect`, `useLayoutEffect`, `useCallback`, `useMemo` をチェックしますが、カスタムフックを追加できます。
277
+
278
+ ```javascript
279
+ {
280
+ "smarthr/best-practice-for-unstable-dependencies": ["error", {
281
+ "additionalTargetHooks": ["useCustomHook", "useMyEffect"]
282
+ }]
283
+ }
284
+ ```
285
+
286
+ ## 検出対象のHooks(デフォルト)
287
+
288
+ - `useEffect`
289
+ - `useCallback`
290
+ - `useMemo`
291
+ - `useLayoutEffect`
292
+
293
+ ## 使用例
294
+
295
+ ### 不安定な参照の追加
296
+
297
+ プロジェクトでよく使われる不安定な参照を追加することで、チーム全体で一貫したコードを書くことができます。
298
+
299
+ ```javascript
300
+ {
301
+ "smarthr/best-practice-for-unstable-dependencies": ["error", {
302
+ "additionalUnstableNames": [
303
+ // ReactNode
304
+ "icon",
305
+ "prefix",
306
+ "suffix",
307
+ // オブジェクト
308
+ "object",
309
+ "config",
310
+ "options",
311
+ "settings",
312
+ // 配列
313
+ "items",
314
+ "list",
315
+ "data",
316
+ "records",
317
+ // 関数
318
+ "callback",
319
+ "handler",
320
+ "onClick",
321
+ "onChange",
322
+ "onSubmit"
323
+ ]
324
+ }]
325
+ }
326
+ ```
327
+
328
+ ### カスタムフックの追加
329
+
330
+ プロジェクト固有のカスタムフックも検出対象に追加できます。
331
+
332
+ ```javascript
333
+ {
334
+ "smarthr/best-practice-for-unstable-dependencies": ["error", {
335
+ "additionalUnstableNames": ["icon", "items"],
336
+ "additionalTargetHooks": ["useCustomEffect", "useMyMemo"]
337
+ }]
338
+ }
339
+ ```