skip to Main Content

I’ve read a gazillion articles about renders and composition in React, sometimes with contradicting information. At some point I thought I got it, when I read that if React "still has the same children prop it got from the App last time, React doesn’t visit that subtree."

In the code below I am triggering a re-render of App every 100ms by setting state, and React outputs "rerendered" in the console every 100ms.

Why does Child get re-rendered at each commit? Am I not "lifting content up" here?

import { useState, useEffect } from 'react';

const App = () => {
  const [now, setNow] = useState()

  // Start a timer
  useEffect(() => {
    const interval = setInterval(() =>
      setNow(Date.now()), 100)
    return () => clearInterval(interval)
  }, [])

  return (
    <div className="App">
      <Parent>
        <Child />
      </Parent>
    </div>
  )
}
export default App

const Parent = ({ children }) => {
  return (
    <div id='parent'>
      {children}
    </div>
  )
}

const Child = () => {
  console.log('rendered')
  return (
    <p>whatever</p>
  )
}
const { useEffect, useState } = React;

const Child = () => {
  console.log('rendered')
  return (
    <p>whatever</p>
  )
}

const Parent = ({ children }) => {
  return (
    <div id='parent'>
      {children}
    </div>
  )
}

const App = () => {
  const [now, setNow] = useState()

  // Start a timer
  useEffect(() => {
    const interval = setInterval(() =>
      setNow(Date.now()), 100)
    return () => clearInterval(interval)
  }, [])

  return (
    <div className="App">
      <Parent>
        <Child />
      </Parent>
    </div>
  )
}

ReactDOM
  .createRoot(document.getElementById("root"))
  .render(<App />);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"></script>

2

Answers


  1. You can memoize the ParentChild portion inside of App. Since the state of App is updated every 100ms, it will re-render every 100ms top-down. If you have elements that do not need the now value as a prop, you will need to use memoization.

    Note: I also moved your timer logic into a hook.

    const { useEffect, useMemo, useState } = React;
    
    const Child = () => {
      console.log('rendered') // only logged one time
      return (
        <p>whatever</p>
      )
    }
    
    const Parent = ({ children }) => {
      return (
        <div>
          {children}
        </div>
      )
    }
    
    const useTime = (updateRateMs) => {
      const [intervalId, setIntervalId] = useState()
      const [now, setNow] = useState(Date.now())
      
      useEffect(() => {
        setIntervalId(setInterval(() => setNow(Date.now()), updateRateMs))
      }, [])
      
      useEffect(() => {
        return () => {
          clearTimeout(intervalId)
        }
      }, [intervalId])
      
      return { now }
    }
    
    const App = () => {
      const { now } = useTime(100); // Update now every 100ms
    
      const memoizedChild = useMemo(() => (
        <Parent>
          <Child />
        </Parent>
      ), [])
    
      return (
        <div className="App">
          {memoizedChild}
          <div>Current Time: {new Date(now).toLocaleString()}</div>
        </div>
      )
    }
    
    ReactDOM
      .createRoot(document.getElementById("root"))
      .render(<App />)
    <div id="root"></div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.development.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"></script>
    Login or Signup to reply.
  2. This is happening because you are creating a new <Child /> component everytime the state in the <App/> component is updated. So the statement that "if React still has the same children prop it got from the App last time, React doesn’t visit that subtree" is no longer true. If you move your timer from <App/> to <Parent/> component you can see in the snippet below that the <Child/> will no longer rerender because it is only created once in the <App/> and passed down as children prop.

    const { useEffect, useState } = React;
    
    const Child = () => {
      console.log('rendered')
      return (
        <p>whatever</p>
      )
    }
    
    const Parent = ({ children }) => {
      const [now, setNow] = useState()
    
      // Start a timer
      useEffect(() => {
        const interval = setInterval(() =>
          setNow(Date.now()), 100)
        return () => clearInterval(interval)
      }, [])
    
      return (
        <div id='parent'>
          {children}
        </div>
      )
    }
    
    const App = () => {
      return (
        <div className="App">
          <Parent>
            <Child />
          </Parent>
        </div>
      )
    }
    
    ReactDOM
      .createRoot(document.getElementById("root"))
      .render(<App />);
    <div id="root"></div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.development.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"></script>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search