ReactではESLintのno-restricted-globalsでcloseを必ず設定するべき

closeはウィンドウを閉じるメソッド

先日、「【JavaScript】関数名をclose()にするのは気をつけろ」という記事が話題になっていました。

closeはウィンドウ(タブ)を閉じるメソッドなので、closeを上書きせずに onClick={close} を書くとボタンを押したときにウィンドウを閉じてしまいます。

closeをモーダルを閉じる関数として作成した場合、その関数を削除したままにしておくと、closeをonClickなどで実行した際にウィンドウが閉じてしまうという重大なバグが発生します。

closeは元々あるメソッドなので、const closeの部分を削除しても警告は表示されません。

試しに以下のコマンドでReact環境を作成して、App.tsxのコードを onClick={close} に変更してみてください。ボタンを押すとウィンドウ(タブ)を閉じます。

npm create vite@latest my-react-close -- --template react-swc-ts
App.tsx
function App() {
  // const close = () => {
  // モーダルを閉じる処理
  // }

  return <button onClick={close}>モーダルを閉じる</button>
}

export default App

no-restricted-globalsでcloseを設定する

closeを設定してもエラーにならない問題はESLintでno-restricted-globalsに ['error', 'close'] を設定することで解決できます。

eslint.config.js
export default tseslint.config(
  { ignores: ['dist'] },
  {
    extends: [js.configs.recommended, ...tseslint.configs.recommended],
    // 中略
    rules: {
      ...reactHooks.configs.recommended.rules,
      'react-refresh/only-export-components': [
        'warn',
        { allowConstantExport: true },
      ],
      'no-restricted-globals': ['error', 'close'],
    },
  },
)

実際はclose以外にもopenやstopなどもあるので、'no-restricted-globals' を設定する際は以下のように設定することをオススメします。

nameとmessageを付けると使ってはいけない理由もESLintの警告時に表示できます。

eslint.config.js
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'

const noRestrictedDefaultMessage = 'JavaScriptの組込み関数です。'

export default tseslint.config(
  { ignores: ['dist'] },
  {
    extends: [js.configs.recommended, ...tseslint.configs.recommended],
    // 中略
    rules: {
      ...reactHooks.configs.recommended.rules,
      'react-refresh/only-export-components': [
        'warn',
        { allowConstantExport: true },
      ],
      'no-restricted-globals': [
        'error',
        {
          'name': 'alert',
          'message': noRestrictedDefaultMessage + 'アラートを開いてしまいます。',
        },
        {
          'name': 'blur',
          'message': noRestrictedDefaultMessage,
        },
        {
          'name': 'close',
          'message': noRestrictedDefaultMessage + 'ウィンドウ(タブ)を閉じてしまいます。',
        },
        {
          'name': 'confirm',
          'message': noRestrictedDefaultMessage + 'メッセージのないダイアログを表示してしまいます。',
        },
        {
          'name': 'focus',
          'message': noRestrictedDefaultMessage,
        },
        {
          'name': 'open',
          'message': noRestrictedDefaultMessage + 'タブを開いてしまいます。',
        },
        {
          'name': 'print',
          'message': noRestrictedDefaultMessage + '印刷画面を開いてしまいます。',
        },
        {
          'name': 'prompt',
          'message': noRestrictedDefaultMessage + 'メッセージのないダイアログを表示してしまいます。',
        },
        {
          'name': 'stop',
          'message': noRestrictedDefaultMessage + 'ページのリソースの読み込みを停止してしまいます。',
        },
      ],
    },
  },
)

なぜこれらがダメなのかについて、close以外も解説します。

open

closeとは逆にタブを開いてしまいます。

TypeScriptを使用している場合は「Type '(url?: string | URL | undefined,〜」の警告が表示されるので、closeよりは間違いにくいです。

App.tsx
function App() {
  // 別タブを開いてしまう
  return <button onClick={open}>button</button>
}

export default App

stop

ページのリソースの読み込みを停止してしまいます。

画像やAPIのデータなどの読込中にstopが実行されると、それらが停止します。

App.tsx
function App() {
  // ページのリソースの読み込みを停止してしまう
  return <button onClick={stop}>button</button>
}

export default App

print

印刷画面を開いてしまいます。

最近は使用されるケースが激減しているので、若いフロントエンドエンジニアだと知らずに使ってしまう可能性が高くなります。

App.tsx
function App() {
  // 印刷画面を開いてしまう
  return <button onClick={print}>button</button>
}

export default App

alert

アラートを開いてしまいます。

App.tsx
function App() {
  // アラートを開いてしまう
  return <button onClick={alert}>button</button>
}

export default App

confirm

メッセージのないダイアログを表示してしまいます。

TypeScriptを使用している場合は引数なしだと警告が出るので気づきやすいです。

App.tsx
function App() {
  // ダイアログを表示してしまう
  return <button onClick={confirm}>button</button>
}

export default App

prompt

confirmと同様にメッセージのないダイアログを表示してしまいます。

こちらもTypeScriptを使用している場合は引数なしだと警告が出るので気づきやすい。

App.tsx
function App() {
  // ダイアログを表示してしまう
  return <button onClick={prompt}>button</button>
}

export default App

blur

onClickイベントで使用した場合は特に何も起きないことが多い。

何も起きず、TypeScriptの警告も表示されないのでコードで使用されても気づきにくい。

App.tsx
function App() {
  // クリックしても特に何も起きない
  return <button onClick={blur}>button</button>
}

export default App

focus

onClickイベントで使用した場合は特に何も起きないことが多い。

何も起きず、TypeScriptの警告も表示されないのでコードで使用されても気づきにくい。

App.tsx
function App() {
  // クリックしても特に何も起きない
  return <button onClick={focus}>button</button>
}

export default App

まとめ

JavaScriptには組み込み関数があり、同じ名前の関数を間違えて使用してしまうと気づかずにバグを組み込んでしまう可能性があるのでESLintでno-restricted-globalsを9つ追加しておくと安全です。

['alert', 'blur', 'close', 'confirm', 'focus', 'open', 'print', 'prompt', 'stop']

JavaScriptの組み込み関数はこれら以外にもたくさんあるのですが、const close = () => で定義したものを消したあとにonClickイベントなどに入れたままにしてしまう可能性が高いものは、私が調べた範囲ではこの9つだけです。

これらを人力で視認してコード内に使用されている箇所がないか確認するのは厳しいので、ESLintを設定して自動的に検出できるようにすると良いでしょう。