facebook / react

The library for web and native user interfaces.
https://react.dev
MIT License
229.66k stars 47.02k forks source link

[Concurrent] Safely disposing uncommitted objects #15317

Open danielkcz opened 5 years ago

danielkcz commented 5 years ago

How to safely keep a reference to uncommitted objects and dispose of them on unmount?

For a MobX world, we are trying to prepare for the Concurrent mode. In short, there is a Reaction object being created to track for observables and it is stored within useRef.

The major problem is, that we can't just useEffect to create it in a safe way later. We need it to start tracking the observables on a first render otherwise we might miss some updates and cause inconsistent behavior.

We do have a semi-working solution, basically, a custom made garbage collector based on setTimeout. However, it's unreliable as it can accidentally dispose of Reactions that are actually being used but weren't committed yet.

Would love to hear we are overlooking some obvious solution there.

trusktr commented 4 years ago

That's all fair enough. It's React team's library, and React team has the right to say how React is supposed to be used and does not need to support everyone else's alternative paradigms.


That being said,

It is not intended as a constructor

That may be the intent, but by providing ways to define method-like things (f.e. useEffect) the FC body has become similar to a constructor because the "instance" (the lexical scope) has the ability to have long lived things associated with it after creation (normally those things are created in something like useEffect, not in the function body itself) including DOM objects.

Actually, the "instance" is the cumulative set of scopes from every time that the FC has been called in its life time (a very important but subtle difference, sometimes a confusing one).

The similarity of FCs to classes may be an unintentional invitation for code that creates state at the beginning. The render (f.e. the FC return value) is the only thing that an external dependency-tracking system can rely on for tracking dependencies used for rendering.


Something like onCancel (or whatever name it would have) seems like a very easy way to solve this problem.

lxsmnsyc commented 4 years ago

To solve this, what I did is to treat my own objects to have a dispose/cleanup logic, and created a hook similar to what @dai-shi created:

function useDispose(dispose) {
  // schedule a cleanup, 16 could be a good number.
  const timeout = setTimeout(dispose, 64);
  // cancel the scheduled cleanup if the side-effects went through.
  useEffect(() => {
    clearTimeout(timeout);
  }, [timeout]);
}

This worked, in my case, specially just to prevent an external source (e.g. an object that subscribes to multiple events) from subscribing twice.

Andarist commented 4 years ago

Actually, the "instance" is the cumulative set of scopes from every time that the FC has been called in its lifetime (a very important but subtle difference, sometimes a confusing one).

This mental model is not correct - some render calls can be freely dropped by React so it's a false assumption to think about an instance being a cumulative set of scopes from every time that FC has been called in its lifetime. If anything it's a cumulative set of committed scopes. This distinction is very important in understanding how to obey React rules.

It has been mentioned here in the thread already - but those rules are old. They just start to be way more important now but breaking those rules probably has caused mem leaks and similar problems in the past already.

I can't say about the community as a whole but it has always been quite clear for me that constructor in a React's class was not an appropriate place for side-effects and while it was maybe annoying at times I just have always used such logic to componentDidMount and usually had no problem with that. It was a simple refactor.

bvaughn commented 4 years ago

Adding a simple hook (f.e. onCancelled) would be fairly simple for React to implement (I think) and it would prevent a lot of complicated code in code written on top of React (namely those state tracking libs). It's a win-win.

You are right that it would be easy to add such a hook. The reason we haven't done it is that it would have a significant negative impact on performance.

I think of the canonical reason why a render might get thrown away as the following:

  • because there's no way to prevent a React.FC from returning something, so basically they always "render" because they always construct a vdom tree. Otherwise, with an idea like useRender in my last comment, React could selectively skip renders for any of the reasons you listed (saving CPU and memory cost, and avoiding the above problem in a different way).

Unless I'm reading this comment wrong, I think it's a misconception. A component can already return null to say that it does not want to render anything. That is not the reason a render may be thrown away. There are a few reasons why React might not commit the value rendered by a component, including:

kitten commented 4 years ago

@bvaughn @gaearon Are you considering a system for push-based subscriptions that coöperate with the component lifecycle by any chance? I still find it to be a little odd that most systems are rather pull-based once they instantiate their state, i.e. outside of effects we prefer to be able to get either "snapshots" (useMutableSource) or the state directly (useSubscription). The same goes for suspense, it's all rather dependent on having pull-based state/effect sources.

I think a lot of problems could be solved if there was a way to make push-based systems work better together with React's hooks & lifecycles. This would indeed be rather novel, but I think it'd fix this. This is especially important for derived event sources, where the state isn't pull-accessible. Currently in urql we work around this by creating a state snapshot by subscribing & unsubscribing to our source to get a current one-time snapshot of our state.

However, if state was tied to some kind of observable source from the start of a component lifecycle, similar to how useMutableSource prevents tearing and forces the source into a more coöperative timing, it'd have a good method of ensuring that a synchronous event would lead to a valid initial state and that subsequent events would have a sufficient amount of backpressure applied to them — in other words, a push-event would still act just like a normal state setter today.

CaptainN commented 4 years ago

If there are truly use cases that aren't solvable with the current primitives that React provides, then the community should collect a list of specific use cases that are completely incapable of being solved so that we can figure out what primitives are needed.

Here's the issue in Meteor. Meteor uses a computation for setting up observers on "reactive" sources, based on accesses to those sources on the first run of the computation function. It does that in the render pass now, with some caveats. We could simply turn off the observer for the render pass, then run it again in useEffect - in this case it would always need to run again, because state might change between render and useEffect. We currently use timers to try and avoid that, but we already had to disabled that for one case to support StrictMode, and it's proving unmaintainable.

The question is, if we have to run our setup code a second time on commit, is it really earning performance, or just moving the performance hit off to other parts of the app? Then again, in most cases in Meteor, it's probably not a big deal to run the computation again - most computations are relatively cheap, unless there is some huge complex mini mongo query or something

I went down the rabbit hole of trying to pull that out in to a data source, as described in the "suspense for data" section in the React docs, but that has a number of problems. Primarily, it complicates what is otherwise a clear and easy API for us (with useTracker - except for deps, developers regularly mess up deps), but it also felt like I was just picking up that kicked down the road performance work, and things were getting COMPLICATED. Ultimately, I couldn't figure out a real great way to make an externalized API like that work because of various scoping issues, and the complexity of basically rebuilding redux with a sort of dependency injected inversion of control system (which is where I was headed) felt like a step backwards.

markerikson commented 4 years ago

@CaptainN : fwiw, that "might need to run again because state could have changed before the effect" part sounds exactly like how the new useMutableSource hook is intended to work. Have you looked at that at all?

arcanis commented 4 years ago

I've also hit a few times the case of a component needing to instantiate objects tied to the WASM heap. In those cases there is no garbage collection to rely on for freeing uncommitted resources (not yet, at the very least), so the only choices seem to be to enter a world of double-renders with useLayoutEffect, or to break the CM assumptions. I'm currently doing the first, but it seems prone to degraded performances should those components be nested a bit too much.

[edit] I made a search on "WASM" before sharing my experience, but I think the following comment is pretty much what the React team would answer to that: https://github.com/facebook/react/issues/15317#issuecomment-716210926

That said, you could just rely on GC eventually. The WeakRef proposal lets you add some cleanup logic. Its timing would not be deterministic, but neither would it be for a similar React-managed mechanism. So this seems like the longer term solution for the rare cases where you can’t use an effect and for some reason have to allocate a resource during render.

Unfortunately it'll be years before WeakRef reaches general availability. I guess it can work if CM takes years too 😛

CaptainN commented 4 years ago

@markerikson I did look at that a few months back, but wasn't sure exactly what that does or how (or if it was ready for use) - is that finalized or still in RFC? Last time I looked at it, I thought it would drop any component using it in to a "deopt" mode, opting out of the benefits of concurrent mode for that component, and wanted to see if I could keep the benefits.

It's on my list to pursue a useMutableSource based solution for a future version the generic useTracker (or simply eat the second setup pass) while moving to a new more tailored set of hooks to integrate React with Meteor, which can be implemented in the pure way, without a ton of complexity. Those hooks already look better.

markerikson commented 4 years ago

@CaptainN : useMutableSource has been merged in, but I think it's still only available in "experimental" builds. The "deopt" behavior as I understand it only kicks in in certain situations.

Not saying it's going to magically fix your problems, but I think you should go review the RFC and PR discussions around it and give it a shot to see if it does potentially address your use case.

brucou commented 4 years ago

But the React component contract has always emphasized that the render method is meant to be a pure function of props and state.

@gaearon Functions components that use hooks are not pure in the most general cases (i.e., unless you find a hook that is actually pure). I am not sure what is the render method you are referring to. I assume you are referring to the render method use in React's class API? That render method should indeed be a pure function of the parameters it is passed, as it is documented. But then there seems to be some confusion in your next assertion:

But it shouldn’t be surprising that when we add new higher-level features to React that rely on the component contract having a pure render method, those features may not work. I think it’s fair to say that render being pure is well-documented and one of the very first things people learn about React.

The component contract you are referring to is the class component contract. If you get rid of class components, you get rid of that contract. Then function components (if they use hooks) renounced to purity so you can't really refer there to a pure render here. Given that functional components and concurrent mode are a new thing, why not clarify the new contracts for the new thing? That takes possibly wrong assumptions out of the way together with the bugs they create. This especially applies to concurrent mode as it seriously changes what was assumed to be a contract in former React versions. Some people continue to think that it is React as usual apart from cosmetic changes but it is really a fundamental change. I am not sure this has been captured yet in the documentation.

bvaughn commented 4 years ago

I am not sure what is the render method you are referring to.

class ClassComponent extends React.Component() {
  // ...

  render() {
    // This is a render method and should be pure.
  }
}

function FunctionComponent(props) {
  // This is also a render method and should be pure.

  // Built-in hooks (e.g. React.useState) are an exception
  // because they are explicitly managed by React
  // and so are more like locally created variables.
}
benjamingr commented 4 years ago

Can't MobX just use a FinalizationRegistry @mweststrate ?

brucou commented 4 years ago

@bvaughn The second case (FunctionComponent) is a function. Methods usually refer to functions within classes (or by extension objects). Then, again, it is not accurate to say that function components should be pure when them being impure is far from being an edge case (due to hooks usage). There is no such thing like an impure function that is like a pure function except this or that. People get confused precisely because we use the same words to mean different things. In the context of computer science, functions that may be impure are usually referred to as procedures. So you may call a function component a render procedure if you will, but not a pure function in a large majority of cases (when they use hooks). Which is one of my points and the reason why people get confused by functional programming notions decaffeinated with exception and metaphors.

bvaughn commented 4 years ago

I'm not looking to debate the semantics of "methods" or "functions", just addressing your comment that you were "not sure what is the render method". The answer is: Dan was referring to both class components and function components.

gaearon commented 4 years ago

I hear that the documentation does not do a good enough job defining “purity”. This is something we’re keeping in mind for the upcoming docs rewrite.

On the other hand, if you take the technical argument that “function components are impure because of Hooks”, I don’t think you can claim that “the class render method is pure” either. After all, it reads this.props and this.state which are by definition properties on a mutable instance — thereby making any render method technically impure. This technicality hasn’t prevented people from understanding that the rendering process in React (whether with a class method or a function component) is meant to be generally pure. But we could definitely be stricter in our definitions and this is good feedback.

brucou commented 4 years ago

@gaeron you are indeed right. The render class method is often impure too. That escaped me. I thought it received props as an argument, but it is true that it gets it from this. Actually often the reason why you would use a class is precisely to do this kind of impure things that pure functions does not let you do (this.state). Your point is well taken.

But still, whatever it is that you mean by the rendering process is meant to be generally pure can probably be expressed in a more precise (albeit more wordy) way. Did you mean something like the vDom should be (generally) computed only from props, context and children --- independently of how you actually get that information, and that effectful components should be the exception and non-effectful components the rule? There is a pattern like that, which combines renderless components and pure components. Or did you simply mean that React functions components can do all kind of things as long as the computation of those things only depends on props, context, children etc and a variety of React APIs? That is, the output (understood as effect executed + computed return value) of function components depend only on inputs and the React runtime state? So basically you don't do effects that do not go through a React API.

With concurrent mode, I believe that accuracy is going to be even more important. The hard part is going to have developers build the mental model that shortens debugging sessions and reduce the need for technical support. Of course, examples of usage will help a ton and you have good stuff already on the documentation site. But having consistency in words and meaning (we call that ubiquitous language in TDD) goes a long way too. It was for instance important that React introduced different words for rendering and for committing in order to explain the different operations that React does at different moments and it would be unfortunate to mix one with the other randomly.

mweststrate commented 4 years ago

Random update: in mobx-react-lite we are now experimenting with using a FinalizationRegistery as suggested (and executed) by @bnaya / @benjamingr. So far it looks quite promising and something that could be abstracted away in a generic hook (possibly with a timer based fallback).

The two liner summary of the approach is that in the component some a local object is allocated and stored using useState: const [objectRetainedByReact] = React.useState({}). With a finalizationRegistery we can then register a callback to get a notification when the objectRetainedByReact is garbage collected by the JS engine, which should be a safe heuristic that a an instance wasn't committed.

dai-shi commented 4 years ago

Interesting. So, it doesn't even check if it's committed with useEffect. Should we expect JS engine not to garbage collect objects very soon if there's enough free memory?

mweststrate commented 4 years ago

It does useEffect to mark comitted, left all the implementation details out 😉

On Tue, 3 Nov 2020, 22:31 Daishi Kato, notifications@github.com wrote:

Interesting. So, it doesn't even check if it's committed with useEffect. Should we expect JS engine not to garbage collect objects very soon if there's enough free memory?

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/facebook/react/issues/15317#issuecomment-721408206, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAN4NBHHYJZDCAVPTSM4DBDSOCAEPANCNFSM4HDRIAHQ .

benjamingr commented 4 years ago

Just a small correction, I suggested using FinalizationRegistry (for MobX) - the execution is 100% @Bnaya :]

(the idea came after discussing the idea ±2 years ago for MobX in some JSConf with Daniel Erhenberg in the context of "how should Node behave" and I figured "what other libraries am I involved with and may use this")

Michel had an interesting idea (to make this into a generic hook - useDisposer or something) which I believe would be very cool.

@gaearon I think it would be very useful if you can't commit to cleanup or when cleanup runs if React provided this sort of hook as part of React since the use case is pretty common. Is that something you'd be willing to explore or is that antithetical to the direction you want to see React take?

In any case I think it's better to discuss this once @Bnaya is further along with the useDisposer work.

dai-shi commented 4 years ago

Cool. I was going to draft something like useDisposer with FinalizationRegistry, but will sit and wait. My two cents is this is still like an escape hatch and is not something like a first recommended pattern.

Andarist commented 4 years ago

Is using FinalizationRegistry really a good idea? I mean - I understand some part of the frustration coming from people wanting this kind of a feature, I also understand why this is not something that the React team wants to support (unless compelling arguments are found). However, resorting to things like this is a huge bandaid, a hack. I think that no one even would question that. So if this is a hack (and not a small one) - is it worth pursuing? I feel like either idiomatic React approaches should be used - which for some use cases are at least quirky to deal with, I don't have super compelling use cases for myself regarding this as, usually, I can rather easily work around this or convince the React team that this is really needed for the community. It's obvious that the React team understands challenges and the use cases mentioned here but those deviate from their model and they are hesitant to implement this as it can actually be a footgun if not used properly, but maybe it's time to gather the people most interested in this in sort-of a focus group that would figure out the path forward? Maybe this happens somewhere in the background - I don't know, but at this point in time this really seems like a stagnant issue. A pretty much the same thing is repeated all over the place from both sides and the discussion seems kinda fruitless.

If no consensus is found then I expect such hacks to be actually implemented and used, not because I feel like that they are needed that much but just because of human nature. We all have strong opinions about things and it's inevitable that something like this will exist in the community eventually, question is - can we prevent it? Or at least eliminate it from being needed for more use cases?

Given the recent work on the docs - some of the use cases mentioned here should probably be documented there to present what's the React team's stance on them, what are proposed solutions.

markerikson commented 4 years ago

@Andarist fwiw, my take is that this might be a "hack" in the sense of "well, this really isn't how you ought to be writing your code in the first place based on the rules", but it may very well be a valid technical solution for MobX's use case and potentially other folks as well.

Given that, I wouldn't expect the React team to specifically recommend this approach, but perhaps it can be published as an independent package by whoever's writing it, and the React team could maybe at least document it as a known workaround.

sebmarkbage commented 4 years ago

Especially as an incremental upgrade approach can be a valid strategy. As new research is going into new patterns.

However, there's one interesting case to think about. The main motivation for FinalizationRegistry to be added to the language now of all times, is because it allows building interop with WASM linear memory (aka array buffers). In a more general sense, it's how you build interop with a tracing GC language and a ref count or manual collection GC language.

For any library implementing its backing store in a WASM world, this would be the primary way to interop with it.

There are many patterns that might be a bad idea around this (like opening web socket connections without deduplication until the GC runs). The core principle of using FinalizationRegistry to interop with an external memory management library isn't though. That's the point of FinalizationRegistry in the first place.

Bnaya commented 4 years ago

I've created useDisposeUncommitted hook, similar to MobX's impl https://www.npmjs.com/package/use-dispose-uncommitted I've also opened a self-PR to collect feedback on the initial version and api https://github.com/Bnaya/use-dispose-uncommitted/pull/3/files

danielearwicker commented 3 years ago

And for anyone like me who ends up here because their jest test uses observer and logs:

Jest did not exit one second after the test run has completed.

This usually means that there are asynchronous operations that weren't stopped in your tests. Consider running Jest with --detectOpenHandles to troubleshoot this issue.

Try:

import { clearTimers } from "mobx-react-lite";

// at end of your test:
clearTimers();

This tells the timer-based cleanup to run immediately instead of waiting N seconds.