blog.itcode.devblog.itcode.dev

[recoil] Resolving the Duplicate atom key Error

React and state management are inseparable. There have been many attempts to make state management more efficient and smarter, and recoil is one of them. Unlike its predecessors Redux and mobx, recoil is loved by many for being simple yet powerful. I'm also a fan of it when developing with React.

[recoil] Resolving the Duplicate atom key Error

React and state management are inseparable. There have been many attempts to make state management more efficient and smarter, and recoil is one of them. Unlike its predecessors Redux and mobx, recoil is loved by many for being simple yet powerful. I'm also a fan of it when developing with React.
RWB0104
@RWBwritten at 2023-02-03 16:56:31

React and state management are inseparable. There have been many attempts to make state management more efficient and smarter, and recoil is one of them.

Unlike its predecessors Redux and mobx, recoil is loved by many for being simple yet powerful. I'm also a fan of it when developing with React.


If you use recoil with an SSR framework like NextJS, you'll often see this error in the console.

TXT

Expectation Violation: Duplicate atom key [KEY_NAME].

This is a FATAL ERROR in production.
But it is safe to ignore this warning if it occurred because of hot module replacement.

Literally, this means an atom's key is duplicated, but this error occurs even when there's clearly no atom with a duplicate key.

Fortunately, there's no functional issue, and the error disappears after building. Still, even though it's just a nominal error, it's quite annoying to leave it as is.

This post covers why this phenomenon happens and how to resolve it.

This phenomenon usually only occurs in the development environment, and is mostly not caught in the production environment after building and deploying. It would have been quite troublesome if it were the other way around.

Why does this not-quite-an-error occur only in the development environment, without causing any actual functional issues?


TSX

const stringAtom = atom<string | undefined>({
  key: 'stringAtom',
  default: undefined
});

The code above shows the basic way to declare an atom. The key is a unique value used to internally distinguish the atom. Even though stringAtom clearly isn't used by any other atom, the error will still occur.

The reason is closely related to a characteristic unique to the React development environment. When developing React, hot loading is used for convenience. When code changes, the dev server detects it and swaps in the changes in real time. During this process, some code gets re-rendered, causing recoil's atom to be reassigned as well.

So from recoil's perspective, an already-assigned stringAtom is being reassigned, and to prevent corruption of the atom's unique key, it raises an error to notify the developer. But from the developer's perspective, this happens with no intent on their part at all, so there's a gap in perspective between the two.

Of course, a normal service doesn't run this way. As mentioned above, this only happens in the development environment due to hot loading. In the production environment, since it obviously runs only with the built output, this never happens.

Fixing this error isn't that hard. Some resources suggest finding the recoil library file in node_modules and commenting out the console.error() that triggers it. But as most people know, arbitrarily modifying a library like this isn't a great approach. Of course, if there's really no other way, that's fine — but in that case, you can't really blame the library developers if something goes wrong.

So let's pick one of the two methods below, and get both a clean fix and keep our respect for the library developers intact.

Since this quirky bug bothered basically every recoil user, discussions about it never stopped, both in the library's repository and in various communities.

Fortunately, starting from version 0.7.6, the library added a related option, making it much easier to control the error.

There's one method using an environment variable, and another using code.

PROPERTIES

 # .env
RECOIL_DUPLICATE_ATOM_KEY_CHECKING_ENABLED=false

Just turn off the RECOIL_DUPLICATE_ATOM_KEY_CHECKING_ENABLED option in the environment variables.

That said, since this logic protects the uniqueness of atom keys, and in production it doesn't fire indiscriminately like in development unless there's an actual duplicate, blanket disabling it doesn't seem like the best choice.

In that case, you can configure it more smartly like below.


PROPERTIES

 # .env.development
 # development environment variable
RECOIL_DUPLICATE_ATOM_KEY_CHECKING_ENABLED=false

 # .env.production
 # production environment variable
RECOIL_DUPLICATE_ATOM_KEY_CHECKING_ENABLED=true

This lets you set different values depending on the environment.

Occasionally, certain frameworks require a specific prefix for environment variables, which makes it inconvenient to use them this way. There may also be other reasons you can't use environment variables.

Don't worry — you can achieve the same behavior in code.

TSX

import { RecoilEnv, RecoilRoot } from 'recoil';

// Disable duplicate checking
RecoilEnv.RECOIL_DUPLICATE_ATOM_KEY_CHECKING_ENABLED = process.env.NODE_ENV === 'production';

export default function App(): JSX.Element
{
  return (
    <RecoilRoot>
      <Component />
    </RecoilRoot>
  );
}

You can control the option in code via RecoilEnv. Since process.env.NODE_ENV holds the environment value, you can use it to disable the option only in the development environment, as shown above.

Versions below 0.7.6 don't provide RecoilEnv. So you need to find another way in code.

Since this is a general-purpose approach solved entirely in code, it can also be applied even if your recoil version is 0.7.6 or above.


The principle is simple. This error occurs because an atom with the same key gets assigned during the reassignment caused by hot loading.

So all you need to do is prevent key duplication only in the development environment. Since the atom gets replaced anyway upon reassignment, there's no functional difference even if the key changes.


TSX

const stringAtom = atom<string | undefined>({
  key: process.env.NODE_ENV === 'development' ? `stringAtom-${Date.now()}` : 'stringAtom' ,
  default: undefined
});

Using process.env.NODE_ENV, a timestamp is appended to the key only in the development environment. It doesn't have to be a timestamp — you can use anything with a randomness characteristic, like Math.random(), if you prefer.

For a smarter setup, you can extract the key generation logic into a method, as shown below.

TSX

function getAtomKey(key: string)
{
  return process.env.NODE_ENV === 'development' ? `${key}-${Date.now()}` : key;
}

const stringAtom = atom<string | undefined>({
  key: getAtomKey('stringAtom'),
  default: undefined
});

Doing it this way removes code duplication.

You can pick whichever method above you like.

There's no difference in outcome, but I personally prefer the solution that works for all recoil versions.

That's because I think it's better not to trigger the error in the first place, rather than deliberately turning off validation logic to suppress an error that has already occurred. Of course, I haven't looked under the hood, so I don't know exactly how it behaves internally.


Now let's get rid of that annoying error.

# React# Web# recoil
ship
blog.itcode.dev

Notes from the π-th Alpaca

7.0.1
Developed by RWB since 2021.057th upgraded at 2026.08