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

React.StrictMode combined with useState causes component to render twice #15074

Closed
TrueWill opened this issue Mar 9, 2019 · 22 comments
Closed

Comments

@TrueWill
Copy link

TrueWill commented Mar 9, 2019

Do you want to request a feature or report a bug?
Bug (maybe)

What is the current behavior?
If wrapped in React.StrictMode and a function component contains a call to useState, the function (render) is called twice - even if the state setter is never called.

If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem.
https://codesandbox.io/s/lyw9514j4q
(please see console)

What is the expected behavior?
I would expect it to only render once.

Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?
16.8.3 in Chrome on macOS Mojave; requires hooks so not tested with previous versions.

This may be related to #12961 - but note that nowhere am I setting state (only initializing it).

If this is expected behavior please feel free to close. Thank you!

@gaearon
Copy link
Collaborator

gaearon commented Mar 9, 2019

It's an intentional feature of the StrictMode. This only happens in development, and helps find accidental side effects put into the render phase. We only do this for components with Hooks because those are more likely to accidentally have side effects in the wrong place.

@sdennett55
Copy link

sdennett55 commented Apr 10, 2020

Sorry, I know this was closed, but you say

We only do this for components with Hooks...

but if your component uses a class -- even if it's stateless -- it also renders twice. Threw me for a loop today trying to figure out why my component wasn't working properly.

export default class App extends React.Component {
  render() {
    console.log('this logs twice with React.StrictMode');
    return null;
  }
}

@gaearon
Copy link
Collaborator

gaearon commented Apr 10, 2020

Strict Mode always did this for classes. That’s the first thing it ever did. :-)

The question was about function components. So I explained that for function components, this was only on when they contained Hooks. In any case, we’ve changed that too already so it will fire for all function components soon.

@IPetrik
Copy link

IPetrik commented Apr 30, 2020

If I'm understanding the documentation correctly, the purpose of this is to bring the developer's attention to lifestate function that are non-idempotent, because these might cause problems when concurrent mode is rolled out. So, I'm assuming just ignoring double renders that cause double sideeffects in StrictMode is not a good idea for future proofing.

What then, is best practice for functions that get passed to useReducer? Currently, I have a state that is an array, and the actions passed to the reducer are objects to append to the state. The reducer appends and returns the new state. The objects are singletons and may get sent to the dispatch function more than once, so just checking whether that object has been appended already doesn't work. What is the best practice design pattern for an idempotent reducer that appends to the state?

Nevermind, I had a brain fart. It's a non-issue; both times the function is called in this double render, it is called with the same initial state, so appending again and returning will return the same result.

@nermand
Copy link

nermand commented May 8, 2020

I know this one is closed, but I don't want to open a new ticket just for this.
What am I doing wrong here:
https://codesandbox.io/s/double-event-cb6hg

The first click will be triggered once, but subsequent clicks will add duplicate items.
My real app renders twice and dispatch is triggered twice on every click, this is just a contrived example.

@jaredkhan
Copy link

Hey @nermand

(I think) the trouble you're seeing is because state.items.push in your reducer modifies the array in place. This means you're trying to modify a piece of React state outside of the setter for that state.

The use of .push can be replaced with a use of .concat. Sandbox with the fix

Notice that reducer(state, event) still gets called twice for each click (with the same objects for state and event in each call), but it is no longer an issue since state is no longer changed between those two calls. As mentioned above, this is a deliberate behaviour of strict mode to help us notice issues like this.

@nermand
Copy link

nermand commented May 9, 2020

Cheers @jaredkhan, that explains it.
We just need to keep treating every piece of state immutable and everything should be fine.
Thanks!

@DaniGuardiola
Copy link

DaniGuardiola commented May 22, 2020

Hi, I understand the motivation for this. However, I really don't know how to approach a problem I have with debouncing an event handler. Here's a simplified version of my current code:

function ExampleComponent () {
  const [debouncedHandlerFn] = useState(() => debounce(handlerFn, 1000))
  return <div onSomeEvent={debouncedHandlerFn} />
}

What I'm trying to achieve here is to handle an event in a debounced manner. The debounce function creates the debounced version of the handler.

I'm using lazy state initialization because it is the only way to put a function in the component state (it is also more efficient as it is not creating debounced versions of the function on every render).

What I expect is to have a unique debounced function instance per mounted component. This does work in production, but strict mode is rendering my component twice and executing the lazy-loading function twice. Actually I haven't checked if it still works fine (I guess it should, as it would retain the last instance, I'm gonna check now), but it doesn't seem like a clean way to go about this.

Any thoughts? Thanks :)

@DaniGuardiola
Copy link

Nevermind, I got it. Figured out this is not a problem as long as the debounced function is being called from an event handler, as it will be called at later later after the second render is done, so no side-effects here.

@nanyang24
Copy link

🙆‍♂️ Dan's answer explained my confusion
But I still think it may be an added burden on the mind

@tranvansang
Copy link

tranvansang commented Aug 18, 2020

In my project, I integrate SSR in which the server delivers data to client to be used in the first render.

To save memory, I want to clear the data after the first render (and also prevent the data from being re-used). Because StrictMode makes the render happen twice, which always forces the data refetched (in the second render).

I have 2 questions. I really appreciate if anyone can help me figure it out

  1. Yes. My render function has side effect. However, it only happens in the first render. If this is the case, will it break the concurrent mode in the future?

  2. Is there any (best effort) way to satisfy these 3 purposes, priority from high to low:

  • Make sure concurrent mode work (I want something that works in future)
  • Clear data after the first render
  • Make it work in StrictMode

Thank you very much.

@gaearon
Copy link
Collaborator

gaearon commented Aug 19, 2020

Can you clean it in the effect?

@tranvansang
Copy link

tranvansang commented Aug 19, 2020

Can you clean it in the effect?

Awesome. Thank you for opening my mind. I did not think about it.

There is still one more point that I need to control the orders of data fetching, which I am solving now (and I think it can be handled). (I have changed to make all API endpoints with the same parameter return the same data. There is no more order check)

Thank you very much.

Edit: I have made it. No more warning, re-fetching.

@xgqfrms

This comment has been minimized.

@mallchel
Copy link

Ohh. Why didn't you mark this in the documentation for StrictMode?

@sank2000
Copy link

sank2000 commented Jan 1, 2022

demo link

I am updating the parent state count inside the array from children but strict mode updates the state twice.

For example :
sample state

[   {
      active: true,
      count: 0
    },
    {
      active: false,
      count: 0
    },
    {
      active: false,
      count: 0
    }
]

I want to increase the count and invert the active state but the following code

 setTrades((old) => {
    old[id] = {
      count: old[id].count + 1,
      active: !old[id].active
    };
    return [...old];
  });

with strict mode causes the count to increase by 2
and boolean operation remains the same.

Any way to fix this issue?

@dmost714
Copy link

dmost714 commented Jan 3, 2022

demo link
Any way to fix this issue?

The old[id] assignment is modifying the array being passed in. Instead, modify a copy of that array:

  setTrades((old) => {
    const updated = [...old]
    updated[id] = {
      count: old[id].count + 1,
      active: !old[id].active
    }
    return updated
  })

@sank2000
Copy link

demo link
Any way to fix this issue?

The old[id] assignment is modifying the array being passed in. Instead, modify a copy of that array:

  setTrades((old) => {
    const updated = [...old]
    updated[id] = {
      count: old[id].count + 1,
      active: !old[id].active
    }
    return updated
  })

thanks, mate works fine 🚀

@tranvansang
Copy link

tranvansang commented Apr 9, 2022

#15074 (comment)

Can you clean it in the effect?

Now, with the new StrictMode's behavior in react 18,
the second render always renders with the cleaned state and it causes hydration failing.

My data fetching library works as follows:

  • place SSR data in store
  • when useData() hook is called, I increase the subscriptionCount for that entry by one.
  • when useData() hook is destroyed, I decrease the subscriptionCount for that entry by one and remove the entry from store if its subscriptionCount reaches 0.

The subscriptionCount pattern is useful in my app because it prevents duplicated requests to server, for example, for data like isLoggedIn, myProfile. It also removes the unsubscribed data from the store to keep the memory light.

However, in react 18, StrictMode does a full render (render + destroy) before re-render (previously, the first render doesn't call destroy function), all entries placed by SSR data in the store are cleared (because the subscriptionCount is decreased to 0). This causes the second renders failing to match the server and the first renders.

Currently, I have a workaround by increasing the subscriptionCount of all entries from SSR data by 1.
This has a very minor negative effect that these entries will never get removed from the store.
However, for now, this just works without any serious consequence.

I would like to raise my usecase here to ask if you have any advice on this. Or is subscriptionCount an anti-pattern?

@gaearon
Copy link
Collaborator

gaearon commented Apr 9, 2022

Can you post a minimal runnable code example?

@tranvansang
Copy link

tranvansang commented Apr 16, 2022

It is hard to mimic the hydration behavior with client-only code, I tried my best to reproduce the error here:

https://codesandbox.io/s/react-18-strict-mode-with-subscription-pattern-zfrfss?file=%2Fsrc%2FApp.js

Note about the demo: the error is actually different in the real SSR app (my app).

In my app (real SSR), error is "Warning: Did not expect server HTML to contain a <div> in <main>." error , while in the demo codesandbox, it is "This Suspense boundary received an update before it finished hydrating. This caused the boundary to switch to client rendering. The usual way to fix this is to wrap the original update in startTransition.".

However, the behaviors are the same.

Error in the real SSR app.
image

Error in client-only SSR-mimic demo.
image

Test 1

data is empty, the server sends HTML with empty data immediately and the client fetches the data itself.

const LazyChild = lazy(async () => {
  await sleep(1000)
  return {default: ConsumeData}
})
const App = () => <>
  <ConsumeData/>
  <Suspense>
    <LazyChild/>
  </Suspense>
</>

Step 1: App is rendered, LazyChild is suspended
Step 2: ConsumeData fetches data (in an useEffect)
Step 3: data is fetched, and populated to the store
Step 4: LazyChild is resolved and rendered. But now it reads the fetched data. While in server, because the lazy component is always available, lazy components are rendered immediately and read empty data.

Test 2:

data is fully resolved in server, and populated to the store in the client before the hydration starts. StrictMode causes hydration mismatch.

const App = () => <StrictMode>
  <ConsumeData/>
  <Suspense>
    <ConsumeData/>
  </Suspense>
</StrictMode>

Step 1: data is populated to the store
Step 2: the tree is hydrated
Step 3: first render happens. Interestingly, ConsumeData is suspended even though it is not a lazy component.
Step 4: first render is destroyed
Step 5: data is unsubscribed. The store sees that there is no more subscription, it clears data to save memory
Step 6: second render reads data from memory. However, the data is now empty, and the hydration fails to match in the inner (the suspended) ConsumeData

In test 2, if Suspense OR StrictMode is removed from the tree, no error will occur.

@gaearon
Copy link
Collaborator

gaearon commented Apr 16, 2022

@tranvansang

Please file a new one so we can track it? This issue is from 2019 and is about a different topic.

I haven't looked closely but I think you're probably misusing Suspense. Suspense is not meant to be managed by external store + effects. It's meant to be used with a separate cache that uses a different architecture. But regardless, a separate issue to discuss would be best.

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

No branches or pull requests