Concept
Version-currency callout, read this first: confirmed via direct npm registry lookup, Recoil's most recent published version (0.7.7) went out in February 2024, under the facebookexperimental GitHub/npm org, the org name itself signals its original status. This isn't a formal npm deprecation notice, but over two years without a release is a genuine, honest signal: Recoil is not actively evolving the way Jotai (its conceptual successor, in practice) currently is. This topic teaches Recoil accurately and completely, because it's still curriculum-relevant and still asked about in some longer-established codebases, but without pretending it's an equally live choice for a brand-new project today.
The core model: atoms and selectors, requiring RecoilRoot
import { RecoilRoot, atom, useRecoilState } from "recoil";
const countAtom = atom({
key: "count", // Recoil atoms need a unique STRING key, unlike Jotai's atoms
default: 0,
});
function Counter() {
const [count, setCount] = useRecoilState(countAtom);
return <button onClick={() => setCount((c) => c + 1)}>{count}</button>;
}
function App() {
return (
<RecoilRoot> {/* REQUIRED, confirmed as a real export, unlike Jotai's optional Provider */}
<Counter />
</RecoilRoot>
);
}Confirmed by inspecting the installed package: RecoilRoot, atom, selector, and the useRecoilState/useRecoilValue/useSetRecoilState hook family all exist and match their long-standing documented API. The one immediately visible structural difference from Jotai: Recoil requires wrapping the app in <RecoilRoot>, there's no equivalent to Jotai's "just call the hook, no provider needed" default.
selector: Recoil's derived state, and the direct ancestor of Jotai's atom(read, write)
const celsiusAtom = atom({ key: "celsius", default: 0 });
const fahrenheitSelector = selector({
key: "fahrenheit",
get: ({ get }) => get(celsiusAtom) * 9 / 5 + 32,
set: ({ set }, newValue) => set(celsiusAtom, (newValue - 32) * 5 / 9),
});If this looks almost identical in shape to Jotai's writable derived atom from Jotai, that's not a coincidence, Recoil's selector with paired get/set functions is the earlier design Jotai's later, more minimal atom(read, write) API drew from. Learning one genuinely transfers to the other; the underlying dependency-graph mental model (a selector automatically depends on whatever atoms its get function reads) is the same idea covered in depth in Jotai's topic, reference that topic's jotai-atom-granularity visualization rather than re-deriving the same lesson here, since the mechanics are conceptually the same.
The required key: Recoil's one real, distinguishing quirk
const countAtom = atom({ key: "count", default: 0 }); // "count" must be GLOBALLY uniqueEvery Recoil atom and selector needs an explicit, app-wide-unique string key, used internally for serialization and debugging. This has no equivalent in Jotai (atom identity is just the object reference, no string needed) and is worth knowing specifically because it's the detail most likely to trip someone up moving between the two: forgetting a key, or accidentally duplicating one across two atoms, produces real, sometimes confusing errors.
Try It
Predict the outcome before checking the solution.
const aAtom = atom({ key: "a", default: 1 });
const bAtomBad = atom({ key: "a", default: 2 }); // ❌ same key as aAtomWhat happens when this code runs?
Solution
Recoil throws a runtime error about a duplicate atom key. Since the key is how Recoil internally identifies and tracks each atom (for its dependency graph, serialization, and debugging tools), two atoms sharing the same key is treated as a genuine conflict, not a harmless coincidence, this is exactly the kind of Recoil-specific mistake that doesn't have a direct equivalent in Jotai, where atom identity is just the object reference and no explicit key is ever required.
Implement It Yourself
Since Recoil's atom/selector model is conceptually the same dependency-graph idea as Jotai's, revisit Jotai's "Implement It Yourself" mini atom-graph implementation, the same exercise applies here nearly unchanged, just imagining each atom carrying an explicit string key alongside its value. Rebuilding it a second time wouldn't teach anything genuinely new; the value in this topic is recognizing the shared lineage, not re-deriving the mechanism from scratch.
Under the Hood
Recoil's selector and Jotai's atom(read, write) are different names for essentially the same dependency-tracking mechanism covered in depth in Jotai, this topic deliberately doesn't re-embed that lesson's visualizer, since the underlying re-render behavior (an update propagating through whatever reads a changed atom, directly or via a derived selector) is identical. MobX, covered next, takes a genuinely different approach, automatic Proxy-based tracking instead of an explicit atom/selector graph, worth contrasting once you've seen both.
Common Mistakes
1. Forgetting RecoilRoot
function App() {
return <Counter />; // ❌ missing RecoilRoot, useRecoilState will throw
}Confirmed: unlike Jotai's optional-by-default provider, Recoil's hooks require an ancestor RecoilRoot to function at all, omitting it produces an immediate runtime error, not a silent fallback.
2. Duplicating a key across atoms/selectors
const a = atom({ key: "shared", default: 1 });
const b = selector({ key: "shared", get: () => 2 }); // ❌ key collision, even across atom/selectorConfirmed to throw, keys must be unique across the entire app, not just within atoms or within selectors separately.
3. Choosing Recoil for a brand-new project without weighing its maintenance status
// "let's use Recoil, it's from Meta", worth checking WHEN it was last actually updatedConfirmed via direct registry lookup: over two years without a release as of this writing. For a new project with no existing Recoil investment, this is a genuine, concrete factor worth weighing against Jotai's active development, not a reason to avoid Recoil entirely if a team already has deep investment in it, but a real consideration for new adoption.
Best Practices
- Always wrap the app in
RecoilRootbefore using any Recoil hook, this is a hard requirement, not a convenience. - Namespace your
keystrings deliberately (e.g., prefixing by feature area) to avoid accidental collisions in a larger app, since Recoil's key uniqueness is enforced globally. - For a brand-new project with no existing Recoil code, evaluate Jotai first, the atomic model transfers directly, and it's the actively-maintained option; reach for Recoil specifically when working within an existing Recoil-based codebase, not as a first choice for new work.
Performance Tips
- Recoil's dependency-graph-based update propagation has the same performance characteristics as Jotai's, an update only notifies genuine dependents, not the whole app, the two libraries don't meaningfully differ here despite the maintenance-status gap.
- The required
keystrings have no runtime performance cost; they exist purely for identity/debugging/serialization, not as part of the hot update path.
