🏋🏽‍♂️ Hooks Workout #2. Advanced Hooks

🏋🏽‍♂️ Hooks Workout #2. Advanced Hooks

Hiya! I see you made it to Lesson 2!


This lesson is a part of the Hooks workout series:

  1. Your First React Hooks Application  
  2. Advanced Hooks 👈🏽 You're here  
  3. Fetching Data with Hooks
  4. Custom Reusable Hooks
  5. Moving into Production

In the first lesson, we talked a little about the basics of hooks and got a simple application running.

If you didn’t find the time to go through Lesson #, no worries at all. I totally get it. We’re all busy.

However, to get the most out of today’s lesson, you may need to check out the previous lesson.

Great!

Now, let’s get started with today’s lesson on more advanced hook concepts.

Using Object State

In the second example from yesterday, we introduced two state variables in the app.

const [count, setCount] = useState(0);
const [time, setTime] = useState(new Date());

This is the perfect way to use multiple state values especially if the values aren’t related e.g. count and time aren’t particularly related, so it’s a good idea to keep those in separate useState calls.

In the real world, you may not always have this luxury.

You may need to keep track of multiple related state values via an object.

So, the first task in this lesson will be to refactor the multiple useState calls into a single useState call. To do this you’ll use an object to keep track of the multiple state values.

Here’s the first step to take:

// before 
const [count, setCount] = useState(0);
const [time, setTime] = useState(new Date());

// now 
const [{count, time}, setState] = useState({
  count: 0,
  time: new Date()
})

Go ahead and write that in your application from yesterday, or start from here if you can’t find it.

What’s going on there?

Remember that useState is always invoked with some initial state value. In this case, we’ve passed the following object:

{
  count: 0,
  time: new Date()
}

A single object to keep track of both the count and time variables.

Also, remember from the previous lesson that the useState call returns two values in an array.

//invoke useState 
useState(initialValue);

//returns the following 
[value1, value2]

Where value1 represents the state value, and value2 the state updater function.

An important point to remember is that value1 will always initially be equal to the  initialValue passed to the useState call.

In this example where we passed an object, value1 will also be an object with count and time properties.

Using the object destructing syntax, we then go ahead to retrieve the state values as count and time.

Note the syntax below:

// look here 👇
const [{count, time}, setState] = useState({
  count: 0,
  time: new Date()
})
// same as this: 
const [state, setState] = useState({
 count: 0,
 time: new Date()
}
const count = state.count 
const time = state.time

With this change, the functionality of the app is now broken.

This is because the updater functions setCount and setTime have to be replaced with the new updater function setState— which expects a new value corresponding to the same structure as the initial state value i.e an object with count and time properties.

You’ll now fix that by doing the following:

// before 
const updateCountAndTime = () => {
      setCount(count + 1);
      setTime(new Date());
};

// now:
const updateCountAndTime = () => {
   // look 👇
   setState({
        count: count + 1,
        time: new Date()
    })
};

Go ahead and write that!

The new setState updater function is now invoked with the new state value — an object.

NB: Even In cases where you only want to update a single state property value, you must pass the entire new state object to the updater function when using objects.

For example, if you only wanted to update the count property value, don’t do this:

setState({
     count: count + 1
})

This will update the state value to the object you’ve passed in, and time will be undefined, as the new state value you’ve passed in replaces the former.

Instead do this:

setState({
       time: time, // the same value for "time"
       count: count + 1  // update "count"
    })

This way the new state update will still have a reference to the time property.

Got that?

I hope you implemented the exercise yourself. Here’s my own solution.

It's also possible to use the function syntax below:

setState(previousState => ({
       ...previousState, // pass the entire previous state
       count: previousState.count + 1  // only update "count"
})) 

Remember:

  • With state objects, an updater function call replaces the state value.
  • If you pass an object to an updater function, be sure to pass in the previous values in the object as well.
  • The function update syntax is typically preferred when your state update depends on a previous state value e.g. previousState.count  

useMemo - a more advanced hook

We’ve discussed 2 hooks so far: useState and useEffect. In this section, we will have a look at one of the more advanced hooks: useMemo.

I’m going to show you how useMemo works with an example app I’ve taken some time to setup. Here’s the app so you can code along.

This is what the app looks like:

Now, everything you see above is rendered by the App component.

The App component renders a child component called Profile — this renders the profile picture, 🙎‍♀️, and greeting, Hello, Anna.

For the profile component to work as expected, it requires a profileDetails props.

The return statement for the App component is shown below:

...
return (
    <div className="App">
      <Profile profileDetails={profileDetails} />
      <p>Anna is probably {age} years old</p>
      <buttton onClick={guessAge} className="guess">
        Guess her age
      </buttton>
    </div>

Do you see how Profile is rendered with a profileDetails props?

Also note that that the age of the user, in this case, Anna, changes when the “guess her age” button is clicked. You have the link to the app — be sure to click the button!

Here’s the full implementation of App :

function App() {
  // age state value 👇
  const [age, setAge] = useState(10);
  const guessAge = () => setAge(Math.round(Math.random() * 100));
  
  // the profile details required for <Profile />  👇
  const profileDetails = { name: "Anna", profilePic: "🙎‍" };

  return (
    <div className="App">
      <Profile profileDetails={profileDetails} />
      <p>Anna is probably {age} years old</p>
      <buttton onClick={guessAge} className="guess">
        Guess her age
      </buttton>
    </div>
  );
}

Did you get that?

The implementation of the Profile component is very simple. It destructures the name and profilePic from the profileDetails prop and renders the following:

const Profile = ({ profileDetails: { name, profilePic } }) => {
  return (
    <div>
      <p className="pic">{profilePic}</p>
      <p> Hello, {name} </p>
    </div>
  );
};

Having understood the structure of the app, where exactly does the useMemo hook fit in?

First, go to the app and add the following line to the Profile component:

const Profile = ({ profileDetails: { name, profilePic } }) => {
  // add this 👇
  console.log("Profile app was rendered");

  return (
    <div>
      <p className="pic">{profilePic}</p>
      <p>Hello, {name}</p>
    </div>
  );
};

If you did that, go ahead and click the button to update Anna’s age. Then look in the console.

Every time you click the button, there’s an update to the to the age state variable. With every update to the age state variable, the entire children of App is re-rendered, including the Profile component.

So you get the log for every click.

On a closer look, this is not ideal.

The Profile component has nothing to do with any new age update. Clicking the button only updates the age value — which is NOT rendered anywhere in the Profile component.

While you should not over optimise your application too early, this is usually called wasted render — a common source of performance leaks in React projects.

Let’s attempt to fix this wasted render by making the Profile component a true pure component — one that doesn’t re-render unless there’s a change to its props.

For that we’ll use the memo function.

Go to the first line of the app and import memo

// note the addition of "memo" 👇
import React, { useState, memo } from "react";

Then wrap the Profile component around a memo invocation, like this:

const Profile = memo(({ profileDetails: { name, profilePic } }) => {
  console.log("Profile app was rendered");
  return (
    <div>
      <p className="pic">{profilePic}</p>
      <p>Hello, {name}</p>
    </div>
  );
});

i.e memo(...everthingElseGoesHere)

Please, make sure you write that out by hand.

This will make the functional component, Profile act as a PureComponent.

NB: memo is different from the useMemo hook.

Now, go back and click the “guess her age” button. Is the Profile component still re-rendered?

Well, yes!

Do you know why?

This is because whenever the App component re-renders from an update to age, a new reference to the profileDetails object is created.

function App() {
  ...
  // look here 👇
  const profileDetails =  { name: "Anna", profilePic: "🙎‍" }

  ...
}

Hence, when the props passed to Profile is compared by the memo function, memo believes there’s been a change, so Profile is re-rendered.

A good way to remember this behaviour is to try this statement in your browser console.

{name: 'Anna'} === {name: 'Anna'} 
// false

That will return false! Even though the objects look identically the same, objects in Javascript are compared by “reference” unlike strings and numbers which are compared by “value”.

"Anna" === "Anna" 
// true 

So, how do we prevent profileDetails from having a new reference upon every re-render of App?

Well, a great solution is to memoize the object value profileDetails.

This is decent a use case for the useMemo hook. The useMemo hook returns a memoized value.

Here’s how we could use it in this example.

Go to your app, and write this:

// change profileDetails to this: 
const profileDetails = useMemo(
  () => ({ name: "Anna", profilePic: "🙎‍" })
);

useMemo takes a function argument that returns a value. The value returned from this function will be memoized and returned on every re-render.

Now, if you go ahead and click the age button, does Profile still re-render?

Yes, go and try it …

Sadly, the Profile component still re-renders.

Any idea what the problem may be this time?

Well, the answer is simple.

By default, the useMemo hook is invoked on every re-render! That doesn’t help our cause. Does it ?

Every time the age state is updated, useMemo recomputes the memoized value. This invalidates the "cache".

The solution is to someway prevent useMemo from recomputing the cached value — except it needs to.

For this, useMemo takes in a second argument typically called the array dependency e.g. useMemo(() => memoizedValue, [])

If you pass an empty array, then useMemo will compute the memoized value only once — when the component mounts!

If you pass a value to the array, then it’ll recompute the memoized value only when that array value changes e.g. useMemo(() => memoizedValue, [change].

In this case, useMemo will recompute the memoized value only if the change value changes across re-renders.

So, to solve our Profile re-render issue, here’s the final solution.

Go ahead and write this:

// change profileDetails 👇
const profileDetails = useMemo(
    () => ({ name: "Anna", profilePic: "🙎‍" }),
    []
  );

All we’ve done is add an empty array dependency. This way no recompilation occurs. The memoized value is created when the component mounts, and it doesn’t change!

Go ahead and click the change age button. You should get no logs in the console :)

Problem solved! Here’s my version of the completed app. Did you follow along?

Remember:

  • useMemo takes a function that returns a memoized value.
  • You can pass an array dependency to useMemo. This prevents recomputing the memoized value.

Conclusion

Hooks are basically just functions that let you do some interesting things within functional components. I hope you had as much fun as I did working with useMemo in this lesson.

Exercise: Your Turn

Change the implementation of the ProfileDetails component to accept two strings instead of an object.

// change to this, and make it work: 
<Profile name="Anna" profilePic="🙎‍♀️" />

Remove the use of the useMemo hook as well.

Once you complete the exercise, does the <Profile /> component re-render when the “guess her age” button is clicked ?

If no, why?

Can you answer these questions?

Say No to Javascript Fatigue

At Devcher, we're changing how you stay up to date with Javascript by building the principal destination for short-form developer mobile video. Try Devcher.

Join Devcher: The Anti Fatigue Community

Comments

Become a Devcher member below to join the conversation. As a member, you will also receive new posts by email (you can unsubscribe at any time).