DEV Community

Cover image for 5 Things AI Can't Do, Even in • Recoil
DevUnionX
DevUnionX

Posted on

5 Things AI Can't Do, Even in • Recoil

5 Things AI Still Can't Do Even With Recoil

I wrote this report based on Recoil's (for React) atom to selector based data flow graph approach. The goal was showing concrete limits through Recoil-specific technical nuances rather than staying at slogans like "AI writes code but can't design". Recoil official documentation speaks of a data graph flowing from atoms to selectors and selectors being able to do sync/async transformations. citeturn8view1 The five places where AI struggles in this technical framework all come down to the same root in my view: lack of intent and context.

As of March 20, 2026, there's another critical background affecting the table: Recoil's most current release note is 0.7.7 from March 1, 2023, containing various SSR/Suspense issues and possible "unhandled promise rejection" fix for useRecoilCallback. citeturn10view2 Additionally Recoil GitHub repository was archived (read-only) on January 1, 2025. citeturn10view0 This further weakens the assumption that "AI already knows everything" because AI assistants mostly don't account for current maintenance status and ecosystem risk when generating code.

The report's five main findings can be summarized like this. When component to state composition isn't properly structured, Recoil atom keys, boundaries (RecoilRoot), shared state versus local state distinction quickly gets out of control. citeturn8view1turn10view0 In atom/selector design, especially with atomFamily/selectorFamily normalization, small key or cache choice can return as "slowdown" or "memory leak" in real projects. citeturn7view0turn7view1turn8view2 Async selectors and atom effects, especially async calling of setSelf, can lead to race conditions and difficult bugs like "later value overwrote user change". citeturn4view1turn8view0 On debugging/snapshot/time-travel side, snapshot lifetime management with retain/release and "API under development" warnings are details AI frequently misses causing problems directly in production. citeturn6view1turn6view2turn6view0 In performance and large-scale compatibility, reading large collections through families in loops can lock CPU, plus memory management of some patterns is questionable. AI can produce these as "works" and leave it. citeturn7view0turn7view2turn7view3

For preparing this report, I first examined Recoil's official documentation including Core Concepts, atom/selector APIs, dev tools and snapshot pages, Recoil blog release notes, and issues in Recoil GitHub repository. citeturn8view1turn8view3turn6view2turn10view2turn10view0 To understand Recoil's async selector patterns, I additionally utilized case writings focused on Recoil and community examples. citeturn0search20turn0search18turn0search4 In accessibility section, I didn't treat Recoil as "direct a11y tool". Instead I related how state changes create risk on UI/focus/announcement (live region) side with WCAG and WAI-ARIA guides. citeturn2search3turn3search1turn3search0 Finally to contextualize AI code assistant error modes, I referenced current research on AI agent PR analysis and software supply chain risks like package hallucination and slopsquatting. citeturn11search6turn11search5turn11search0

Connected corporate document source wasn't provided this session, so I proceeded only with publicly available web sources. Therefore I didn't make claims like "says so in internal docs".

Following flow summarizes where I see AI as accelerator versus risk multiplier in Recoil project.

flowchart TD
  A[Product requirement and boundaries: local state or Recoil?] --> B[Draft with AI: atom/selector, atomFamily/selectorFamily suggestions]
  B --> C{Atom/selector key strategy consistent?}
  C -- No --> D[Create key dictionary and naming standard] --> B
  C -- Yes --> E{Should derived state really be selector?}
  E -- No --> F[Separate local state / memo / computation layer] --> B
  E -- Yes --> G{Async flow exists? (Promise selector, atom effects, SSR/Suspense)}
  G -- Yes --> H[Test race/cancel/retain rules, validate SSR target] --> B
  G -- No --> I{Performance and memory review: family+loop, leak risk}
  I -- Problem exists --> J[Change scaling pattern, apply cache policy & splitting] --> B
  I -- Clean --> K{a11y: focus, live region, modal behavior correct?}
  K -- No --> L[WAI-ARIA/WCAG check: focus trap + aria-live + keyboard flow] --> B
  K -- Yes --> M[Code review + test + prod observation]
Enter fullscreen mode Exit fullscreen mode

Technical background: Recoil builds data graph flowing from atoms to selectors. Atoms are subscribable state units and selectors transform this state sync or async. citeturn8view1turn8view0 Official documentation says atoms "can be used instead of React local component state", meaning technically you can atomize everything. citeturn8view1 This point moves design decision from "library" to "architecture". Whether a state will be shared (atom), derived (selector), or component-specific (local state) requires reading product behavior more than generating code.

AI's concrete error modes: AI most frequently shows "let's write atom for everything" reflex in Recoil world. This produces two easy but dangerous results: (1) atom keys become soup, (2) application's "semantic boundaries" disappear (which state is UI state, which is domain state?). Recoil requires atom keys to be globally unique and these keys get used in places like debugging/persistence. Same key in two atoms gets accepted as error. citeturn8view1turn8view3 AI leaning toward Date.now or random IDs saying "let's generate key" undermines key's need to be "stable across executions" especially in persistence/debug scenarios. Recoil documentation emphasizes selector key (and similarly atom key) needs to be unique across entire application and stable for persistence. citeturn8view0turn8view3

Additionally Recoil tries protecting atom/selector values from direct mutation to detect change correctly. If object held in atom or selector gets directly mutated, might not notify subscribers correctly, so freezes value objects in development mode. citeturn8view0turn8view3 AI using doors like dangerouslyAllowMutability as "quick fix" suppresses error short term but produces ugliest failures like "sometimes doesn't render" long term.

Short code examples showing good versus bad. Good example with keys module-level constant and meaningful. Export const cartItemCountAtom equals atom number with key cart/itemCount comment persistent, unique, carries meaning, and default 0. Bad example with different key every run causing debug/persist nightmare. Export const cartItemCountAtom equals atom number with key cart/itemCount plus Date.now comment key not stable, and default 0.

Good example moving expensive state read to action not render. useRecoilCallback provides reading through snapshot without subscribing during render. citeturn9view2 Import useRecoilCallback from recoil. Function CartDebugButton. Const dumpCart equals useRecoilCallback with snapshot parameter async arrow function. Comment reading on click not render time. Const count equals await snapshot.getPromise cartItemCountAtom. Console.log Cart count. Return button onClick dumpCart showing Log Cart.

Mitigation strategies for developer: I treat Recoil key management like "hidden API", extracting a key dictionary, establishing naming standard like domain/entity/field, first place I look in code review when adding new atom is key. To not mutate atom/selector objects, I make immutable update habit into "rule", instead of turning off freeze warnings in development mode, I fix point where warning appears. citeturn8view3turn8view0 If expensive read needed during render, I prefer useRecoilCallback or on-demand snapshot approach because Recoil explicitly warns tools like useRecoilSnapshot can trigger re-render at every state change. citeturn9view0turn9view2

Technical background: Atoms are "source of truth", selectors are "derived state". Selectors should be thought of as pure/side-effect-free function. citeturn8view1turn8view0 Recoil officially provides pattern with selectorFamily that takes parameter and returns same memoized selector instance for same parameter, very powerful for normalization and "ID-based access". citeturn8view2 Despite this, normalization itself, which atomFamily, which selectorFamily, which key schema, is modeling decision not technical. When done wrong, returns not just "wrong value" but "slowness" and "memory bloat".

AI's concrete error modes: AI has two typical extremes here. (1) making everything atomFamily using like "as if DB", (2) conversely keeping everything in single atom trying to fragment with selectors. When large collections have atomFamily/selectorFamily designed wrong, there are early-period issues saying resources on Recoil side aren't released. For instance one issue reports "resources created with atomFamily/selectorFamily aren't properly freed after unmount". citeturn7view2 Parallel to this, "memory management/garbage collection" topic also discussed in Recoil issues under separate headings. citeturn7view3 AI without knowing this history and discussions can present family plus large list combination as default solution saying "works anyway".

More dangerous: using selector for "lazy init" of atom default. Atom API documentation says default can be selector and if selector default used, atom's default can dynamically update as default selector updates. citeturn8view3 But in real world, there's closed issue saying "memory leak when atom default is selector". This issue claims making atom default lazy with selector "caches all old set values" leading to memory leak. Proposed fix even touches eviction strategy with cachePolicy_UNSTABLE. citeturn7view1 AI cannot be expected to catch such "exists in docs but turned out problematic in practice" distinctions.

Short code examples showing normalization plus error. Good example with ID-based atomFamily plus derived selectorFamily. Import atomFamily and selectorFamily from recoil. Type User with id, name, teamId optional. Export const userByIdAtom equals atomFamily User or null, string with key entities/userById and default null. Export const userNameSelector equals selectorFamily string, string with key derived/userName and get with id parameter showing get function with user equals get userByIdAtom id, return user?.name or (nameless). Comment: Atom source of truth, selector derived state, separation clear. citeturn8view2

Risky pattern making atom default lazy init with selector. Some scenarios reported memory leak. citeturn7view1 Import atom and selector from recoil. Export const transactionsAtom equals atom with key transactions and default selector with key transactions/default and get async arrow returning await retrieveTransactions. Comment showing lazy init intent.

Mitigation strategies for developer: I put two safety belts in atom/selector design. First, I do normalization "little but correct", using families only when really need ID-based sharing. Patterns like "pulling thousands of items with loop in selectorFamily" I put in early performance test because this locking CPU was reported with real issue. citeturn7view0 Second, in controversial patterns like making atom default lazy with selector, I shift to atom effects or more controlled loading strategies. Recoil explains atom effects specifically with "putting policy into atom definition" motivation and allows managing side effects by returning cleanup handler. citeturn4view1

Technical background: Recoil selectors can be sync or work async by returning Promise. Documentation positions selectors as "idempotent/pure function" but also explicitly shows get function can return Promise. citeturn8view0 On atom side, default value can be Promise or async selector, in this case atom becomes "pending" and can trigger Suspense. citeturn8view3turn10view2 This is nice but in practice all questions like "when will fetch trigger", "how will it cache", "what happens in SSR" are design decisions.

AI's concrete error modes: Even in simplest scenario like do I fetch data on render or on button click, there are different patterns in Recoil ecosystem. For instance in Recoil issue about data fetching scenario with web API, two options asked: (a) do fetch on onClick writing result to atom, or (b) onClick changes an atom and selector reloads through dependency? citeturn5view2 This question alone makes it hard for AI to produce "one correct answer" because answer depends on product behavior like prefetch, idempotent, user action, rate-limit. AI mostly chooses pattern (b) because "reactive", but this can lead to selector triggering again and again as atom changes and unintentionally raining API calls.

On atom effects side, Recoil officially supports policies like persistence/sync/history with setSelf and onSet. citeturn4view1 But there's subtle landmine here: async setSelf call, according to docs, can overwrite atom's value if comes after atom set by user. citeturn4view1 In "async init plus user interaction" codes AI produces, you mostly see this race condition. User fills form, then old state from localForage arrives and rewinds form. This isn't Recoil's fault, it's design error.

On snapshot side also async subtle detail: Snapshot documentation says snapshots held during callback duration, retain needed for async usage, also warns "async selectors can be canceled if not actively used, must retain if looking with snapshot". citeturn6view2 AI assistants frequently skip this retain requirement in codes wanting to resolve async selector with snapshot to see resolved value. Result becomes hair-pulling failure like "sometimes comes sometimes doesn't".

Short code examples showing async selector plus atom effect. Async selectorFamily fetching with ID parameter, same parameter gets memoized. citeturn8view2turn8view0 Import selectorFamily from recoil. Export const userQuery equals selectorFamily with key queries/user and get with id string parameter async arrow. Const res equals await fetch /api/users/${id}. If not res.ok throw new Error User couldn't be fetched. Return await res.json.

Warning atom effect with async setSelf showing watch for race condition. According to docs "if atom set, later setSelf can overwrite". citeturn4view1 Import atom from recoil. Export const settingsAtom equals atom with key settings, default object theme light, effects array with setSelf, onSet, trigger parameters. If trigger equals get, comment persisted value coming late can overwrite user change. setTimeout with setSelf object theme dark after 1000ms. onSet with newValue arrow, comment persistent write etc. localStorage.setItem settings with JSON.stringify newValue.

Mitigation strategies for developer: I apply two principles in async state. (1) selectors should be pure, if side effect needed move to atom effects or UI layer event handler, (2) "async init" should always be written with assumption "can overwrite user interaction". I take seriously trigger usage in atom effects documentation, especially limiting expensive init with trigger equals get, and don't neglect cleanup handlers like unsubscribe. citeturn4view1 If fetch needed on user action instead of render, I prefer onClick with useRecoilCallback for snapshot read plus set/refresh pattern because this hook designed with motivation of async reading without render-time subscription. citeturn9view2

Technical background: Recoil officially presents "observe all state changes" and inspect through snapshot approach. Dev tools guide says you can subscribe to state changes with useRecoilSnapshot or useRecoilTransactionObserver_UNSTABLE getting snapshot, also puts warning "API under development, will change". citeturn6view1turn6view2 Snapshot API page also notes this area evolving with _UNSTABLE emphasis. citeturn6view2 For time travel, useGotoRecoilSnapshot recommended. citeturn9view1turn6view1

AI's concrete error modes: Most typical AI error here: copying time-travel example from docs and storing snapshots inside React state. Problem is snapshot lifetime is limited. Time-travel issue on GitHub says keeping snapshot outside callback duration gives warning and "if you'll keep snapshot long, do retain/release" sharing example code. citeturn6view0turn6view2 AI assistant generally doesn't see this warning because example appears to "work", but even issue text mentions will turn into exception later. citeturn6view0

Another error is using useRecoilSnapshot everywhere in application for debug. Recoil documentation explicitly says this hook will re-render component at all Recoil state changes and asks to be careful. citeturn9view0 AI can suggest "put debug observer everywhere", then unnecessary render storm starts in prod. Additionally useGotoRecoilSnapshot page notes transaction example is inefficient because subscribes "all state changes". citeturn9view1turn9view0

Short code examples showing correct time travel. If I'll store snapshot, I comply with retain/release discipline. citeturn6view2turn6view0 Import useEffect and useRef from react, useRecoilSnapshot and useGotoRecoilSnapshot from recoil. Export function UndoRedo. Const snapshot equals useRecoilSnapshot. Const gotoSnapshot equals useGotoRecoilSnapshot. Comment holding retained snapshots here. Const historyRef equals useRef array type. useEffect with const release equals snapshot.retain comment extending snapshot lifetime. historyRef.current.push object snap snapshot and release. Comment example 50 step limit. If historyRef.current.length greater than 50, const first equals historyRef.current.shift, first?.release. Return arrow function comment releasing all when component closes. historyRef.current.forEach h arrow h.release. historyRef.current equals empty array. With snapshot dependency. Return button onClick showing last equals historyRef.current.at(-2) comment previous one, if last gotoSnapshot last.snap showing Undo.

Debug state dump with "on-demand snapshot" approach. Dev tools guide and useRecoilCallback recommend this. citeturn6view1turn9view2 Import useRecoilCallback from recoil. Function DumpStateButton. Const dump equals useRecoilCallback with snapshot parameter async arrow. console.debug Atom/selector dump starting. For const node of snapshot.getNodes_UNSTABLE. Const value equals await snapshot.getPromise node. console.debug node.key value. Return button onClick dump showing State Dump.

Mitigation strategies for developer: I design debugging tools like snapshot observer and time travel for "dev" not "prod", taking seriously "under development" note in docs putting durable abstraction layer (adapter) against version changes. citeturn6view1turn6view2 If I'll store snapshots, I make retain/release discipline into code standard, otherwise "ghost snapshot" and memory pressure can happen over time. citeturn6view2turn6view0 Additionally I use feature flag or env guard to not put hooks subscribing to all state changes (especially useRecoilSnapshot) into prod bundle. citeturn9view0turn9view1

Technical background: Recoil's performance promise relies on "render as needed" idea with atom-based subscription and selector re-evaluations. Core Concepts says when atom updated, components subscribed to that atom re-render. citeturn8view1 Selector documentation also explains selector re-evaluates when dependency changes and additionally warns "if you mutate selector value object, subscriber notification can be bypassed". citeturn8view0 So performance comes when application modeling done right, when done wrong Recoil isn't "magic".

AI's concrete error modes regarding performance and memory: In large collections, getting a bunch of IDs one by one with get in loop with selectorFamily can hit CPU to 100% locking main thread in practice. Developer experiencing this opened issue saying "computation takes very long with big array of ids, CPU 100%" sharing example code. citeturn7view0turn8view2 AI can suggest this pattern as "very clean" but problem explodes when scale grows.

Memory side has similar reality: there's issue claiming atomFamily/selectorFamily resources not freed at unmount and key changes, containing strong suggestion like "should be marked _UNSAFE". citeturn7view2 Memory leak reports exist in pattern making atom default lazy init with selector. Issue explains this leak relates to cache behavior and eviction suggestion made with cachePolicy_UNSTABLE. citeturn7view1turn8view0 These don't mean "Recoil is bad" but mean "wrong pattern, wrong cache policy, wrong scale assumption". AI mostly thinks "copy-paste scales".

Ecosystem compatibility for large scale question: Real risk in big project isn't just performance but maintenance and compatibility. Recoil repo being archived and last release note staying in 2023 affects long-term maintenance plan. citeturn10view0turn10view2 In such situation, AI assistant saying "latest Recoil feature" can suggest actually controversial/_UNSTABLE API causing you to lock into it in prod.

Additionally AI's another scale risk in supply chain: models generating code can suggest hallucinated package names and this creating "package confusion" attack surface was examined in detail in large-scale study. citeturn11search0turn11search4 In Recoil ecosystem also third-party tools exist like recoil-persist and recoilize. AI can suggest non-existent package as if exists. This is why I don't accept "package AI suggested equals automatically correct".

Short code examples showing performance and safety belt. Warning large lists plus selectorFamily loop reported as "CPU 100%" in issues. citeturn7view0 Comment I definitely measure this type pattern with profiler. Const resourcesState equals selectorFamily with key resourcesState and get with ids string array parameter showing get function with ids.map id arrow get resourceState id.

Good example consciously choosing cache policy, passes in issue as leak fix suggestion. citeturn7view1turn8view0 Import selector from recoil. Export const safeSelector equals selector with key transactions/defaultSelector, get retrieveTransactions, cachePolicy_UNSTABLE with eviction most-recent. Comment controlled eviction instead of keep-all.

AI versus Human: Recoil output comparison table intentionally simplified. I prepared following table to quickly answer question "can I take Recoil code written with AI to prod same day". Recoil-specific risks like key stability, snapshot retain discipline, family scale, _UNSTABLE APIs affect most criteria. citeturn8view1turn6view2turn7view0turn10view0

Criterion AI Generated Recoil Code Human Generated Recoil Code
Correctness Mostly "works" but fragile in edge cases like retain, race, cache Validated with behavior-focused tests, modeling according to intent
Debuggability Snapshot/time-travel errors and "subscribe to all state" traps frequent Keeps debug tools in dev, snapshot lifetime and cost managed
Maintainability Key standards, normalization, file organization tend weak Standards, key dictionary, reusable selector/atom patterns
Package size Mostly similar but AI can suggest "hallucinated package" (supply chain risk) Package choice conscious, dependency policy and security scanning exist
Runtime safety Race condition, memory leak patterns, _UNSTABLE API lock risk Scale and maintenance plan considered, "archived repo" reality accounted for

Top comments (0)