Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Zombie child, yet again #302

Closed
ratoi-crysty opened this issue Feb 10, 2021 · 27 comments · Fixed by #303
Closed

Zombie child, yet again #302

ratoi-crysty opened this issue Feb 10, 2021 · 27 comments · Fixed by #303

Comments

@ratoi-crysty
Copy link
Contributor

ratoi-crysty commented Feb 10, 2021

There still seems to be the issue of zombie child. It requires some special criteria to reproduce:

  1. The state needs to have initial value
  2. The delete action, needs to be async called in regards to the event handler (Ex.: wrap the action call inside a setTimeout)
  3. This is reproducible only on the elements that were present in the initial state.

Note, that the same scenario is not reproducible while using redux with hooks.

Repo to reproduce: https://github.com/ratoi-crysty/zustand-zombie-child

@dai-shi
Copy link
Member

dai-shi commented Feb 11, 2021

Thanks for the repro.
This seems the same as what we discussed in #286.
The quick fix is: https://codesandbox.io/s/fast-shadow-12726?file=/src/components/example/list.tsx:631-660

Note, that the same scenario is not reproducible while using redux with hooks.

This, I'm not sure why though.

@ratoi-crysty
Copy link
Contributor Author

Yup! It seemed related to that and as you pointed out in your codebox, using unstable_batchedUpdates, will do the trick.

But I still have one question though.

What would be the downsides of moving the subscription from the useEffect to directly in the hook call? (Ex.: index.tsx)
This way, the listeners will be added in the same order as the component tree render order, therefore, avoiding having the child listener called before the parent. In this case, it should dramatically reduce, if not eliminate, the problem with the zombie child, as the parent will be rendered before the child.

@ratoi-crysty
Copy link
Contributor Author

As for the scenario when using react-redux. It seems that they are already using it. In the subscription file (Subscription.js) they are using batch to wrap the all the listeners calls. By default, the the batch function is unstable_batchedUpdates. This is set in index.js

@dai-shi
Copy link
Member

dai-shi commented Feb 11, 2021

What would be the downsides of moving the subscription from the useEffect to directly in the hook call?

That would be a side effect in render, which is never recommended in react.
In legacy mode, it would work, but in concurrent mode, it will be really problematic.
It may subscribe to the store and never unsubscribe from it, in some scenarios.

@ratoi-crysty
Copy link
Contributor Author

Thanks for your answer. It makes sense.

As for the scenario when using react-redux. It seems that they are already using it. In the subscription file (Subscription.js) they are using batch to wrap the all the listeners calls. By default, the the batch function is unstable_batchedUpdates. This is set in index.js

Maybe we should add the functionality to allow passing a batching method when creating a store, this way, it should prevent the zombie-child issue and also improve performance as it will call all listeners of that store inside one batch (if that is done outside the react event, like setTimeout). And having it optional, it will not add any breaking changes.

What's your opinion on this?

@dai-shi
Copy link
Member

dai-shi commented Feb 11, 2021

As for the scenario when using react-redux. It seems that they are already using it. In the subscription file (Subscription.js) they are using batch to wrap the all the listeners calls. By default, the the batch function is unstable_batchedUpdates. This is set in index.js

Ah, nice finding. I thought I saw this before, but I was searching batchedUpdates today and nothing found. 😓

@dai-shi
Copy link
Member

dai-shi commented Feb 11, 2021

We could try using batchedUpdates, but it'd be not easy to make bundles for react-dom and react-native. I wonder if v4 #160 will solve this issue eventually...

Oh, setBatch is totally opt-in. I missed it. That might work. Would you like to work on it?

But, then, the DX is not very good, isn't it? The workaround in #286 seems easier to me.

@ratoi-crysty
Copy link
Contributor Author

ratoi-crysty commented Feb 11, 2021

After giving further thoughts, I think that having a setBatch will probably not add allot of value. Because, if you manually wrap your action (in my example, deleteItem) inside the batching method, that will automatically wrap all the listeners calls inside it, as zustand will do this synchronously. And therefore, have the same value as a setBatch method. Another way to it, is to manually wrap the set method in the store creator and only use the wrapped method to update the state, this will also have the same effect as setBatch.

What do you think? Do you see any value in implementing a setBatch? Probably something that I have missed?
If not, then you could close the issue, as it works as expected.

@dai-shi
Copy link
Member

dai-shi commented Feb 11, 2021

Yeah, that's what I meant by "DX is not very good". I don't think it's worth the effort, at this point. But let us keep this in mind.

For now, it would be best for us to add a small section about the workaround in readme.md. Would you or anyone like to work on it? cc @martonlanga @Tirzono .

(I will close this issue a bit later.)

@ratoi-crysty
Copy link
Contributor Author

ratoi-crysty commented Feb 11, 2021

@dai-shi let me know if this is ok or if I should change anything.

dai-shi added a commit that referenced this issue Feb 11, 2021
* feat(docs): add an example for setting the state outside event (#302)

* fix gramar issues

* Point more details URL to the issue instead of the tweet

* Simplify example

Co-authored-by: Daishi Kato <dai-shi@users.noreply.github.com>

* Update readme.md

* Update readme.md

Co-authored-by: Cristian Ratoi <cristian.ratoi@everymatrix.com>
Co-authored-by: Daishi Kato <dai-shi@users.noreply.github.com>
@drcmda
Copy link
Member

drcmda commented Feb 11, 2021

one question @dai-shi didn't @JeremyRH find a way to do this without batchedupdates? i remember he told me we can now remove it. the trouble is mostly that we're using two reconcilers: react-dom and r3f.

our middleware currently looks like this:

import { Renderer } from 'react-three-fiber'
import { unstable_batchedUpdates } from 'react-dom'

const batched = (config: StateCreator<BuerliState>) => (
  set: SetState<BuerliState>,
  get: GetState<BuerliState>,
  api: any,
) => config(args => unstable_batchedUpdates(() => Renderer.batchedUpdates(() => set(args))), get, api)

if there is any way to avoid this we would be very happy ...

btw react automatically batches in blocking and concurrent mode, has anyone tested if the problem persists?

@JeremyRH
Copy link
Contributor

To fix this in non-concurrent mode without batchedupdates, you need to ensure a parent's setState is called before its children's setState. There are many ways to do that. I did it by keeping track of render order (mutating an array during render) and it didn't work in concurrent mode.
Someone should be able to figure out a solution but it might involve doing different things in concurrent mode.

@dai-shi
Copy link
Member

dai-shi commented Feb 11, 2021

if there is any way to avoid this we would be very happy ...

well, this looks to me the easiest and the cleanest.

I'm interested if #160 fixes this issue from the ground. Otherwise, the current solution would be good in total.

I did it by keeping track of render order (mutating an array during render)

My assumption is this doesn't work very well performance-wise even in legacy mode.

@drcmda
Copy link
Member

drcmda commented Feb 11, 2021

just switching to blocking would also take care of it right? that would be a good solution if it did.

@dai-shi
Copy link
Member

dai-shi commented Feb 11, 2021

AFAIU, you would need batchedUpdates in non-react callbacks, if you needed to ensure top-down updates, even in legacy mode.
I expect the future version of react may fix this and that's why it's unstable_.

@drcmda
Copy link
Member

drcmda commented Feb 12, 2021

it says here that both blocking and cm autobatch https://reactjs.org/docs/concurrent-mode-adoption.html#feature-comparison

*: Legacy mode has automatic batching in React-managed events but it’s limited to one browser task. Non-React events must opt-in using unstable_batchedUpdates. In Blocking Mode and Concurrent Mode, all setStates are batched by default.

@dai-shi
Copy link
Member

dai-shi commented Feb 12, 2021

Oh, you mean Blocking Mode which is the one between CM and LM. I missed that part of your comment.

In Blocking Mode and Concurrent Mode, all setStates are batched by default.

This, I'm not sure. Hopefully, they batch multiple setStates called in async but in a short period?

@JeremyRH
Copy link
Contributor

JeremyRH commented Feb 14, 2021

btw react automatically batches in blocking and concurrent mode, has anyone tested if the problem persists?

Last I tested, using ReactDom.unstable_batchedUpdates prevented the zombie child issue. This should mean the problem wont persist in blocking or concurrent mode.

@ivancuric
Copy link

ivancuric commented Mar 17, 2021

Should this issue be reopened?
Perhaps added as a middleware to modify set?

I've been trying to figure out where the unstable_batchedUpdates api is actually required. As far as I've understood, it's only required when using setState. Not sure why it's not on by default.

More reading:
facebook/react#18402

@dai-shi
Copy link
Member

dai-shi commented Mar 17, 2021

I feel like this is not a real issue from the library perspective. React may behave differently in the future as we discussed.
The case we need unstable_batchedUpdates is rare as I understand, and adding it to setState by default doesn't sound correct to me, but not very sure. AFAIR, react-redux had it by default, but it's now opt-in.

A new middleware to modify set might be an option if there's a certain demand.

btw, react doesn't seem to have Blocking Mode any longer.

@meglio
Copy link

meglio commented Jul 8, 2021

Could someone help me understand the following. In the readme, there is this example:

image

What I can't understand is why unstable_batchedUpdates() is used in location A and not in location B?

Consider the following example:

image

Here, the callback is a function that the api wrapper will call when an ajax request is complete. Yet it is declared in location B, not somewhere outside.

In my particular example, what part exactly should I wrap with unstable_batchedUpdates()? -- many thanks

@dai-shi
Copy link
Member

dai-shi commented Jul 8, 2021

The readme doesn't recommend having unstable_batchedUpdates() in "B", because increaseFishes can be called from react callbacks. You should use it where you are sure that it's non-react callback.

In your case, if onSuccess is non-react callback and if you are facing the zombine problem, you can surely put it there.

@meglio
Copy link

meglio commented Jul 8, 2021

Thanks. How do I distinguish a non-react callback from a react-callback? Not sure I have a clear understanding of this term's meaning.

@dai-shi
Copy link
Member

dai-shi commented Jul 8, 2021

react callbacks are like <button onClick={...}>. non-react callback is like setTimeout(...).
react 18 will auto batch for non-react callbacks. actually, react 18 alpha discussion would explain the difference better. reactwg/react-18#21

@meglio
Copy link

meglio commented Jul 8, 2021

Almost clear! The last bit that's confusing is this:
Would a Promise's than() callback be still classified a non-react callback even if the Promise is created inside the onClick() react callback?

I suppose so based on their example, could you confirm please?

function handleClick() {
    fetchSomething().then(() => {
      // React 17 and earlier does NOT batch these because
      // they run *after* the event in a callback, not *during* it
      setCount(c => c + 1); // Causes a re-render
      setFlag(f => !f); // Causes a re-render
    });
  }

... and why I keep asking for clarifications here is that I eventually would like to understand what positions exactly it is safe to call the bath wrapper function considering the is zustand intermediary. In other words, does the fact that there is zustand in between adds any limitations as to when I can / cannot call unstable_batchedUpdates()?

@dai-shi
Copy link
Member

dai-shi commented Jul 8, 2021

Would a Promise's then() callback be still classified a non-react callback even if the Promise is created inside the onClick() react callback?

yes.

@fongfai
Copy link

fongfai commented Jan 2, 2023

Yup! It seemed related to that and as you pointed out in your codebox, using unstable_batchedUpdates, will do the trick.

But I still have one question though.

What would be the downsides of moving the subscription from the useEffect to directly in the hook call? (Ex.: index.tsx) This way, the listeners will be added in the same order as the component tree render order, therefore, avoiding having the child listener called before the parent. In this case, it should dramatically reduce, if not eliminate, the problem with the zombie child, as the parent will be rendered before the child.

your link address "index.tsx" has been 404,

keyserj added a commit to amelioro/ameliorate that referenced this issue Jan 31, 2023
see pmndrs/zustand#302
unfortunately, batching doesn't fix this because the error isn't when rendering,
it's when checking the store's comparers
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants