简体   繁体   中英

React component render twice using useState

I'm having a really hard time to figure out what's happening when there is nothing being used to trigger re-render the component.

Events.js Component renders twice when I remove the useState() from the Event.js it renders once, but I need to keep it. when I use useEffect() inside Event components, renders fourth time.

I just kept the dummy data to give you to fill the emptiness and tried to remove React.memo , nothing happens. the problem is with the Event.js component I believe. I'm also using the Context API, but forth time rendering is too much.

useEffect inside App.js is getting some value from the localStorage, I can't access that direct 'cause the value is undefined by default

sandbox code here: https://codesandbox.io/s/event-manager-reactjs-nbz8z?file=/src/Pages/Events/Events.js The Events.js file is located on /Pages/Events/Events.js

example code is below

Event.js ( child component )

function Events() {

    // Sate Managing
    const [allEvents, setAllEvents]   = React.useState(null);
    console.log('Rendering EventsJs', allEvents);

    React.useEffect(() => {
         setAllEvents(['apple', 'banana']);
    }, []);



    return (
        <div className="events">

                { console.log('Event Rendered.js =>') }

        </div>
    )
}


export default React.memo(Events, (prevProps, nextProps) => {
        return true;
} );

App.js ( parent component )

import { BrowserRouter, Route, Redirect } from 'react-router-dom';

function App() {

  const [userId, setUserId] = React.useState(null);

  React.useEffect(() => {
    setUserId(1);
  }, []);


  // Login

  return (
    <BrowserRouter>

      <Navigation />

      <Route  path='/events' component={Events} />

      {console.log('App Rendered')}

    </BrowserRouter>
  );
}

export default App;

Error:

在此处输入图像描述

Your app is working fine. It is rendering as it should. As we know:

A React component re-renders whenever its props or state change.

And react component lifecycle order is:

  1. Initial props/state --> render --> DOM update --> mounted
  2. props/state changed --> render --> DOM update --> updated... so on

In the example below, it is rendering 2 times and that's correct:

  • First one (first console.log) is due to initial render with state as []
  • Second one (second console.log) is due to state change (caused by useEffect) to ['apple', 'banana']
function Events() {
  const [allEvents, setAllEvents] = React.useState([]);
  console.log('Event Rendered', allEvents);

  useEffect(() => {
    setAllEvents(['apple', 'banana']);
  }, []);

  return <>Events</>;
}

About using React.memo:

React.memo only checks for props changes. If your function component wrapped in React.memo has a useState or useContext Hook in its implementation, it will still rerender when state or context change .

You can not skip re-render using React.memo due to change in state . You can only optimize to skip re-rendering caused by change in props .

But in the example above, you don't have props passed from the parent component, the only props passed to Events are those passed by react-router ie route props. So, there is no need to use React.memo.

Here is sandbox , check the console.logs. You will see only 3 logs: "App render", "Event render with initial state", "Event render with new state".


EDIT:

If we remove StrictMode from index.html , and add below console.logs in components:

App.js --> console.log('App rendered')
Evenets.js --> console.log('Event rendered', allEvents, isLoading) // (allEvents and isLoading are state variables here)

And go to http://localhost:3000, we see 1 log:

App Rendered

Now click on "Events", we see 3 logs:

1: Event Rendered, [], true
2: Event Rendered, [{}, ... 54 items], true
3: Event Rendered, [{}, ... 54 items], false

which is correct behavior (refer lifecycles order written above):

  • 1st log: render with initial state ( [] , true )
  • 2nd log: render with new allEvents ( 54 items ) and old isLoading ( true )
  • 3rd log: render with old allEvents ( 54 items ) and new isLoading ( false )

Below are the right questions to ask now:

Question1:

Why 2nd and 3rd render (log) are separate, should not they be batched (merged) and applied together as they are written in the same function?

fetch('url').then(() => {
  // ... code here
  setAllEvents([...events])
  setLoading(false)
})

Answer:

No, they will not be batched in above code . As explained by Dan Abramov :

This is implementation detail and may change in future versions.

In current release, they will be batched together if you are inside a React event handler. React batches all setStates done during a React event handler, and applies them just before exiting its own browser event handler.

With current version, several setStates outside of event handlers (eg in network responses) will not be batched. So you would get two re-renders in that case.

There exists a temporary API to force batching . If you write ReactDOM.unstable_batchedUpdates(() => { this.fn1(); }); then both calls will be batched. But we expect to remove this API in the future and instead batch everything by default.

So, you can write (inside fetch's then), if you want, it will save 1 render :

ReactDOM.unstable_batchedUpdates(() => {
  setAllEvents([...events])
  setLoading(false)
})

Question2:

What's React event handler in above quote?

Answer: foo in example below. These 2 set states will be batched.

const foo = () => {
  setAllEvents([
    { _id: '5ede5af03915bc469a9d598e', title: 'jfklsd', },
  ])
  setLoading(false) 
}

<button onClick={foo}>CLICK</button>

Question3:

Does it update HTML DOM as many times as it renders (prints console.log)?

Answer: No . React compares calculated virtual DOMs before updating real DOM, so only those changes are applied to real DOM which are required to update the UI.

Question4:

Why was rendering doubled when we use StrictMode ?

Answer: Yes, StrictMode will intentionally double invoke "render" and some other lifecycle methods to detect side-effects . Strict mode checks are run in development mode only; they do not impact the production build.

Well actually this is caused by your usage of React.memo , its second parameter is called areEqual , and you pass in () => false , so you are basically telling React that the props are always changing. Therefore whenever App rerenders, Events rerenders too.

You should let React.memo check for prop changes. By passing () => false you are actually telling that its props always change (they are never equal).

export default React.memo(Events);

Here's a working example .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM