Recoil: Memory leak

Created on 12 Jul 2020  路  15Comments  路  Source: facebookexperimental/Recoil

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:
image
0.0.7:
image

bug performance

Most helpful comment

We're finalizing some changes for Concurrent Mode now, then can try to investigate this before the next release.

All 15 comments

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
image

Adding 3000 atomFamily
image

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;
Was this page helpful?
0 / 5 - 0 ratings

Related issues

art1373 picture art1373  路  4Comments

ymolists picture ymolists  路  3Comments

Sawtaytoes picture Sawtaytoes  路  4Comments

pesterhazy picture pesterhazy  路  4Comments

eLeontev picture eLeontev  路  3Comments