Question
From my understanding, Lazy State Instantiation and the useEffect dependency array both keep React from doing work again that it shouldn’t. What’s the difference between them?
Answer
Imagine React is a restaurant. A component is a table at the restaurant and state is the food that’s been ordered by that table. Rendering the component is updating the table to have the food they ordered. Anytime their order changes, React brings food to the table (re-renders the component) and updates it based on their order. If the order (state) gets set to add an order of onion rings, React re-renders the table (component) to have onion rings.
Lazy State Instantiation
Lazy State initialization is about optimizing how you handle what the order is when the people at the table FIRST show up.
They get to the restaurant and they sit down at the table and they haven’t even talked to a waiter yet. What is their order? In most restaurants it’s nothing. They’ll need to order (set the state) later and put something there.
Bringing this back to code, that’s like starting the state as 0
or an empty string ""
or an empty object {}
. It might look something like this:
const [total, setTotal] = useState(0)
const [name, setName] = useState("")
const [user, setUser] = useState({})
In this scenario, we don’t need Lazy State Initialization. None of those things take very long for the computer to create, so no optimization is needed.
But what if the restaurant brings out a complimentary basket of breadsticks to every table before they order? The initial order (initial state) is something more complex.
Bringing that back to code, maybe we want to initialize a piece of state to a random shuffling of an array of cards. Say we have a getShuffledCards()
function that creates a new array of a specified number of cards and shuffles it. That function could take some processing time, especially if we ask for a lot of cards or we’re doing a complex shuffle.
const [cardDeck, setCardDeck] = useState(
getShuffledCards(52)
)
Well, with how functional components work, this line of code will run that getShuffledCards
function every time the component rerenders. It will only actually need that shuffled deck of cards the first time, after that it will completely ignore the initial value. But it will still do all that work every single time.
So coming back to the restaurant metaphor — if rerendering the component in our metaphor is updating the table with new food — then this restaurant, rather than just making the breadsticks when a new table of people shows up to the restaurant, is making a new basket of breadsticks every time the waiter goes to the table for any reason. And every time the waiter’s like “Oh, I don’t need that” and more breadsticks go in the trash. The waiter goes to refill the table’s water glasses and more breadsticks go in the trash. The waiter picks up some dirty dishes and more breadsticks go in the trash.
It’s an appalling waste of breadsticks.
Here’s where Lazy Instantiation comes in! If we make one little change to our code no breadsticks will get thrown away. Or, bringing it back to the code, the shuffled deck of cards will only get created once. It could look something like this:
const [cardDeck, setCardDeck] = useState(
() => getShuffledCards(52)
)
By wrapping the time-intensive work in a function passed to useState
, we ensure that the getShuffledCards()
function only gets called the first time the component renders, when useState
needs the initial value.
That is Lazy Instantiation of State. Or, as I like to call it, Lazy Making of Complementary Breadsticks.
UseEffect Dependency Array
Now let’s talk the useEffect dependency array.
Waiters in a restaurant are very busy. The restaurant wants to keep them running nicely and make sure nothing slows them down. Bringing it back to code, waiters are like component render functions. In class components, it’s the render()
method. In functional components, it’s the entire component function. They handle updating the table based on the order (re-rendering the component based on the state).
What we don’t want to do is slow down our component render functions. That will make the page sluggish to update, and users don’t like that. So if there’s something that needs to be done that isn’t directly related to updating tables based on orders (rendering components based on state), then we want to have someone who isn’t a waiter deal with that (or have it run outside the render function).
Let’s call things that aren’t related to updating tables — or rendering components — Side Effects. Anytime we have one of those, we want to put it in a function and pass it to the useEffect
hook. This is like another restaurant worker, maybe a dishwasher. He will do his work right after the waiter has finished visiting the table (rendering the component).
useEffect(() => {
wash(dirtyDishes)
})
But, just like the breadstick maker, the dishwasher can be a bit overeager. He will try to wash dishes every time the waiter gets back from updating the table, even if the waiter doesn’t have any dirty dishes for him. Which is a waste of water and bad for the environment.
If we don’t want him to do that, we can give him a dependency array. This tells him to only do his thing (washing dishes) if the dirtyDishes
array has changed in some way (maybe because there are new dishes to wash).
useEffect(() => {
wash(dirtyDishes)
}, [dirtyDishes])
Now he will only try to wash dishes if he sees a change in the dirty dishes array. No water wasted.
That is the useEffect
dependency array.
Conclusion
So, as you noticed, they have a similar role. They keep React from doing unnecessary work, but in two different places.