I'm sure an example can be simplified, but somewhere between 0.0.7 and 0.0.10 memory leak were introduced.
import React from "react";
import { RecoilRoot, useSetRecoilState, atom, useRecoilValue } from "recoil";
const state = atom({
key: "test",
default: []
});
const Subscriber = () => {
const setTest = useSetRecoilState(state);
React.useEffect(() => {
const u = old => {
const item = Math.random();
if (old.length >= 5000) {
return [item, ...old.slice(0, -1)];
} else {
return [item, ...old];
}
};
const t = () => setTest(u);
setInterval(t, 10);
}, []);
return null;
};
const Test = () => {
const test = useRecoilValue(state);
return <div>{test.length}</div>;
};
export default function App() {
console.log("render");
return (
<RecoilRoot>
<Subscriber />
<Test />
</RecoilRoot>
);
}
Codesandbox:
0.0.10: code, demo
0.0.7: code, demo
Sandbox with 0.0.7 version doesn't have memory leak, as you might see here (app run about 2 minutes between heap snapshots).
0.0.10:

0.0.7:

While setInterval might seem too synthetic to care about, I originally discovered an issue in an app where state is bombarded via WebSockets (plural).
Would the issue be addressed? Had to switch from recoil because of it. (maybe I'm doing something wrong)
Having issues with memory as well
We're finalizing some changes for Concurrent Mode now, then can try to investigate this before the next release.
In case it's helpful, here is a sample webapp with code that demonstrates symptoms of a memory leak:
Webapp: https://saltycrane.github.io/recoil-vs-context-grid-test/recoil?x=200&y=100
Repo: https://github.com/saltycrane/recoil-vs-context-grid-test
@saltycrane - Thank you for the reproducer.
@drarmstr any ETA on the next release? we're heavily dependant on Recoil and plan to migrate a huge application from redux to recoil.
Thank you for creating a missing piece of React 鉂わ笍
@Nishchit14 I've myself migrated app to focal
It has similar concepts and is battle-tested in production at Grammarly.
Give it a try.
@drarmstr any ETA on the next release? we're heavily dependant on Recoil and plan to migrate a huge application from redux to recoil.
Thank you for creating a missing piece of React 鉂わ笍
Pretty soon, actually!
This is fixed in master, which we plan to release tomorrow. Please note that development builds will leak memory by pushing onto window.$recoilDebugStates. You can delete from this array if it causes you problems.
@opudalo Sorry this took so long to fix. Thanks for reporting it! I'd be interested to hear how your experience with Focal goes.
@davidmccabe No worries, priorities, priorities 鈥撀營 understand.
As for focal, it is my go-to state management library. Reactivity overall has wonderful devx and fits nice for web apps.
@opudalo Focal seems independent state management lib, How it connects with React?
@Nishchit14 it is not. It is build for react. See example: https://github.com/grammarly/focal/#example
Hit me up on twitter (@opudalo) to not continue offtopic here.
This is fixed in master, which we plan to release tomorrow. Please note that development builds will leak memory by pushing onto
window.$recoilDebugStates. You can delete from this array if it causes you problems.
@davidmccabe Seems still memory leak in dev mode. ver 0.0.13.
Adding 1500 atomFamily

Adding 3000 atomFamily

demo code:
import React from 'react';
import { atomFamily, useRecoilCallback, useRecoilTransactionObserver_UNSTABLE } from 'recoil';
const testState = atomFamily({
key: 'test',
default: params => params
})
let counter = 0
let setNullCounter = 0
let resetCounter = 0
const App = () => {
const [inputValue, changeInputValue] = React.useState('');
useRecoilTransactionObserver_UNSTABLE(({snapshot}) => {
console.log('snapshot', snapshot);
})
const getSnapShot = useRecoilCallback(({snapshot}) => () => {
// @ts-ignore
for(const atom of snapshot.getNodes_UNSTABLE()){
const atomLoadable = snapshot.getLoadable(atom);
console.log( atomLoadable.contents, atom)
}
})
const handleAddState = useRecoilCallback(({ set }) => id => {
// @ts-ignore
set(testState(id), { id, text: 'default string', tips: 'default string' })
})
const handleReset = useRecoilCallback(({ reset }) => id => {
// @ts-ignore
reset(testState(id));
})
const handleSetNull = useRecoilCallback(({ set }) => id => {
// @ts-ignore
set(testState(id), null)
})
const handleResetWrapper = () => {
for (let i = 0; i < 100; i++) {
handleReset(resetCounter)
resetCounter++
}
console.log('reset', resetCounter)
}
const handlePlus = () => {
for (let i = 0; i < 100; i++) {
handleAddState(counter)
counter++
}
console.log('count', counter)
}
const handleSetNullWrapper = () => {
for (let i = 0; i < 100; i++) {
handleSetNull(setNullCounter)
setNullCounter++
}
console.log('setNull', setNullCounter)
}
return (
<div>
<input value={inputValue} onChange={e => changeInputValue(e.target.value)}/>
<button onClick={handlePlus}>plus</button>
<button onClick={handleSetNullWrapper}>setNull</button>
<button onClick={handleResetWrapper}>reset</button>
<button onClick={getSnapShot}>log</button>
</div>
)
}
export default App;
Most helpful comment
We're finalizing some changes for Concurrent Mode now, then can try to investigate this before the next release.