# no-global-render-counter

> A module-level counter (SSR id, item number, render count) changed during render never matches the value in the browser. This rule reports the write.

Source: https://hydration.jscrate.dev/docs/rules/no-global-render-counter
Last updated: 2026-09-18

Changing a module-level variable during render makes the two renders
disagree: the server keeps the value across every request, while each browser
tab starts from the initial value. Anything built on a module-level counter
(SSR id values, item numbers, render counts) drifts.
`no-global-render-counter` reports writes to module-level `let` and `var`
variables in render code.

| | |
| --- | --- |
| Rule | `hydration-proof/no-global-render-counter` |
| What it reports | Disallow changing module-level variables while a component renders |
| recommended / next | Error |
| strict | Error |
| Server Components | Skipped with the next preset |
| Suggestions | No |
| Options | allow |

## What it reports

Writes (`++`, `--`, `=`, `+=`, `??=`, destructuring assignments) to a
module-level `let` or `var` from
[render code](https://hydration.jscrate.dev/docs/eslint#what-counts-as-render). Writes at module level, in
effects and in event handlers are fine, and so is mutating an object
(`cache.set(...)`): only reassigning the variable is reported.

A counter that ends up in an id (`` `field-${nextId++}` ``) is reported by
[`no-unstable-id`](https://hydration.jscrate.dev/docs/rules/no-unstable-id) instead, with a message that
points to `useId()`.

## Why a module-level counter, SSR id or render count drifts

A module is loaded once per server process and once per browser tab. On the
server the variable keeps its value across every request and every user; in
the browser it starts from the initial value:

```text
server HTML (request 1,000): <p>Item #3001</p>
client render:               <p>Item #1</p>
```

React Strict Mode and concurrent rendering also render components more than
once, so the value drifts even in the browser.

## Incorrect

```jsx
let renderCount = 0;

function Item({ name }) {
  renderCount += 1;
  return (
    <p>
      Item #{renderCount}: {name}
    </p>
  );
}

let lastUser;

function useUser(user) {
  lastUser = user;
  return lastUser;
}
```

## Correct

```jsx
function List({ items }) {
  return items.map((item, index) => (
    <p key={item.id}>
      Item #{index + 1}: {item.name}
    </p>
  ));
}

function useUser(user) {
  const lastUser = useRef(user);
  useEffect(() => {
    lastUser.current = user;
  }, [user]);
  return user;
}
```

Use `useId()` for ids and `useRef` for values that should survive re-renders
of one component.

## Options

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `allow` | `string[]` | — | Module-level variable names that may be written during render (for example a deliberate cache). |

```js title="eslint.config.mjs"
{
  rules: {
    'hydration-proof/no-global-render-counter': ['error', { allow: ['cache'] }],
  },
}
```

- `allow` (string array, default `[]`): module-level variables that may be
  written during render, for example a deliberate memoization cache whose
  value is the same on both sides.

## Messages

What ESLint prints for this rule, word for word:

- Module-level `<name>` is changed during render. The server keeps its value across every request while the browser starts from the initial value, so anything derived from it does not match during hydration (and React may render twice in development). Keep the value in state or a ref, or use useId() for ids.

## When not to use it

When the module is only ever evaluated in the browser.

## Related

- [`no-unstable-id`](https://hydration.jscrate.dev/docs/rules/no-unstable-id) reports counters that are used
  for ids. Those writes are not reported by this rule.
- [Generated ids that differ](https://hydration.jscrate.dev/docs/causes/unstable-id): why counters break
  ids, and the fix
- [Text content does not match server-rendered HTML](https://hydration.jscrate.dev/docs/errors/text-content-does-not-match-server-rendered-html),
  the error a drifting count produces
