Recoil
A React state-management library built around atoms and derived selectors, with first-class async and Suspense support.
Repository Health
Technical Analysis
Recoil is a state-management library for React that models application state as a graph of small, independently updatable units called atoms, plus pure derived values called selectors. Instead of one large reducer or a single global store, state is split into many discrete pieces that components subscribe to individually, so only the components that actually read a changed atom or selector re-render.
Selectors can be synchronous or asynchronous, and when a selector returns a promise, any component reading it will automatically suspend until the promise resolves, integrating directly with React Suspense and Concurrent Mode instead of requiring separate data-fetching middleware. Recoil also tracks a dependency graph between atoms and selectors internally, which it uses both to propagate updates efficiently and to garbage-collect state that no longer has any subscribers (via its retention system) so long-lived apps don’t leak memory as UI is mounted and unmounted.
The project was published by Facebook’s (Meta’s) open-source team as an experimental library, and ships companion packages — recoil-sync for persisting/restoring atom state, and recoil-relay for integrating with Relay’s GraphQL store — from the same monorepo.
What You Get
atom()andselector()primitives for declaring individual, independently subscribable pieces of state- Automatic Suspense integration — async selectors suspend reading components until their promise resolves, with no extra data-fetching layer required
- A
SnapshotAPI anduseRecoilCallback/useRecoilTransaction_UNSTABLEhooks for reading and atomically updating multiple pieces of state outside of render - Automatic retention/garbage-collection of atoms and selectors that no components are subscribed to anymore
- Companion packages (
recoil-sync,recoil-relay) for URL/storage persistence and Relay/GraphQL integration - Hand-maintained TypeScript type definitions (
typescript/recoil.d.ts) validated with dtslint, alongside a Flow-typed source tree
Common Use Cases
- Sharing derived, computed state (e.g. filtered/sorted lists, aggregated totals) across many components without prop drilling or re-running expensive computations
- Fetching and caching async data per-component via selectors that suspend, instead of wiring up a separate query library
- Large forms or dashboards where different sections need independent, fine-grained re-render boundaries rather than one big state blob
- Apps already using Relay/GraphQL that want a compatible client-state layer via
recoil-relay
Under The Hood
Architecture
Recoil centers on a per-<RecoilRoot> Store (packages/recoil/core/Recoil_RecoilRoot.js) that holds StoreState/TreeState and is threaded through React context. State changes flow through Recoil_FunctionalCore.js’s pure setNodeValue/initializeNode functions, which consult a dependency graph maintained in Recoil_Graph.js to know which downstream atoms/selectors need to be recomputed. Recoil_Retention.js implements reference-counted garbage collection so atoms/selectors with no subscribing components are released rather than retained forever. Recoil_Snapshot.js layers immutable point-in-time views on top of this for useRecoilCallback and transactions. The module boundaries are clean: adt/ for generic data structures (e.g. a persistent queue), core/ for the store/graph/state machinery, recoil_values/ for the public atom/selector constructors, hooks/ for the React-facing API, and caches/ for selector memoization — a layered design where the public hooks are a thin veneer over the graph engine.
Tech Stack
The implementation is written in Flow (@flow strict-local per file) rather than TypeScript, built with Babel and bundled via Rollup (rollup.config.js) into CommonJS, ES module, UMD, and React Native targets declared in package.json. A hand-written typescript/recoil.d.ts is maintained separately and checked with dtslint so TypeScript consumers get accurate types despite the Flow-based source. The package itself has almost no runtime dependencies — hamt_plus for persistent hash maps and transit-js for serialization — with React >=16.13.1 as the only peer dependency. The repo is a Yarn-managed monorepo (packages/ for recoil, recoil-sync, recoil-relay, refine, shared; packages-ext/ for example apps and a website), with Jest configured across all sub-packages and ESLint using a custom eslint-plugin-rulesdir ruleset.
Code Quality
Each core module (adt, core, recoil_values, hooks, caches) ships its own __tests__ directory, and recoil_values additionally has __flowtests__ for compile-time type assertions — dozens of test files in total, run via yarn test (Jest + babel-jest) and exercised in CI through .github/workflows/nodejs.yml. Flow’s strict-local mode is enforced file-by-file, giving the internal codebase static type checking without adopting TypeScript. Naming is consistent and Facebook-internal-style (Recoil_ file prefixes, @oncall recoil headers), and functions carry detailed doc comments explaining semantics and edge cases (e.g. scoped atoms, retention behavior) directly above their implementations.
API Design
The public surface is intentionally small: atom({key, default}) and selector({key, get, set?}) cover the vast majority of usage, with hooks like useRecoilState, useRecoilValue, and useSetRecoilState mirroring React’s own useState naming so the mental model transfers quickly. Async selectors need no special-cased API — returning a promise from get is enough to get Suspense behavior for free, which removes a class of boilerplate that separate data-fetching libraries usually require. The one point of friction is the key string every atom/selector must supply for internal identification and persistence, which is easy to collide across a large codebase and pushes teams toward key-prefixing conventions.