Skip to content

[Bug]: react-hooks/set-state-in-effect ESLint error when trying to avoid hydration mismatch #374

@spacecat

Description

@spacecat

What happened?

The following code:

https://github.com/pacocoursey/next-themes?tab=readme-ov-file#avoid-hydration-mismatch

import { useState, useEffect } from 'react'
import { useTheme } from 'next-themes'

const ThemeSwitch = () => {
  const [mounted, setMounted] = useState(false)
  const { theme, setTheme } = useTheme()

  // useEffect only runs on the client, so now we can safely show the UI
  useEffect(() => {
    setMounted(true)
  }, [])

  if (!mounted) {
    return null
  }

  return (
    <select value={theme} onChange={e => setTheme(e.target.value)}>
      <option value="system">System</option>
      <option value="dark">Dark</option>
      <option value="light">Light</option>
    </select>
  )
}

export default ThemeSwitch

is causing the following ESLint error:

/some/path/my-component.tsx
  15:5  error  Error: Calling setState synchronously within an effect can trigger cascading renders

Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:
* Update external systems with the latest state from React.
* Subscribe for updates from some external system, calling setState in a callback function when external state changes.

Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect).

/some/path/my-component.tsx:15:5
  13 |
  14 |   useEffect(() => {
> 15 |     setMounted(true);
     |     ^^^^^^^^^^ Avoid calling setState() directly within an effect
  16 |   }, []);
  17 |
  18 |   if (!mounted) {  react-hooks/set-state-in-effect

Is there a way to fix these errors?

Workaround - add "react-hooks/set-state-in-effect": "off", to rules in your eslint.config.mjs or
/* eslint-disable react-hooks/set-state-in-effect */ to your component. Or you can inline it also if you like.

eslint.confg.mjs:

import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
import prettier from "eslint-config-prettier/flat";
import { defineConfig, globalIgnores } from "eslint/config";

const eslintConfig = defineConfig([
  ...nextVitals,
  ...nextTs,
  {
    rules: {
      "@typescript-eslint/no-explicit-any": "off",
      "@typescript-eslint/no-unused-vars": "off",
      "@typescript-eslint/no-unsafe-function-type": "off",
      "@typescript-eslint/ban-ts-comment": "off",
      "react-hooks/set-state-in-effect": "off",
    },
  },
  prettier,
  // Override default ignores of eslint-config-next.
  globalIgnores([
    // Default ignores of eslint-config-next:
    ".next/**",
    "out/**",
    "build/**",
    "next-env.d.ts",
  ]),
]);

export default eslintConfig;

Version

"next-themes": "1.0.0-beta.0",

What browsers are you seeing the problem on?

No response

package.json:

{
  "name": "My App",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "dev-https": "next dev --experimental-https",
    "build": "next build",
    "start": "next start",
    "lint": "eslint"
  },
  "dependencies": {
...
    "next": "16.0.1",
    "next-themes": "1.0.0-beta.0",
    "react": "19.2.0",
    "react-dom": "19.2.0",
...
  },
  "devDependencies": {
...
    "eslint": "9.39.1",
    "eslint-config-next": "16.0.1",
    "eslint-config-prettier": "10.1.8",
    "prettier": "3.6.2",
    "typescript": "5.9.3"
...
  }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingtriage

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions