Questions about initializing `useState` with `Date.now()`

Viewed 4141

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

2 Answers

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

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 ...
}

Related