· Revised
Forking Recoil
A TypeScript continuation of Recoil, with React 18 and 19 support, a Rollup ESM and CJS build, and the same public API.
The product I was working on, Chatmaid, schedules and sends WhatsApp messages from your own number, through a mobile app, a web dashboard, and an API. The whole stack used Recoil for state. A React 18 upgrade broke it. Upstream Recoil had already stopped moving, so I forked the library.
React 18 changed how a library must subscribe to store updates. The supported path is useSyncExternalStore, which Recoil adopted in 0.6 on 28 January 2022 (#1488). The hook exists so an external store cannot tear under concurrent rendering: React reads a consistent snapshot for that render. It also requires a getServerSnapshot for server render. Without that function, Next.js apps failed during SSR (#1526). Some host environments, including nested renderers, exposed a React 18 dispatcher but not the hook itself, which produced useSyncExternalStore is not a function. Recoil added a fallback in 0.7.6 (#2001, #2010), and further Next.js 13 SSR fixes in 0.7.7 (#2082, #2086). Application and test code had their own breaks: ReactDOM.render gave way to createRoot, and act moved from ReactTestUtils into the react package. Chatmaid's upgrade hit this class of failure.
The last Recoil release, 0.7.7, shipped on 1 March 2023. The last upstream commit was on 7 September 2023. A planned handover to external maintainers stalled after the remaining public maintainers were laid off (discussion #2171). Meta archived facebookexperimental/Recoil on 1 January 2025.
The fork
I created Mutesa-Cedric/Recoil-next on 27 November 2024, a month before the archive. The next day, in fb238a5, I raised the peer dependency from react >= 16.13.1 to react >= 18.3.1, switched the example app to createRoot, and replaced ReactTestUtils.act with act from react. That commit updates the package metadata together with the test and example trees, 44 files in all. The current peer range is react >= 18. The development tree uses React 19.1.1, and the test matrix covers React 18 and React 19 on Node 20 and 22.
TypeScript
Recoil was written in Flow. On 6 July 2025 I started a port to TypeScript (c09d731). This was a rewrite of the sources, not a types layer over Flow. Shared utilities, the ADT and cache layers, atoms, selectors, hooks, and RecoilRoot all moved before the first publish. The core package was complete on 16 July (ecb69f0). I then ported recoil-relay, recoil-sync, and refine. Pull request #1 landed on 8 August and made the TypeScript tree the repository root.
The build moved to Rollup, emitting ESM and CommonJS with generated typings. Tests moved from Jest to Vitest. The workspace uses pnpm. ESLint is on v9. The ported core test tree has 43 files and 284 test/it cases. I published the rewritten package as recoil-next 0.1.0 on 28 July 2025. It has been downloaded more than 350,000 times since then.
Migration is an import change, and that is the only required code change. The Recoil API is unchanged, so the original Recoil documentation still applies:
// Before
import {atom, selector, useRecoilState} from 'recoil';
// After
import {atom, selector, useRecoilState} from 'recoil-next';
Version 0.2.0, on 10 August, stopped react/jsx-runtime from being bundled into the output (f327be4) and updated the todo example to use recoil-next. On 15 August the ported recoil-core, recoil-sync, and recoil-relay tests were passing. I added __DEV__ support and published 0.3.0.
recoil-next@0.4.4 ships an unminified ESM build of 204 KB, 41.7 KB gzipped. Recoil 0.7.7's unminified es/index.js is 275 KB, 63.0 KB gzipped.
The NaN loop
An atom whose default was NaN could throw Maximum update depth exceeded as soon as a component read it:
const Atom = atom({
key: 'myKey',
default: NaN,
});
export default function Test() {
const val = useRecoilValue(Atom);
return <div>{val}</div>;
}
Loadable.is() compared contents with ===. In JavaScript, NaN === NaN is false, so two loadables holding NaN looked different on every check. Recoil uses that equality to decide whether a value has changed and subscribers must re-render, so the store kept updating. The fix is Object.is, which treats NaN as equal to itself:
is(other: Loadable<any>): boolean {
return (
(other as any).state === (this as any).state &&
Object.is((other as any).contents, (this as any).contents)
);
}
That change is 2342fff, documented in #3. It shipped in 0.4.0 on 7 November 2025.
One package
After the TypeScript port, recoil-relay, recoil-sync, and refine still lived in the same workspace as core. Those packages are optional integrations, not the Recoil API. I published the companions as recoil-relay-next and refine-next on 18 August 2025. On 28 December, pull request #4 removed them from this repository. A core install is the Recoil API and nothing else. recoil-next@0.4.4 was published on 26 January 2026.
The public API is still the Recoil one, including atoms, selectors, hooks, and RecoilRoot. The sources are TypeScript, and the build emits ESM and CommonJS for React 18 and 19.