React state & effects

Learn how to use the useState and useEffect hooks to create dynamic interactions in React

React is designed to build dynamic apps with lots of interaction. A common difficulty with apps like this is keeping the DOM up-to-date as the user interacts. React has two concepts to help keep this manageable: "state" and "effects".

React state

State is data that changes while your application is running. This might be in response to user actions, or after a fetch request finishes.

In React all stateful values are stored in JS as special variables. We can render our UI based on these variables—when they change React will automatically re-run the component function and update the DOM to reflect the new state value.

Using state

Imagine we have a counter component. When the button is clicked we want the count to go up one:

function Counter(props) {
  const count = 0;
  return <button>Count is {count}</button>;
}

We need some way to make our Counter function run again if this value changes.

The React.useState method can be used to create a "stateful" value. It takes the initial state value as an argument, and returns an array. This array contains two things: the state value, and a function that lets you update the state value.

function Counter(props) {
  const stateArray = React.useState(0);
  const count = stateArray[0];
  const setCount = stateArray[1];
  return <button>Count is {count}</button>;
}

It's common to use array destructuring to simplify this:

function Counter(props) {
  const [count, setCount] = React.useState(0);
  return <button>Count is {count}</button>;
}

The setCount function lets us update our state value and tells React to re-run this component. E.g. if we called setCount(10) React will call our Counter component function again, but this time the count variable would be 10 instead of 0.

This is how React keeps your UI in sync with the state.

Event listeners

We have a function that will let us update the state, but how do we attach event listeners to our DOM nodes?

function Counter(props) {
  const [count, setCount] = React.useState(0);
  function increment() {
    setCount(count + 1);
  }`
  return <button onClick={increment}>Count is {count}</button>;
}

You can pass event listener functions in JSX like any other property. They are always formatted as "on" followed by the camelCased event name. So "onClick", "onKeyDown", "onChange" etc.

In this example we are passing a function that calls setCount with our new value of count.

Challenge 1

Time to add some state! Open up challenge-1.html in your editor. You should see the Counter component we just created. This is an example; you can delete it if you want.

Create a new component called Toggle. It should render a button that toggles a boolean state value when clicked. It should also render a div containing its children, but only when the boolean state value is true.

Example usage:

function App() {
  return <Toggle>This text is hidden until the button is clicked</Toggle>;
}

Side effects

React is designed to make it easy to keep your application in sync with your data/state. Component functions render DOM elements and keep them in sync with any state values.

But most apps need more than just a UI—there are also things like fetching data from an API, timers/intervals, global event listeners etc. These are known as "side effects"—they can't be represented with JSX.

We need a way to ensure our effects reflect changes in state just like our UI does.

Using effects

React provides another "hook" like useState() for running side-effects after your component renders. It's called useEffect(). It takes a function as an argument, which will be run after every render (by default).

Let's say we want our counter component to also update the page title (so the count shows in the browser tab). There's no way to represent this update using the JSX our component returns. Instead we can use an effect:

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

  React.useEffect(() => {
    document.title = `Count: ${count}`;
  });

  return <button onClick={() => setCount(count + 1)}>Count is {count}</button>;
}

React will run the arrow function we passed to useEffect() every time this component renders. Since calling setCount will trigger a re-render (as the state is updated) the page title will stay in sync with our state as the button is clicked.

Skipping effects

By default all the effects in a component will re-run after every render of that component. This ensures the effect always has the correct state values.

If your effect does something expensive/slow like fetching from an API (or sorting a massive array etc) then this could be a problem.

useEffect() takes a second argument to optimise when it re-runs: an array of dependencies for the effect. Any variable used inside your effect function should go into this array:

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

  React.useEffect(() => {
    document.title = `Count: ${count}`;
  }, [count]);

  return <button onClick={() => setCount(count + 1)}>Count is {count}</button>;
}

Now our effect will only re-run if the value of count has changed.

Running effects once

Sometimes your effect will not be dependent on any props or state, and you only want it to run once (after the component renders the first time). In this case you can pass an empty array as the second argument to useEffect(), to signify that the effect has no dependencies and never needs to be re-run.

For example if we wanted our counter to increment when the "up" arrow key is pressed:

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

  React.useEffect(() => {
    function handleKeyDown(event) {
      if (event.key === "ArrowUp") {
        setCount((prevCount) => prevCount + 1);
      }
    }
    window.addEventListener("keydown", handleKeyDown);
  }, []);

  return <div>Count is {count}</div>;
}

We add an event listener to the window, and pass an empty array to useEffect(). This will keep us from adding new event listeners every time count updates and triggers a re-render.

Cleaning up effects

Some effects need to be "cleaned up" if the component is removed from the page. For example timers need to be cancelled and global event listeners need to be removed. Otherwise you'd have a bunch of code running in the background trying to update a component that doesn't exist anymore.

If you return a function from your effect React will save it and call it if the component is removed from the page. React will also run it to clean up when a component re-renders (before the effects run again).

Let's clean up after our effect example from above:

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

  React.useEffect(() => {
    function handleKeyDown(event) {
      if (event.key === "ArrowUp") {
        setCount((prevCount) => prevCount + 1);
      }
    }
    // run handler function when keydowns happen
    window.addEventListener("keydown", handleKeyDown);
    // create fn that removes event listener
    function cleanup() {
      window.removeEventListener("keydown", handleKeyDown);
    }
    // react will run `cleanup` whenever it needs to remove this effect
    return cleanup;
  }, []);

  return <div>Count is {count}</div>;
}

The cleanup function we return will be called if the component unmounts (is removed from the page). That will ensure we don't keep running an unnecessary event listener and trying to update state that doesn't exist anymore.

Challenge 2

We're going to enhance our Toggle component from Part 3. You can either keep working in the same file or open up challenge-2.html to start fresh.

  1. Edit the Toggle component so that the page title (in the tab) shows whether the toggle is on or off.

  2. Then create a new component called MousePosition. It should keep track of where the mouse is in the window and render the mouse x and y positions.

  3. Put MousePosition inside your Toggle so you can show and hide it. This is how your final App should look:

    function App() {
      return (
        <Toggle>
          <MousePosition />
        </Toggle>
      );
    }

Last updated