3

I am using an API to retrieve latest posts from Firestore. The API is something is like this

function loadFeedPosts(
  createdAtMax,
  limit
) {
  return db
    .collection("posts")
    .orderBy("createdAt", "desc")
    .where("createdAt", "<", createdAtMax)
    .limit(limit)
    .get()
    .then(getDocsFromSnapshot)
}

createdAtMax is the time we need to specify in order to retrieve the posts.

And I have a Feed component. It looks like this.



function Feed() {
  const [posts, setPosts] = useState([])
  const [currentTime, setCurrentTime] = useState(Date.now())
  const [limit, setLimit] = useState(3)

  useEffect(() => {
    loadFeedPosts(currentTime, limit).then(posts => {
      setPosts(posts)
    })
  }, [currentTime, limit])

  return (
    <div className="Feed">
      <div className="Feed_button_wrapper">
        <button className="Feed_new_posts_button icon_button">
          View 3 New Posts
        </button>
      </div>

      {posts.map(post => (
        <FeedPost post={post} />
      ))}

      <div className="Feed_button_wrapper">
        <button
          className="Feed_new_posts_button icon_button"
          onClick={() => {
            setLimit(limit + 3)
          }}
        >
          View More
        </button>
      </div>
    </div>
  )
}

So when people click on the View More button, it will go retrieve 3 more posts.

My question is, since clicking on the View More button triggers a re-render, I assume the line const [currentTime, setCurrentTime] = useState(Date.now()) will get run again, does the Date.now() which serves as the initial value for currentTime get evaluated again for every re-render?

I tested it myself by logging out currentTime, and it seemed not updating for every render. According to the React doc https://reactjs.org/docs/hooks-reference.html#lazy-initial-state, I thought only when using Lazy initial state, the initialState is only calculated once. But here I am not using Lazy initial state, how come it is not calculated every time the component re-renders?

Although the intended behavior of the initial state of currentTime is exactly stays the same for every re-render, I am just baffled why it didn't get re-calculated for every re-render since I am not using Lazy initial state

Joji
  • 4,703
  • 7
  • 41
  • 86
  • 1
    the argument in `useState()` defines the `initialValue` which gets only set on the first render – sebastian-ruehmann Jun 18 '20 at 21:02
  • @sebastian-ruehmann then why do we have Lazy initial state? Isn't that redundent? – Joji Jun 18 '20 at 21:23
  • *If the initial state is the result of an expensive computation, you may provide a function instead* https://reactjs.org/docs/hooks-reference.html#lazy-initial-state – sebastian-ruehmann Jun 18 '20 at 21:29
  • I think you emphasized the wrong thing in the explanation of `React's useState Lazy initial State` Docs. The reason for having the `Lazy initial state` is not that by default the initialValue is recomputed on render. Rather is it made for having an exclusive and only initially called "State set up function" which is memoized until the component unmounts – sebastian-ruehmann Jun 18 '20 at 21:38
  • @sebastian-ruehmann Hi still trying to understand the difference here. so let's say if we have a simple `useState` hook like this `const [myState, setMyState] = useState(fn())` and a Lazy initial state `const [myState, setMyState] = useState( () => fn() )` . will both `fn()` be run for only the first initial render or the first hook will run `fn()` for every re-render? – Joji Jun 19 '20 at 03:03
  • `const [myState, setMyState] = useState(fn())` will disregarded being called on rerender `const [myState, setMyState] = useState( () => fn())` on initial render only. Good explanation: https://stackoverflow.com/a/58539958/13745258 – sebastian-ruehmann Jun 19 '20 at 08:11

2 Answers2

2

Using useState you are providing only the initial value, as the doc about the useState hook says:

What do we pass to useState as an argument? The only argument to the useState() Hook is the initial state.

The reason why we have lazy initialization is because in subsequent renders the initialValue will be disregarded, and if it is the result of an expensive computation you may want to avoid to re-execute it.

const [value, setValue] = useState(expensiveComputation());
// expensiveComputation will be executed at each render and the result disregarded

const [value, setValue] = useState(() => expensiveComputation());
// expensiveComputation will be executed only at the first render
Devid Farinelli
  • 7,514
  • 9
  • 42
  • 73
  • Sorry I think there is a misunderstanding. Maybe I didn't express myself clearly. Not updating the current time every time the component re-renders is the intended behavior I wanted. I was just confused with the usage of ` Lazy initial state`. I thought only when using Lazy initial state we get the initialState calculated once, otherwise it is re-calculated every time the component rerenders – Joji Jun 18 '20 at 21:26
  • Ah ok, nice to know. I know understand you confusion, I've updated my answer, hope it helps ;) – Devid Farinelli Jun 18 '20 at 21:31
  • Also, if `currentTime` isn't meant to be used to re-render the component, consider using a mutable variable with `useRef` – Devid Farinelli Jun 18 '20 at 21:35
  • Hi thanks for the reply. This is exactly what I thought. So as you said, for `const [currentTime, setCurrentTime] = useState(Date.now())`, `Date.now()` will be executed at each render, but it doesn't affect the `value` even thought it is the initial value? That's why after every re-render when we print out `currentTime` we will see the same value? – Joji Jun 19 '20 at 03:10
  • Yeah exactly, the state should persist between renders so the Date is computed but the value is ignored – Devid Farinelli Jun 19 '20 at 07:16
2

useState() initializes the state with mount, it runs just before first render.

From the doc :

During the initial render, the returned state (state) is the same as the value passed as the first argument (initialState).

Why do we need lazy initial state?

Lets say we derive our initial state after some high complexity function. Lets try without lazy state approach :

const A = () => {
  const [a, setA] = useState("blabla")
  const processedA = processA(a) // Wait! this will run on every render!, 
  // so i need to find a way to prevent to be processed after first render.

  return ...
}

To implement function that only run for once, i should keep track of renders with useRef :

const A = () => {
  const [a, setA] = useState("blabla")
  let processedA

  const willMount = useRef(true);
  if (willMount.current) {
    processedA = processA(a)
  }

  useEffect(() => {
    willMount.current = false;
  }), []);

 return ...
}

What an ugly approach, isnt it?

Another approach can be using useEffect, but this time it will work on second render after mount. So i will have unnecessary first render :

const A = () => {
  const [a, setA] = useState("blabla")

  useEffect(()=>{
    const processedA = processA(a)
    setA(processedA)
  }, [a])

  return ...
}

Kerem atam
  • 2,387
  • 22
  • 34
  • Hi thanks for the reply. I don't understand, since the initialState is only calculated once during the initial render, then why do we need Lazy initial state? – Joji Jun 18 '20 at 21:22
  • @Joji i tried to explain why we need lazy initial state – Kerem atam Jun 18 '20 at 21:44
  • 1
    Hi @Kerematam :) I've found this blogpost really interesting https://overreacted.io/a-complete-guide-to-useeffect/. It is advised to never lie about the `useEffect` dependencies and to think in "hooks" instead of "lifecycle" – Devid Farinelli Jun 18 '20 at 21:52
  • 1
    @DevidFarinelli Thanks for the article! and yeah, i have tendency to code hooks in life cycle perspective:D – Kerem atam Jun 18 '20 at 22:03