Chapter 7. Introducing useEffect

We now have a good sense of what happens when we render a component. A component is simply a function that usually takes in state and/or props and always renders a user interface. Renders occur when the app first loads and when props and state values change. But what happens when we need to do something after a render? Let’s take a closer look.

Consider a simple component, the Checkbox. We’re using useState to set a checked value and a function to change the value of checked: setChecked. A user can check and uncheck the box, but how might we alert the user that the box has been checked. Let’s try this with an alert as it is a great way to block the thread:

const Checkbox = () => {
  const [checked, setChecked] = useState(false);

  alert(`checked: ${checked.toString()}`);

  return (
    <>
      <input
        type="checkbox"
        value={checked}
        onChange={() => setChecked(checked => !checked)}
      />
      {checked ? "checked" : "not checked"}
    </>
  );
};

We’ve added the alert before the render to block the render. The component will not render until the user clicks the OK button on the alert box. Because the alert is blocking, we don’t see the next state of the checkbox rendered until clicking OK.

That isn’t the goal, so maybe we should place the alert after the return?

export const Checkbox = () => {
  const [checked, setChecked] = useState(false);

  return (
    <>
      <input
        type="checkbox"
        value={checked}
        onChange={() => setChecked(checked => !checked)}
      />
      {checked ? "checked" : "not checked"}
    </>
  );

  alert(`checked: ${checked.toString()}`);
};

Scratch that. We can’t call alert after the render because the code will never be reached. To ensure that we see the alert as expected, we can use useEffect. Placing the alert inside of the useEffect function means that the function will be called after the render, as a side effect:

export const Checkbox = () => {
  const [checked, setChecked] = useState(false);

  useEffect(() => {
    alert(`checked: ${checked.toString()}`);
  });

  return (
    <>
      <input
        type="checkbox"
        value={checked}
        onChange={() => setChecked(checked => !checked)}
      />
      {checked ? "checked" : "not checked"}
    </>
  );
};

We use useEffect when a render needs to cause side effects. Think of a side effect as something that a function does that isn’t part of the return. The function is the Checkbox. The Checkbox function returns a fragment. But we might want the component to do more than that. Those things we want the component to do other than return UI are called effects.

An alert, a console.log, or an interaction with a browser or native API is not part of the render. It’s not part of the return. In a React app though, the render affects the results of one of these events. We can use useEffect to wait for the render, and then provide the values to an alert or a console.log:

useEffect(() => {
  console.log(checked ? "Yes, checked" : "No, not checked");
});

Similarly, we could check in with the value of checked on render and then set that to a value in localStorage:

useEffect(() => {
  localStorage.setItem("checkbox-value", checked);
});

We also might use useEffect to focus on a specific text input that has been added to the DOM. React will render the output, and then call useEffect to focus the element:

useEffect(() => {
  txtInputRef.current.focus();
});

On render, the txtInputRef will have a value. We can access that value in the effect to apply the focus. Every time we render, useEffect has access to the latest values from that render: props, state, refs, etc.

Cool, but… why? Think about a render. I render a checkbox where the checked value is false. On that render, React will look at the value of checked and call useEffect:

useEffect(() => {
  console.log(checked ? "Yes, checked" : "No, not checked");
});

React is calling this function post-render:

useEffect(() => console.log("No, not checked"));

Then we update the checked value to true. This causes another render. At this point, the render will lead to useEffect being called again but at this point the function is different:

useEffect(() => console.log("Yes, checked"));

Every time the component renders, we can see the value of checked in useEffect because useEffect is a unique function every time. Think of useEffect as being a function that happens after a render. When a render fires, we can take a look at that render’s values and use them in the effect. Then once we render again, the whole thing starts over. New values, then new renders, then new effects.

The Dependency Array

useEffect is designed to work in conjunction with other stateful Hooks like useState and the heretofore unmentioned useReducer which we promise to discuss later in the chapter. React will re-render the component tree when the state changes. As we’ve learned, useEffect will be called after these renders.

Consider the following, the App component has two separate state values:

import React, { useState, useEffect } from "react";
import "./App.css";

function App() {
  const [val, set] = useState("");
  const [phrase, setPhrase] = useState("example phrase");

  const createPhrase = () => {
    setPhrase(val);
    set("");
  };

  useEffect(() => {
    console.log(`typing "${val}"`);
  });

  useEffect(() => {
    console.log(`saved phrase: "${phrase}"`);
  });

  return (
    <>
      <label>Favorite phrase:</label>
      <input
        value={val}
        placeholder={phrase}
        onChange={e => set(e.target.value)}
      />
      <button onClick={createPhrase}>send</button>
    </>
  );
}

val is a state variable that represents the value of the input field. The val changes every time the value of the input field changes. It causes the component to render ever time the user types a new character. When the user clicks the send button, the val of the text area is saved as the phrase, and the val is reset to “”, which empties the text field.

This works as expected, but the component is rendered more times than it should be. After every render, both useEffect Hooks are called.

typing ""                             // First Render
saved phrase: "example phrase"        // First Render
typing "S"                            // Second Render
saved phrase: "example phrase"        // Second Render
typing "Sh"                           // Third Render
saved phrase: "example phrase"        // Third Render
typing "Shr"                          // Fourth Render
saved phrase: "example phrase"        // Fourth Render
typing "Shre"                         // Fifth Render
saved phrase: "example phrase"        // Fifth Render
typing "Shred"                        // Sixth Render
saved phrase: "example phrase"        // Sixth Render

We do not want every effect to be invoked on every render. We should just see what the user is typing, not the information about the saved phrase. To solve this problem, we can incorporate the dependency array. The dependency array can be used to control when an effect is invoked:

useEffect(() => {
  console.log(`typing "${val}"`);
}, [val]);

useEffect(() => {
  console.log(`saved phrase: "${phrase}"`);
}, [phrase]);

We’ve added the dependency array to both effects to control when they are invoked. The first effect is only invoked when the val value has changed. The second effect is only invoked when the phrase value has changed. Now when we run the app and take a look at the console, we’ll see more efficient updates occurring:

typing ""                              // First Render
saved phrase: "example phrase"         // First Render
typing "S"                             // Second Render
typing "Sh"                            // Third Render
typing "Shr"                           // Fourth Render
typing "Shre"                          // Fifth Render
typing "Shred"                         // Sixth Render
typing ""                              // Seventh Render
saved phrase: "Shred"                  // Seventh Render

Changing the val value by typing into the input only causes the first effect to fire. When we click the button, the phrase is saved and the val is reset to "".

It’s an array after all, so it’s possible to check multiple values in the dependency array. Let’s say we wanted to run a specific effect anytime either the val or phrase has changed:

useEffect(() => {
  console.log("either val or phrase has changed");
}, [val, phrase]);

If either of those values changes, the effect will be called again. It’s also possible to supply an empty array as the second argument to a useEffect function. An empty dependency array causes the effect to only be invoked once after the initial render:

useEffect(() => {
  console.log("only once after initial render");
}, []);

Since there are no dependencies in the array, the effect is invoked for the initial render. No dependencies means no changes, so the effect will never be invoked again. Effects that are only invoked on the first render are extremely useful for initialization.

useEffect(() => {
  welcomeChime.play();
}, []);

If you return a function from the effect, the function will be invoked when the component is removed from the tree:

useEffect(() => {
  welcomeChime.play();
  return () => goodbyeChime.play();
}, []);

This means that you can use useEffect for setup and teardown. The empty array means that the welcome chime will play once on first render. Then we’ll return a function as a cleanup function to play a goodbye chime.

This pattern is useful in many situations. Perhaps we’ll subscribe to a news feed on first render. Then we’ll unsubscribe from the news feed with the cleanup function. More specifically, we’ll start by creating a state value for posts and a function to change that value called setPosts. Then we’ll create a function addPosts that will take in the newest post and add it to the array. Then we can use useEffect to subscribe to the news feed, to play the chime. Plus we can return the cleanup functions: unsubscribing and playing the goodbye chime:

const [posts, setPosts] = useState([]);
const addPost = post => setPosts(allPosts => [post, ...allPosts]);

useEffect(() => {
  newsFeed.subscribe(addPost);
  welcomeChime.play();
  return () => {
    newsFeed.unsubscribe(addPost);
    goodbyeChime.play();
  };
}, []);

This is a lot going on in useEffect though. We might want to use a separate useEffect for the news feed events and another useEffect for the chime events:

useEffect(() => {
  newsFeed.subscribe(addPost);
  return () => newsFeed.unsubscribe(addPost);
}, []);

useEffect(() => {
  welcomeChime.play();
  return () => goodbyeChime.play();
}, []);

Splitting functionality into multiple useEffect calls is typically a good idea. But let’s enhance this even further. What we’re trying to create here is a component that subscribes to news feed event. Our custom hook called useJazzyNews listens to a news feed and collects new posts as they are added. It contains a useState hook, and two useEffect Hooks:

const useJazzyNews = () => {
  const [posts, setPosts] = useState([]);
  const addPost = post => setPosts(allPosts => [post, ...allPosts]);

  useEffect(() => {
    newsFeed.subscribe(addPost);
    return () => newsFeed.unsubscribe(addPost);
  }, []);

  useEffect(() => {
    welcomeChime.play();
    return () => goodbyeChime.play();
  }, []);

  return posts;
};

Our custom hook contains all of the functionality to handle a jazzy news feed, which means that we can easily share this functionality with our components. In a new component called NewsFeed, we’ll use the custom hook:

function NewsFeed({ url }) {
  const posts = useJazzyNews();

  return (
    <>
      <h1>{posts.length} articles</h1>
      {posts.map(post => (
        <Post key={post.id} {...post} />
      ))}
    </>
  );
}

Deep Checking Dependencies

So far, the dependencies that we’ve added to the array have been strings. JavaScript primitives like strings, booleans, numbers, etc. compare. A string would equal a string as expected:

if ("gnar" === "gnar") {
  console.log("gnarly!!");
}

However, when we start to compare objects, arrays, and functions, the comparison is different. If we compared two arrays for example:

if ([1, 2, 3] !== [1, 2, 3]) {
  console.log("but they are the same");
}

These arrays [1,2,3] and [1,2,3] are not equal even though they look identical in length and in entries. This is because they are two different instances of a similar looking array. If we create a variable to hold this array value and then compare, we’ll see the expected output:

const array = [1, 2, 3];
if (array === array) {
  console.log("because it's the exact same instance");
}

In JavaScript, arrays, objects, and functions are the same only when they are the exact same instance. So how does this relate to the useEffect dependency array? To demonstrate this, we are going to need a component that we can force to render as much as we want. Let’s build a hook that causes a component to render whenever a key is pressed:

const useAnyKeyToRender = () => {
  const [, forceRender] = useState();

  useEffect(() => {
    window.addEventListener("keydown", forceRender);
    return () => window.removeEventListener("keydown", forceRender);
  });
};

At minimum, all we need to do to force a render is to invoke a state change function. We don’t care about the state value. We only want the state function: forceRender. (That’s why we added the comma using array destructuring. Remember, from Chapter 2?) When the component first renders, we will listen for keydown events. When a key is pressed, we will force the component to render by invoking forceRender. As we’ve done before, we’ll return a cleanup function where we stop listening to keydown events. By adding this hook to a component, we can force it to re-render simply by pressing a key.

With the custom hook built, we can use it in the App component (and any other component for that matter! Hooks are cool.):

function App() {
  useAnyKeyToRender();

  useEffect(() => {
    console.log("fresh render");
  });

  return <h1>Open the console</h1>;
}

Every time we press a key, the App component is rendered. useEffect demonstrates this by logging “fresh render” to the console ever time the App is rendered. Let’s adjust useEffect in the App component to reference the word value. If word changes, we’ll re-render:

const word = "gnar";
useEffect(() => {
  console.log("fresh render");
}, [word]);

Instead of calling useEffect on every keydown event, we would only call this after first render and any time the word value changes. It doesn’t change, so subsequent re-renders don’t occur. Adding a primitive or a number to the dependency array works as expected. The effect is invoked once.

What happens if instead of a single word, we use an array of words?

const words = ["sick", "powder", "day"];
useEffect(() => {
  console.log("fresh render");
}, [words]);

The variable words is an array. Because a new array is declared with each render, JavaScript assumes that words has changed, thus invoking the “fresh render” effect every time. The array is a new instance each time, and this registers as an update that should trigger a re-render.

Declaring words outside of the scope of the App would solve the problem:

const words = ["sick", "powder", "day"];

function App() {
  useAnyKeyToRender();
  useEffect(() => {
    console.log("fresh render");
  }, [words]);

  return <h1>component</h1>;
}

The dependency array in this case refers to one instance of words that is declared outside of the function. The “fresh render” effect does not get called again after the first render because words is the same instance as the last render. This is a good solution for this example, but it’s not always possible (or advisable) to have a variable defined outside of the scope of the function. Sometimes the value passed to the dependency array requires variables in scope. For example, we might need to create the words array from a React property like children:

function WordCount({ children = "" }) {
  useAnyKeyToRender();

  const words = children.split(" ");

  useEffect(() => {
    console.log("fresh render");
  }, [words]);

  return (
    <>
      <p>{children}</p>
      <p>
        <strong>{words.length} - words</strong>
      </p>
    </>
  );
}

function App() {
  return <WordCount>You are not going to believe this but...</WordCount>;
}

The App component contains some words that are children of the WordCount component. The WordCount component takes in children as a property. Then we set words in the component equal to an array of those words that we’ve called .split on. We would hope that the component will re-render only if words changes, but as soon as I press a key, we see the dreaded “fresh render” words appearing in the console.

Let’s replace that feeling of dread with one of calm, because the React team has provided us a way to avoid these extra renders. They wouldn’t hang us out to dry like that. The solution to this problem is, as you might expect, another hook: useMemo.

useMemo invokes a function to calculate a memoized value. In computer science in general, memoization is a technique that is used to improve performance. In a memoized function, the result of a function call is saved and cached. Then when the function is called again with the same inputs, the cached value is returned.

The way that useMemo works is that we pass it a function that is used to calculate and create a memoized value. useMemo will only recalculate that value when one of the dependencies has changed. First, let’s import the useMemo hook:

import React, { useEffect, useMemo } from "react";

Then we’ll use the function to set words:

const words = useMemo(() => {
  const word = children.split(" ");
  return word;
}, []);

useEffect(() => {
  console.log("fresh render");
}, [words]);

useMemo invokes the function sent to it and sets words to the return value of that function. Like useEffect, useMemo relies on a dependency array.

const words = useMemo(() => children.split(" "));

When you don’t include the dependency array with useMemo, the words are calculated with every render. The dependency array controls when the callback function should be invoked. The second argument sent to the useMemo function is the dependency array and should contain the children value.

function WordCount({ children = "" }) {
  useAnyKeyToRender();

  const words = useMemo(() => children.split(" "), [children]);

  useEffect(() => {
    console.log("fresh render");
  }, [words]);

  return (...);
}

The words array is depends on the children property. If children changes, we should calculate a new value for words that reflects that change. At that point, useMemo will calculate a new value for words, when the component initially renders and if the children property changes.

The useMemo hook is a great function to understand when you’re creating React applications. Performance optimization tools are great to have in your belt, but it’s important to avoid premature optimizations. Look to useMemo only when you really need it.

Also in the realm of performance optimization Hooks is useCallback. useCallback can be used like useMemo, but it memoizes functions instead of values. For example:

const fn = () => {
  console.log("hello");
  console.log("world");
};

useEffect(() => {
  console.log("fresh render");
  fn();
}, [fn]);

fn is a function that logs “Hello” then “World”. It is a dependency of useEffect, but just like words, JavaScript assumes fn is different every render. Therefore, it triggers the effect every render. This yields a “fresh render” for every key press. It’s not ideal. If we are thirsty for performance, useCallback could be our tall glass of lemonade.

Start by wrapping the function with useCallback:

const fn = useCallback(() => {
  console.log("hello");
  console.log("world");
}, []);

useEffect(() => {
  console.log("fresh render");
  fn();
}, [fn]);

useCallback memoizes the function value for fn. Just like useMemo and useEffect, it also expects a dependency array as the second argument. In this case, we create the memoized callback once becuase the dependency array is empty.

Now that we have an understanding of the uses and differences between useMemo and useCallback, let’s improve our useJazzyNews hook. Every time there is a new post, we’ll call newPostChime.play(). In this hook, posts are an array, so we’ll need to use useMemo to memoize the value:

const useJazzyNews = () => {
  const [_posts, setPosts] = useState([]);
  const addPost = post => setPosts(allPosts => [post, ...allPosts]);

  const posts = useMemo(() => _posts, [_posts]);

  useEffect(() => {
    newPostChime.play();
  }, [posts]);

  useEffect(() => {
    newsFeed.subscribe(addPost);
    return () => newsFeed.unsubscribe(addPost);
  }, []);

  useEffect(() => {
    welcomeChime.play();
    return () => goodbyeChime.play();
  }, []);

  return posts;
};

Now the Jazzy News hook plays a chime every time there is a new post. We have made this happen with a few changes to the hook. First const [posts, setPosts] was renamed to const [_posts, setPosts]. We will calculate a new value for posts every time _posts change.

Next, we add the effect that plays the chime every time the post array changes. We are listening to the news feed for new posts. When a new post is added, this hook is re-invoked with _posts reflecting that new post. Then a new value for post is memoized because _posts have changed. Then the chime plays, because this effect is dependent on posts. It only plays when the posts change, and the list of posts only changes when a new one is added.

Later in the chapter, we’ll discuss the React Profiler, a browser extension for testing performance and rendering of React components. There we’ll dig into more detail about when to use useMemo and useCallback. (Spoiler alert: sparingly!)

When to useLayoutEffect

We understand that the render always comes before useEffect. The render happens first and then all effects run in order with full access to all of the values from the render. A quick look at the React docs will point out that there’s another type of effect hook: useLayoutEffect.

  1. render

  2. useLayoutEffect is called

  3. Browser Paint: the time when the component’s elements are actually added to the DOM

  4. useEffect is called

This can be observed by adding some simple console messages:

import React, { useEffect, useLayoutEffect } from "react";

function App() {
  useEffect(() => console.log("useEffect"));
  useLayoutEffect(() => console.log("useLayoutEffect"));
  return <div>ready</div>;
}

In the App component, useEffect is the first hook followed by useLayoutEffect. We see that useLayoutEffect is invoked before useEffect:

useLayoutEffect
useEffect

useLayoutEffect is invoked after the render, but before the browser paints the change. In most circumstances, useEffect is the write tool for the job, but if your effect is essential to the browser paint, you may want to use useLayoutEffect. For instance, you may want to obtain the with and height of an element when the window is resized:

const useWindowSize = () => {
  const [width, setWidth] = useState(0);
  const [height, setHeight] = useState(0);

  const resize = () => {
    setWidth(window.innerWidth);
    setHeight(window.innerHeight);
  };

  useLayoutEffect(() => {
    window.addEventListener("resize", resize);
    resize();
    return () => window.removeEventListener("resize", resize);
  });

  return [width, height];
};

The width and height of the window is information that your component may need before the browser paints. useLayoutEffect is used to calculate the window’s width and height before the paint. Another example of when to use useLayoutEffect is when tracking the position of the mouse:

const useMousePosition = () => {
  const [x, setX] = useState(0);
  const [y, setY] = useState(0);

  const setPosition = ({ x, y }) => {
    setX(x);
    setY(y);
  };

  useLayoutEffect(() => {
    window.addEventListener("mousemove", setPosition);
    return () => window.removeEventListener("mousemove", setPosition);
  });

  return [x, y];
};

It is highly likely that the x and y position of the mouse will be used when painting the screen. useLayoutEffect is available to help us calculate those positions accurately before the paint.

Rules To Follow with Hooks

As you are working with Hooks, there are a few guidelines to keep in mind that can help avoid bugs and unusual behavior:

Hooks only run in the scope of a component

Hooks should only be called from React functions. They also can be added to custom Hooks, which eventually are added to components. Hooks are not regular JavaScript. They are a React pattern but are starting to be modeled and incorporated in other libraries like Flutter.

It’s a good idea to break functionality out into multiple Hooks In our earlier example with the Jazzy News component, we split everything related to subscriptions into one effect and everything related to sound effects into another effect. This immediately made the code easier to read but there was another benefit of doing this. Since Hooks are invoked in order, it’s a good idea to keep them small. Once invoked, React saves the values of Hooks in an array so the values can be tracked. Consider the following component:

function Counter() {
  const [count, setCount] = useState(0);
  const [checked, toggle] = useState(false);

  useEffect(() => {
    ...
  }, [checked]);

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

  useEffect(() => {
    ...
  }, [count]);

  return ( ... )
}

The order of Hook calls is the same for each and every render:

[count, checked, DependencyArray, DependencyArray, DependencyArray]

Hooks Should Only Be Called at the Top Level

Hooks should be used at the top level of a React function. They cannot be placed into conditional statements, loops, or nested functions. Let’s adjust the counter:

function Counter() {
  const [count, setCount] = useState(0);

  if (count > 5) {
    const [checked, toggle] = useState(false);
  }

  useEffect(() => {
    ...
  });

  if (count > 5) {
    useEffect(() => {
      ...
    });
  }

  useEffect(() => {
    ...
  });

  return ( ... )
}

When we use useState within the if statement, we’re saying that the hook should only be called when the count value is greater than 5. That will throw off the array values. Sometimes the array will be: [count, checked, DependencyArray, DependencyArray, DependencyArray]. Other times: [count, DependencyArray, DependencyArray]. The index of the effect in that array matters to React. It’s how values are saved.

Wait, so are we saying that we can never use conditional logic in React applications anymore? Of course not! We just have to organize these conditionals differently. You can nest if statements, loops, and other conditionals within the hook:

function Counter() {
  const [count, setCount] = useState(0);
  const [checked, toggle] = useState(count => (count < 5) ? undefined : !c, (count < 5) ? undefined);

  useEffect(() => {
    ...
  });

  useEffect(() => {
    if (count < 5) {
      return
    }
    ...
  });

  useEffect(() => {
    ...
  });

  return ( ... )
}

Here the value for checked is based on the condition that the count is greater than 5. When count is less than 5, the value for checked is undefined. Nesting this conditional inside the hook means that the hook remains on the top level, but the result is similar. The second effect enforces the same rules. If the count is less than 5, the return statment will prevent the effect from continuing to execute. This keeps the hook values array intact: [countValue, checkedValue, DependencyArray, DependencyArray, DependencyArray].

Like conditional logic, you need to nest asynchronous behavior inside of a hook. useEffect takes a function as the first argument, not a promise. So you can’t use an async function as the first argument: +useEffect(async () => {})+. You can however create an async function inside of the nested function like this:

useEffect(() => {
  const fn = async () => {
    await SomePromise();
  };
  fn();
});

We’ve created a variable fn to handle the async/await and then we call the function as the return. You can give this function a name, or you may use async effects as an anonymous function:

useEffect(() => {
  (async () => {
    await SomePromise();
  })();
});

If you follow these rules, you can avoid some common gotchas with React Hooks. If you’re using create-react-app, there is an ESLint plugin included called eslint-plugin-react-hooks that provides warning hints if you’re in violation of these rules.

Improving Code with useReducer

Consider the Checkbox component. This component is a perfect example of a component that holds simple state. The box is either checked or not checked. checked is the state value, and setChecked is a function that will be used to change the state. When the component first renders, the value of checked will be false:

function Checkbox() {
  const [checked, setChecked] = useState(false);

  return (
    <>
      <input
        type="checkbox"
        value={checked}
        onChange={() => setChecked(checked => !checked)}
      />
      {checked ? "checked" : "not checked"}
    </>
  );
}

This works well, but one area of this function could be cause for alarm:

onChange={() => setChecked(checked => !checked)}

Look at it closely. It feels ok at first glance, but are we stirring up trouble here? We’re sending a function that takes in the current value of checked and returns the opposite, !checked. This is probably more complex than it needs to be. Developers could easily send the wrong information and break the whole thing. Instead of handling this way, why not provide a function as a toggle?

Let’s add a function called toggle that will do the same thing: call setChecked and return the opposite of the current value of checked.

function Checkbox() {
  const [checked, setChecked] = useState(false);

  function toggle() {
    setChecked(checked => !checked);
  }

  return (
    <>
      <input type="checkbox" value={checked} onChange={toggle} />
      {checked ? "checked" : "not checked"}
    </>
  );
}

This is better. onChange is set to a predictable value: the toggle function. We know what that function is going to do every time, everywhere it is used. We can still take this one step further to yield even more predictable results each time we use the checkbox component. Remember the function that we sent to setChecked in the toggle function?

setChecked(checked => !checked);

We’re going to refer to this function, +checked => !checked+, by a different name now: a reducer. A reducer function’s most simple definition is that it takes in the current state and returns a new state. If checked is false, it should return the opposite, true. Instead of hardcoding this behavior into onChange events, we can abstract the logic into a reducer function that will always produce the same results. Instead of useState in the component, we’ll use useReducer:

function Checkbox() {
  const [checked, toggle] = useReducer(checked => !checked, false);

  return (
    <>
      <input type="checkbox" value={checked} onChange={setChecked} />
      {checked ? "checked" : "not checked"}
    </>
  );
}

useReducer takes in the reducer function and the initial state, false. Then we’ll set the onChange function to setChecked which will call the reducer function.

Our earlier reducer +checked => !checked+ is a prime example of this. If the same input is provided to a function, the same output should be expected. This concept originates with Array.reduce in JavaScript. reduce fundamentally does the same thing as a reducer: it takes in a function (to reduce all of the values into a single value) and an initial value and returns one value.

Array.reduce takes in a reducer function and an initial value. For each value in the numbers array, the reducer is called until one value is returned.

const numbers = [28, 34, 67, 68];

numbers.reduce((number, nextNumber) => number + nextNumber, 0); // 197

The reducer sent to Array.reduce takes in two arguments. You can also send multiple arguments to a reducer function:

function Numbers() {
  const [number, setNumber] = useReducer(
    (number, newNumber) => number + newNumber,
    0
  );

  return <h1 onClick={() => setNumber(30)}>{number}</h1>;
}

Every time we click on the h1, we’ll add 30 to the total each time.

useReducer to Handle Complex State

useReducer can help us handle state updates more predictably as state becomes more complex. Consider an object that contains user data:

const firstUser = {
  id: "0391-3233-3201",
  firstName: "Bill",
  lastName: "Wilson",
  city: "Missoula",
  state: "Montana",
  email: "[email protected]",
  admin: false
};

Then we have a component called User that sets the firstUser as the initial state, and the component displays the appropriate data:

function User() {
  const [user, setUser] = useState(firstUser);

  return (
    <div>
      <h1>
        {user.firstName} {user.lastName} - {user.admin ? "Admin" : "User"}
      </h1>
      <p>Email: {user.email}</p>
      <p>
        Location: {user.city}, {user.state}
      </p>
      <button>Make Admin</button>
    </div>
  );
}

A common error when managing state is to overwrite the state:

<button
  onClick={() => {
    setUser({ admin: true });
  }}
>
  Make Admin
</button>

Doing this would overwrite state from firstUser and replace it with just what we sent to the setUser function: {admin: true}. This can be fixed by spreading the current values from user, and then overwriting the admin value:

<button
  onClick={() => {
    setUser({ ...user, admin: true });
  }}
>
  Make Admin
</button>

This will take the initial state and push in the new key/values: {admin: true}. We need to rewrite this logic in every onClick, making it prone to error. I might forget to do this when I come back to the app tomorrow.

function User() {
  const [user, setUser] = useReducer(
    (user, newDetails) => ({ ...user, ...newDetails }),
    firstUser
  );
  ...
}

Then send the new state value newDetails to the reducer, and it will be pushed into the object:

<button
  onClick={() => {
    setUser({ admin: true });
  }}
>
  Make Admin
</button>

This pattern is useful when state has multiple sub-values or when the next state depends on a previous state. Teach everyone to spread, they’ll spread for a day. Teach everyone to useReducer and they’ll spread for life.

Legacy setState and useReducer

In previous versions of React, we used a function called setState to update state. Initial state would be assigned in the constructor as an object.

---
class User extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      id: "0391-3233-3201",
      firstName: "Bill",
      lastName: "Wilson",
      city: "Missoula",
      state: "Montana",
      email: "[email protected]",
      admin: false
    };
  }
}
---

update state with setState

---
<button onSubmit={() => { this.setState({ admin: true }); }} > Make Admin </button>
---

The older incarnation of setState merged state values. The same is true of useReducer.

---
const [state, setState] = useReducer( (state, newState) => ({ ...state, ...newState }), initialState );
<button onSubmit={() => { setState({ admin: true }); }}>
Make Admin
</button>
</div>);
---

If you like this pattern, can use legacy-set-state npm or useReducer.

The past few examples are simple applications for a reducer. In the next chapter, we’ll dig deeper into reducer design patterns that can be used to simplify state management in your apps.

Persistent Objects with useRef

In Chapter 6, we discussed useRef, a hook that’s often used to access the values of DOM nodes. These are particularly useful with form elements. If I need to submit the value of an input with a form, I can capture it with a ref:

const txtTitle = useRef();

Once I create the ref, I’ll add it to the input field:

<input ref={txtTitle} type="text" placeholder="color title..." required />

Then I can submit using the .current property:

const submit = e => {
  e.preventDefault();
  const title = txtTitle.current.value;
  txtTitle.current.value = "";
};

This is a useful mechanism and makes sense in this context but what actually is a ref? When calling useRef(), you’re creating an object. This object is a container for storing any mutable value. Consider a component called EmployeeOfTheMonth that has an employee in state and a setEmployee function that changes the value of the employee:

function EmployeeOfTheMonth() {
  const [employee, setEmployee] = useState("Jerry");
  ...
}

The component will allow us to update the word based on a prompt, but we want to store all of the previous employees of the month somewhere. Here we’ll use a ref, initializing the value as an empty array:

const previousEmployees = useRef([]);

If you log the value of previousEmployees, you’ll see that it returns an object with one key, current, that is set to an empty array: the initial value.

console.log(previousEmployees); // { current: [] }

Then we’ll create the component:

function EmployeeOfTheMonth() {
  const [employee, setEmployee] = useState("Jerry");
  const previousEmployees = useRef([]);

  console.log("Current Employee of the Month:", employee);
  console.log("Previous Employees of the Month:", previousEmployees.current);

  return (
    <>
      <h1>Current Employee of the Month: {employee}</h1>
      <button
        onClick={() =>
          setEmployee(
            prompt("Who would you like to make Employee of the Month?")
          )
        }
      >
        Change Employee of the Month
      </button>
    </>
  );
}

Now we need to build the list of all the previous employees with useEffect. We want to make sure that we have record of this, so that someone isn’t given this honor more than once!

useEffect(() => {
  previousEmployees.current = [...previousEmployees.current, employee];
});

We spread the values from the previousEmployees.current array and the recently submitted word value into a new array that is set to previousEmployees.current.

Refs persist between renders but don’t trigger re-renders. The question becomes: do we actually want to save these values as refs? We’d probably want to store these values in state instead.

function EmployeeOfTheMonth() {
  const [employee, setEmployee] = useState("Jerry");
  const [previousEmployees, setPreviousEmployees] = useState([]);

  console.log("Current Employee of the Month", employee);
  console.log("Previous Employees of the Month", previousEmployees);

  const updateEmployee = () => {
    setEmployee(prompt("Who would you like to make Employee of the Month?"));
    setPreviousEmployees([...previousEmployees, employee]);
  };

  return (
    <>
      <h1>Current Employee of the Month: {employee}</h1>
      <button onClick={() => updateEmployee(setEmployee)}>
        Change Employee of the Month
      </button>
    </>
  );
}

What makes this part of the state? It’s part of the component. The data should be part of the component tree. But there are circumstances where you might not be dealing with components, like with timers.

useRef with Timers

JavaScript has built-in timing functions like setInterval and clearInterval. They’re built-in as part of the browser. When these functions are used with Hooks, they don’t always work as you’d expect. Let’s say we wanted to build a component called Timer that increments a timer every second. We’ll start by putting a time value in state and displaying that value.

Then we’d want to incorporate setInterval(function, delay) which takes in a function to be called every delay milliseconds. We’ll use this in useEffect to set the interval for a timer and then to clear the interval. clearInterval() takes in the id of the action that you want to cancel. Remember that when you return a function from useEffect, it will perform cleanup actions. And finally, since we want to only run the effect on mount and cleanup, we’ll add the empty array as the second argument:

function Timer() {
  const [time, setTime] = useState(0);

  function changeTime() {
    setTime(time + 1);
  }

  useEffect(() => {
    const timer = setInterval(changeTime, 1000);
    return () => {
      clearInterval(timer);
    };
  }, []);
  return <h1>Seconds: {time}</h1>;
}

There’s a problem here. If you run it, you’ll see the timer stop at 1 second. useEffect captures the timer from first render when it’s 0. The effect isn’t called again, so the value of time is always time + 1 or 1. It only captures the count from the first render.

This problem can be fixed by incorporating a ref. Remember, a ref is an object where we can store some values, so we’ll use that ref to store the timer value.

Let’s add a ref called interval to the Timer:

function Timer() {
  const [time, setTime] = useState(0);
  const interval = useRef();

  function changeTime() {
    setTime(time + 1);
  }

  return <h1>Seconds: {time}</h1>;
}

The changeTime function will be responsible for storing whatever the value of .current is at the moment it is called.

useEffect(() => {
  interval.current = changeTime;
});

When we call the .current() method in the second useEffect, the function has access to the value of time in scope of the function. time can be read by calling interval.current():

useEffect(() => {
  function getRefValue() {
    interval.current();
  }
  const timer = setInterval(getRefValue, 1000);
  return () => {
    clearInterval(timer);
  };
}, []);

The whole thing together looks like this:

import React, { useState, useEffect, useRef } from "react";
import { render } from "react-dom";

function Timer() {
  const [time, setTime] = useState(0);

  const interval = useRef();

  function changeTime() {
    setTime(time + 1);
  }

  useEffect(() => {
    interval.current = changeTime;
  });

  useEffect(() => {
    function getRefValue() {
      interval.current();
    }
    const timer = setInterval(getRefValue, 1000);
    return () => {
      clearInterval(timer);
    };
  }, []);

  return <h1>Seconds: {time}</h1>;
}

The ref provides us with the storage container where we can always access the value of the interval. We can take a peek inside that container every time the value is incremented, and we can be assured that it’s always correct.

Still though, I have a nagging suspicion that this could be better. Getting and setting refs seems confusing if we have to do this every time we create an interval. It feels like it might be time to create a function to handle creating an interval. It feels like we want to be able to pass dynamic values and yield predictable results. It feels like we need to write our own hook!

We’re going to rebuild the timer using a new hook called useInterval. This will encapsulate all of the timers by taking in a callback and delay. Then we can use these within each subsequently created timer components:

function useInterval(callback, delay) {
  const interval = useRef();

  useEffect(() => {
    interval.current = callback;
  }, [callback]);

  useEffect(() => {
    function getRefValue() {
      interval.current();
    }
    if (delay !== null) {
      let id = setInterval(getRefValue, delay);
      return () => clearInterval(id);
    }
  }, [delay]);
}

Once created, we’ll use this in the Timer component. useInterval takes in the callback which increments the time every 50ms.

function Timer() {
  const [time, setTime] = useState(0);
  useInterval(() => {
    setTime(time + 50);
  }, 50);

  return <h1>{time}</h1>;
}

How awesome is this? useInterval will always take in a function describing what happens at a certain interval and a number of milliseconds for calling that function. If we run this, we see a pretty rapidly moving timer. How might we pause this?

We want to trigger the pause with a button, so we’ll add a toggle function for that button to call setPaused. We also need to adjust the delay. If paused is true, the interval delay will be set to null. If not paused, the interval delay will be 50.

function Timer() {
  const [time, setTime] = useState(0);
  const [paused, setPaused] = useState(false);
  useInterval(
    () => {
      setTime(time + 50);
    },
    paused ? null : 50
  );

  function toggle() {
    setPaused(!paused);
  }

  return (
    <>
      <h1>{time}</h1>
      <button onClick={toggle}>{paused ? "Start" : "Pause"}</button>
    </>
  );
}

In the <button> tag, if paused, it should say “Start”, if not paused, it should say pause.

Then we can add a reset button.

function reset() {
  setTime(0);
  setPaused(true);
}

return (
  <>
    <h1>{time}</h1>
    <button onClick={toggle}>{paused ? "Start" : "Pause"}</button>
    <button onClick={reset}>Reset</button>
  </>
);

useInterval is a great way of taking a few imperative functions and wrapping them. This typically will lead you to more predictable results because developers will be able to use the hook instead of getting bogged down with the quirks of working with React and browser timing functions.

Improving Component Performance

In a React application, components are rendered… usually a lot. Improving performance includes preventing unnecessary renders and reducing the time a render takes to propagate. React comes with tools to help us prevent unnecessary renders: memo, useMemo, and useCallback. We looked at useMemo and useCallback earlier in the chapter, but in this section, we’ll go into more detail about how to use these Hooks to make your websites perform better.

The memo function is used to create Pure Components. As discussed in Chapter 3, we know that given the same parameters, a pure function will always return the same result. A Pure Component works the same way. In React, a Pure Component is a Component that always renders the same output, given the same properties.

Let’s create a component called Cat:

const Cat = ({ name }) => {
  console.log(`rendering ${name}`);
  return <p>{name}</p>;
};

Cat is a Pure component. The output is always a paragraph that displays the name property. If the name provided as a property is the same, the output will be the same.

function App() {
  const [cats, setCats] = useState(["Biscuit", "Jungle", "Outlaw"]);
  return (
    <>
      {cats.map((name, i) => (
        <Cat key={i} name={name} />
      ))}
      <button onClick={() => setCats([...cats, prompt("Name a cat")])}>
        Add a Cat
      </button>
    </>
  );
}

This app uses the Cat component. After the initial render, the console reads:

rendering Biscuit
rendering Jungle
rendering Outlaw

When the “Add a Cat” button is clicked, the user is prompted to add a cat:

Warning

This code works because prompt is blocking. This is just an example. Don’t use prompt in a real app.

If I add the cat named “Ripple”, we see all Cat components are re-rendered:

rendering Biscuit
rendering Jungle
rendering Outlaw
rendering Ripple

Every time I add a cat, every Cat component is rendered, but the Cat component is a pure component. Nothing changes about the output given the same prop, so there shouldm’t be a render for each of these. We don’t want to re-render a Pure Component if the properties have not changed. The memo function can be used to create a component that will only render when its properties change. Start by importing it from the React library and use it to wrap the current Cat component:

import React, { useState, memo } from "react";

const Cat = ({ name }) => {
  console.log(`rendering ${name}`);
  return <p>{name}</p>;
};

const PureCat = memo(Cat);

Here we’ve created a new component called PureCat. PureCat will only cause the Cat to render when the properties change. Then we can replace the Cat component with ``PureCat``in the App component:

cats.map((name, i) => <PureCat key={i} name={name} />);

Now, every time we add a new cat name like “Pancake”, we see only one render in the console:

rendering Pancake

Because the names of the other cats have not changed, we do not re-render those Cat components. This is working well for a name property but what if we introduce a function property to the Cat component?

const Cat = memo(({ name, meow = f => f }) => {
  console.log(`rendering ${name}`);
  return <p onClick={() => meow(name)}>{name}</p>;
});

Every time a cat is clicked on, we can use this property to log a meow to the console:

<PureCat key={i} name={name} meow={name => console.log(`${name} has meowed`)} />

When we add this change, the PureCat no longer works as expected. It is always rendering every cat Cat component even though the name property remains the same. This is because of the added meow property. Unfortunately, every time we define the meow property as a function. It is always new function. To React the meow property has changed, and the component is re-rendered.

The memo function will allow us to define more specific rules around when this component should re-render:

const RenderCatOnce = memo(Cat, () => true);
const AlwaysRenderCat = memo(Cat, () => false);

The second argument sent to the memo function is a predicate. A predicate is a function that only returns true or false. . This function decides whether to re-render a cat or not. When this function returns false, the Cat is re-rendered. When this function returns true the Cat will not be re-rendered. No matter what, the Cat is always rendered at least once. This is why RenderCatOnce, it will render once, and then never again. Typically, this function is used to check actual values:

const PureCat = memo(
  Cat,
  (prevProps, nextProps) => prevProps.name === nextProps.name
);

We can use the second argument to compare properties and decide if Cat should be re-rendered. The predicate receives the previous properties and the next properties. These objects are used to compare the name property. If the name changes, the component will be re-rendered. If the name is the same, it will to be re-rendered regardless of what React thinks about the meow property.

shouldComponentUpdate and PureComponent

The concepts we’re discussing are not new to React. The memo function is a new solution to a common problem. In previous versions of React, there was a method called shouldComponentUpdate. If present in the component, it was used to let React know under which circumstances the component should update. shouldComponentUpdate described which props or state would need to change for the component to re-render. Once shouldComponentUpdate was part of the React library, it was embraced as a very useful feature by many. So useful that the React team decided to create an alternate way of creating a component as a class. A class component would look like this: + class Cat extends React.Component { render() { return ( {name} is a good cat! ) } } + A PureComponent would look like this: + class Cat extends React.PureComponent { render() { return ( {name} is a good cat! ) } } + PureComponent is the same as React.memo, but PureComponent is only for class components. React.memo is only for function components.

useCallback and useMemo can be used to memoize object and function properties. Let’s use useCallback in the Cat component:

const PureCat = memo(Cat);
function App() {
  const meow = useCallback(name => console.log(`${name} has meowed`, []);
  return <PureCat name="Biscuit" meow={meow} />
}

In this case, we did not provide a property checking predicate to memo(Cat). Instead, we used useCallback to ensure the the meow function has not changed. Using these functions can be helpful if you’re dealing with too many re-renders in your component tree.

When to Refactor

The last Hooks that we showed: useMemo and useCallback along with the memo function are commonly overused. React is designed to be fast. It is designed to have components render a lot. The process of optimizing for performance began when you decided to use React in the first place. It’s fast. Any further refactoring should be a last step.

There are tradeoffs to refactoring. Using useCallback and useMemo everywhere because it seems like a good idea may actually make your app less performant. You’re adding more lines of code and developer hours to your application. When you refactor for performance, it is important to have a goal. Perhaps you want to stop the screen from freezing or flickering. Maybe you know there are some costly functions that are slowing the speed of your app unreasonably.

The React Profiler can be used to measure the performance of each of your components. The profiler ships with the React Developer Tools which you’ve likely installed already:

- [Chrome](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en)
- [Firefox](https://addons.mozilla.org/en-US/firefox/addon/react-devtools/)

Always make sure your app works and you are satisfied with the codebase before refactoring. Over refactoring, or refactoring before your app works, can introduce weird bugs that are hard to spot, and it might not be worth your time and focus to introduce these optimizations.

In the last two chapters, we’ve introduced many of the Hooks that ship with React. You’ve seen use cases for each hook. You’ve created your own custom Hooks by composing other Hooks. Next we’ll build on these foundational skills by incorporating additional libraries and advanced patterns.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset