ui icon indicating copy to clipboard operation
ui copied to clipboard

[bug]: `react-hooks/purity` reports `Cannot call impure function during render` with `Math.random()` from Sidebar

Open karlhorky opened this issue 2 months ago • 3 comments

Describe the bug

The react-hooks/purity ESLint rule based on the React Compiler reports Cannot call impure function during render with the Math.random() call in the SidebarMenuSkeleton component:

function SidebarMenuSkeleton({
  className,
  showIcon = false,
  ...props
}: React.ComponentProps<"div"> & {
  showIcon?: boolean
}) {
  // Random width between 50 to 90%.
  const width = React.useMemo(() => {
    return `${Math.floor(Math.random() * 40) + 50}%` // 💥 react-hooks/purity error, see details below
  }, [])

Affected component/components

Sidebar

How to reproduce

  1. Create a Next.js app
    $ mkdir repro-shadcnui-react-hooks-purity-error
    $ cd repro-shadcnui-react-hooks-purity-error
    $ pnpm create [email protected] . --app --no-turbopack --no-src-dir --no-eslint --import-alias @/\* --tailwind --typescript 
    Creating a new Next.js app in /Users/k/p/repro-shadcnui-react-hooks-purity-error.
    
    ...
    
    dependencies:
    + next 15.5.5
    + react 19.1.0
    + react-dom 19.1.0
    
    devDependencies:
    + @tailwindcss/postcss 4.1.14
    + @types/node 20.19.21
    + @types/react 19.2.2
    + @types/react-dom 19.2.2
    + tailwindcss 4.1.14
    + typescript 5.9.3
    
  2. Use shadcn/ui to add a sidebar component
    $ pnpm dlx shadcn@latest add sidebar
    ...
    ✔ Created 8 files:
      ...
      - components/ui/sidebar.tsx
    
  3. Install ESLint dependencies
    $ pnpm add --save-dev eslint @eslint/js typescript typescript-eslint eslint-config-flat-gitignore eslint-plugin-react-hooks
    Packages: +157 -2
    ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
    Progress: resolved 295, reused 246, downloaded 2, added 37, done
    
    devDependencies:
    + @eslint/js 9.37.0 (9.38.0 is available)
    + eslint 9.37.0 (9.38.0 is available)
    + eslint-config-flat-gitignore 2.1.0
    + eslint-plugin-react-hooks 7.0.0
    + typescript 5.9.3
    + typescript-eslint 8.46.1 (8.46.2 is available)
    
    Done in 3s using pnpm v10.19.0
    
  4. Configure the react-hooks/purity rule in eslint.config.mjs, based partly on the typescript-eslint Getting Started docs
    import eslint from '@eslint/js';
    import gitignore from 'eslint-config-flat-gitignore';
    import reactHooks from 'eslint-plugin-react-hooks';
    import tseslint from 'typescript-eslint';
    
    export default tseslint.config(
      gitignore(),
      eslint.configs.recommended,
      tseslint.configs.recommended,
      {
        ignores: ['.next/**'],
        plugins: {
          'react-hooks': reactHooks,
        },
        rules: {
          'react-hooks/purity': 'error',
        },
      },
    );
    
  5. Lint the Sidebar file and receive an error 💥
    $ pnpm eslint components/ui/sidebar.tsx --max-warnings 0
    
    /Users/k/p/repro-shadcnui-react-hooks-purity-error/components/ui/sidebar.tsx
      611:26  error  Error: Cannot call impure function during render
    
    `Math.random` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
    
    /Users/k/p/repro-shadcnui-react-hooks-purity-error/components/ui/sidebar.tsx:611:26
      609 |   // Random width between 50 to 90%.
      610 |   const width = React.useMemo(() => {
    > 611 |     return `${Math.floor(Math.random() * 40) + 50}%`
          |                          ^^^^^^^^^^^^^ Cannot call impure function
      612 |   }, [])
      613 |
      614 |   return (  react-hooks/purity
    
    ✖ 1 problem (1 error, 0 warnings)
    

Codesandbox/StackBlitz link

  • https://codesandbox.io/p/devbox/sweet-wildflower-mtmqdw?file=%2Fcomponents%2Fui%2Fsidebar.tsx%3A611%2C53&workspaceId=ws_GfAuHrswXyA1DoeSwsjjjz
  • GitHub reproduction: https://github.com/karlhorky/repro-shadcnui-react-hooks-purity-error

Suggested solution

Switch useMemo to useState as per the example in the react-hooks/purity docs, disabling the react-naming-convention/use-state rule if necessary:

-  const width = React.useMemo(() => {
+  // eslint-disable-next-line react-naming-convention/use-state -- Allow value-only state destructuring for resolving react-hooks/purity problem https://github.com/shadcn-ui/ui/issues/8540
+  const [width] = React.useState(() => {
     return `${Math.floor(Math.random() * 40) + 50}%`
-  }, [])
+  })

Logs


System Info

eslint-plugin-react-hooks 7.0.0

Before submitting

  • [x] I've made research efforts and searched the documentation
  • [x] I've searched for existing issues

karlhorky avatar Oct 22 '25 13:10 karlhorky

Hello! @karlhorky fixed it, let me know!

naaa760 avatar Oct 23 '25 05:10 naaa760

I also encountered this problem

xu756 avatar Oct 31 '25 05:10 xu756

Same problem here.

Forcing update of shadcdn sidebar component

% npx shadcn@latest add sidebar --overwrite

✔ Checking registry.
✔ Updating CSS variables in src/app/globals.css
✔ Installing dependencies.
ℹ Updated 3 files:
  - src/components/ui/tooltip.tsx
  - src/components/ui/input.tsx
  - src/components/ui/sidebar.tsx
ℹ Skipped 5 files: (files might be identical, use --overwrite to overwrite)
  - src/components/ui/button.tsx
  - src/components/ui/separator.tsx
  - src/components/ui/sheet.tsx
  - src/hooks/use-mobile.ts
  - src/components/ui/skeleton.tsx
  - 

But

/ ... /src/components/ui/sidebar.tsx
  611:26  error  Error: Cannot call impure function during render

`Math.random` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).

/ ... /src/components/ui/sidebar.tsx:611:26
  609 |   // Random width between 50 to 90%.
  610 |   const width = React.useMemo(() => {
> 611 |     return `${Math.floor(Math.random() * 40) + 50}%`
      |                          ^^^^^^^^^^^^^ Cannot call impure function
  612 |   }, [])
  613 |
  614 |   return (  react-hooks/purity

We cannot upgrade to next 16 and to latest eslint recommended configuration due to this error (which of course happens in other components, I suppose)

Actual eslint.config.mjs

import nextCoreWebVitals from "eslint-config-next/core-web-vitals";

export default [
  {
    ignores: [
      "node_modules/**",
      ".next/**",
      "out/**",
      "build/**",
      "next-env.d.ts",
      "lambda/dist",
    ],
  },
  ...nextCoreWebVitals,
];

Is there a workaround? adn/or is there a planned fix?

realtebo avatar Nov 11 '25 08:11 realtebo

This solution from [#8546](https://github.com/shadcn-ui/ui/pull/8546/commits/c26d8ab2f289034f44334737615b4640193f4789) resolved the react-hooks/purity linting error in my project:

  // Random width between 50 to 90%.
const [width] = React.useState(() => `${Math.floor(Math.random() * 40) + 50}%`)

sidebar.tsx line 610

NicholasSutin avatar Nov 28 '25 01:11 NicholasSutin