React: State

  • React
  • JavaScript

Happy Halloween! It's been a super busy month. Music and hang out happenings are beginning to pick up again after the long summer. I soundtracked the annular solar eclipse at Cherrywood Coffeehouse on October 14. We're in the last stages of an enormous project at work. But I'm still here with my regularly-scheduled blog post.

Here's my costume this year:

Joey dressed as a bat, spreading his wings

I'm a bat.

Anyway, life marches on. I have a lot of creative projects in the works: recording with one of my bands at the end of the year, doing my yearly wrap up post, planning blog posts for next year, and more. Without further ado, let's talk a bit about React State.

State and useState()

Whenever we create dynamic values in React, we need to use React state. State is React's built-in memory, and it's used for values that change over time. The classic example of state is tracking whether or not a user is logged in. This is a pretty hefty example – think of how drastically different many websites are when you're logged out as opposed to when you're logged in – but state can also be used to keep track of something as small as whether a dialog is open or not or if a <div> is being hovered.

Take this accordion I built in React for example:

There's quite a bit going on with the whole application, but at its core the Accordion component is just tracking whether the Accordion content should be showing or not. I'll return to this example in a bit. Let's start with the basics of React state.

The useState() Hook

A single value that you want to track using state is often called a state variable. To create a state variable, we use the useState() function. useState() is a hook. A hook is a special type of function that allows us to "hook into" React internals.

useState() takes a single argument: the state variable's initial value. Here's an example:

React.useState(0);

In this example, the initial value is 0. The useState() hook returns an array that contains two items:

  1. The current value of the state variable
  2. A function we can use to update the state variable

The common pattern is to destructure each of these items into their own distinct identifiers. Let's build on our example:

import { useState } from "react";
const [count, setCount] = useState(0);

Now in our example, the current value of the state variable is assigned to the variable count, and the function that we are designating to update the state variable is setCount. Let's put it into a broader context to tie it all together:

import { useState } from "react";
function Counter() {  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>Value: {count}</button>;}
export default Counter;

Naming Convention

It's customary to follow the x, setX naming convention for useState():

const [user, setUser] = useState();const [errorMessage, setErrorMessage] = useState();const [shoppingCart, setShoppingCart] = useState();

Initial Value

As we saw above, we can provide an initial value for useState():

const [count, setCount] = useState(1);console.log(count); // Expected result: 1

We can also supply a function as the initial value. This is useful when we need to fetch some data from an API or do some kind of calculation in order to set the initial value:

const [count, setCount] = useState(() => {  return 1 + 1;});
console.log(count); // 2

Or another example:

const [count, setCount] = useState(() => {  return window.localStorage.getItem("saved-count");});

Core React Loop

This pattern may appear simple at first, but there's a lot of complexity behind the scenes. Using our Counter example, let's develop a mental model for how React works.

Mount

function Counter() {  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>Value: {count}</button>;}

As a bit of review, this transpiles to the following:

function Counter() {  const [count, setCount] = useState(0);
  return React.createElement(    "button",    {      onClick: () => setCount(count + 1),    },    "Value: 0",    count  );}

When this code runs, React.createElement() produces a React element, which is just the following plain JavaScript object:

{  type: 'button',  key: null,  ref: null,  props: {    onClick: () => setCount(count + 1),  },  children: 'Value: 0',  _owner: null,  _store: {    validated: false,  },}

As we know, React elements are essentially descriptions of the UI that we want. This React element is describing a DOM node. React takes that DOM node and appends it to the page, and it ultimately winds up as something sort of like:

<button onclick="setCount()">Value: 0</button>

The process we're describing here is called the mount. The mount happens when we render the component for the first time. A lot of stuff happens here: React creates the necessary DOM nodes from scratch and injects them into the page with their initial state set. There is no previous version or state to compare this component to.

Trigger

Now that the component/<button> is on the page, it's available to click. When it gets clicked, the setCount() function is called, and we pass in a new value. count gets incremented from 0 to 1.

This process, in which the setX function is called, is referred to as the Trigger. It tells React that the value of the state variable has been updated.

In our running example, we have clicked our <button> once, and the count state variable's value has changed from 0 to 1.

Reconciliation/Render/Re-render

Because the state has changed, React figures out all of the places in the DOM that where that change needs to be reflected. This step is called reconciliation. It's sometimes also known as a render or re-render. It's important to note that this does not refer to the actual visual/DOM node change. Instead, in React, "render" is a sort of behind-the-scenes comparison between previous versions of components and state. It's the process by which React figures out what has changed.

In our running example, React observes that state has changed, so React reconciles/renders that count is the only state value that has changed, and its value has changed from 0 to 1.

Commit

If in the reconciliation React determines that a DOM update is required, then React will perform those DOM mutations, specifically targeting the places in the DOM where that change needs to be reflected. React is said to be committing the changes to the DOM. A commit does not trigger a full page refresh. React commits changes to the DOM, and the browser responds by re-painting that DOM element. React itself does not do the re-paint, the browser does.

In our running example, because React reconciles that count changed from 0 to 1 and that value is present on the DOM element, it commits the change to the DOM.

What started out like this:

{  type: 'button',  key: null,  ref: null,  props: {    onClick: () => setCount(count + 1),  },  children: 'Value: 0',  _owner: null,  _store: {    validated: false,  },}

becomes this:

{  type: 'button',  key: null,  ref: null,  props: {    onClick: () => setCount(count + 1),  },  children: 'Value: 1',  _owner: null,  _store: {    validated: false,  },}

Another Example

Okay let's take a look at another example that may help to further break this down.

function HotelRewards({ points }) {  if (points < 1000) {    return <p>You haven't earned enough points yet.</p>;  }
  return <p>You've earned a free night's stay!</p>;}

HotelRewards checks a points prop and returns one of two paragraphs.

We might get something like...

points: 800
{  type: 'p',  key: null,  ref: null,  props: {},  children: "You haven't earned enough points yet.",}

And if we re-render this component, it might become...

points: 900
{  type: 'p',  key: null,  ref: null,  props: {},  children: "You haven't earned enough points yet.",}

So re-rendering can happen on any trigger that changes a state variable, but in this case it doesn't result in any kind of DOM change "commit". In both cases points is less than 1000, so the UI doesn't change.

You can read more in the React docs about Render and Commit.

Asynchronous Updates

If we try to do the following...

function App() {  const [count, setCount] = useState(0);
  return (    <>      <p>You've clicked {count} times.</p>      <button        onClick={() => {          setcount(count + 1);          console.log(count);        }}      >        Click me!      </button>    </>  );}

...when the user clicks on the button 0 gets logged out to the console. That's because state setter functions don't update state immediately. When a state setter function is called, React waits for that current operation (in this example: processing the click) to finish before updating the value and triggering a re-render.

Updating a state variable is asynchronous. It affects what the state will be for the next render.

If we needed to access that value as soon as it was updated within the state setter function, here's how we would do it:

function App() {  const [count, setCount] = useState(0);
  return (    <>      <p>You've clicked {count} times.</p>      <button        onClick={() => {          const nextCount = count + 1;          setCount(nextCount);          console.log(nextCount);        }}      >        Click me!      </button>    </>  );}

This is a very common pattern, to create a variable to store what the state value will become. If the convention is for the state variable to be x and the setter function be setX, it's helpful to name the temporary variable that holds the state's future value as nextX.

Accordion Example

So let's go back to the Accordion example from above:

Here is my App.js:

// App.jsimport "./App.css";import Accordion from "./components/Accordion/Accordion";
const bakingQuestions = [  {    question: "How many grams of flour are there in a cup?",    answer: "There are 120 grams of flour in a cup.",  },  {    question: "Can you substitute cane sugar for granulated?",    answer:      "While cane sugar has slightly larger and somewhat differently colored crystals, it can be substituted for granulated sugar just fine.",  },];
export default function App() {  return (    <div      className="App"      style={{        display: "flex",        alignItems: "center",        justifyContent: "center",        paddingTop: 20,      }}    >      <div        style={{          width: "100%",          maxWidth: 600,          display: "flex",          flexDirection: "column",          gap: 32,        }}      >        {bakingQuestions.map((question, i) => {          return <Accordion key={i} content={question} id={i} />;        })}      </div>    </div>  );}

The first thing happening here is importing App.css for some global styles...

/* App.css */body {  font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue",    Helvetica, Arial, "Lucida Grande", sans-serif;  font-weight: 300;  background-color: #38473e;}

...as well as the Accordion component, which we'll dig into in a moment. Next up I have an array of objects with question and answer properties assigned to the bakingQuestions variable. Within the App component itself I map over this array and pass each array item to the Accordion as a content prop.

Let's dig into the Accordion component now:

// Accordion.jsimport { useState } from "react";import styles from "./Accordion.module.css";
export default function Accordion({ content, id }) {  const { question, answer } = content;  const [isExpanded, setIsExpanded] = useState(false);  function handleExpandedToggle() {    const nextIsExpanded = !isExpanded;    setIsExpanded(nextIsExpanded);  }  return (    <div className={styles.accordion}>      <div className={styles.question}>        <h2 className={styles.heading}>          <button            aria-expanded={isExpanded}            aria-controls={`accordion-content-${id}`}            onClick={handleExpandedToggle}            className={styles.trigger}          >            {question}          </button>        </h2>      </div>      <div        id={`accordion-content-${id}`}        className={styles.content}        hidden={!isExpanded}      >        <p className={styles.paragraph}>{answer}</p>      </div>    </div>  );}

First, we import the useState hook from React, as well as some CSS modules for this component:

/* Accordion.module.css */.heading {  margin: 0;}
.trigger {  background-color: #fff;  border: none;  border-radius: 8px;  color: #000;  text-align: center;  text-decoration: none;  display: block;  width: 100%;  font-size: 16px;  padding: 16px;  font-weight: bold;}
.trigger[aria-expanded="true"] {  border-bottom: 1px solid #000;  border-bottom-left-radius: 0;  border-bottom-right-radius: 0;}
.content {  background-color: #fff;  color: #000;  border-bottom-left-radius: 8px;  border-bottom-right-radius: 8px;  padding: 16px;}
.paragraph {  margin: 0;}

I use these styles throughout the Accordion component to style various <div>s and other JSX elements. Here are the relevant bits:

// in Accordion.js:export default function Accordion({ content, id }) {  const { question, answer } = content;  // ... state stuff  return (    <div className={styles.accordion}>      <div className={styles.question}>        <h2 className={styles.heading}>          <button className={styles.trigger}>{question}</button>        </h2>      </div>      <div className={styles.content}>        <p className={styles.paragraph}>{answer}</p>      </div>    </div>  );}

I'm initializing my state with the following:

const [isExpanded, setIsExpanded] = useState(false);

I basically create an isExpanded state variable with the value of false, and name the function that will update this state variable setIsExpanded.

The goal here is to have the <div> that contains the answer to be hidden when isExpanded is false and visible when isExpanded is true. I do that like so:

<div hidden={!isExpanded}>  <p className={styles.paragraph}>{answer}</p></div>

If isExpanded is true then hidden should be false; and if isExpanded is false then hidden should be true.

The other part of this is that we need for the <button> to be the element that triggers the accordion's expanding and collapsing. We can do that like so:

export default function Accordion({ content, id }) {  const { question, answer } = content;  const [isExpanded, setIsExpanded] = useState(false);  function handleExpandedToggle() {    const nextIsExpanded = !isExpanded;    setIsExpanded(nextIsExpanded);  }  return (    <div>      <div>        <h2>          <button onClick={handleExpandedToggle}>{question}</button>        </h2>      </div>      <div hidden={!isExpanded}>        <p>{answer}</p>      </div>    </div>  );}

I've defined a handleExpandedToggle() function. In this function I've created a nextIsExpanded variable, which is set to the negated current value of isExpanded and passes this new value to setIsExpanded(). If isExpanded is false then nextIsExpanded will true, and if isExpanded is true then nextIsExpanded will be false. handleExpandedToggle() gets passed to the <button>'s onClick event handler.

The other stuff in this component relates to making this a more accessible component. This component is by no means production ready, by the way. But for reference, aria-expanded on the <button> element helps Assistive Technologies to convey to their user whether an item associated with that button is expanded or not. aria-controls on the <button> element associates that button control with the DOM element that has the provided id, namely the <div id={`accordion-content-${id}`}> element. So here is the Accordion component code in full:

// Accordion.jsimport { useState } from "react";import styles from "./Accordion.module.css";
export default function Accordion({ content, id }) {  const { question, answer } = content;  const [isExpanded, setIsExpanded] = useState(false);  function handleExpandedToggle() {    const nextIsExpanded = !isExpanded;    setIsExpanded(nextIsExpanded);  }  return (    <div className={styles.accordion}>      <div className={styles.question}>        <h2 className={styles.heading}>          <button            aria-expanded={isExpanded}            aria-controls={`accordion-content-${id}`}            onClick={handleExpandedToggle}            className={styles.trigger}          >            {question}          </button>        </h2>      </div>      <div        id={`accordion-content-${id}`}        className={styles.content}        hidden={!isExpanded}      >        <p className={styles.paragraph}>{answer}</p>      </div>    </div>  );}

Review

When you have a value that is going to change over time during the lifecycle of your program, you will use React state to keep track of that value and its changes.

The easiest way to use React state is with the useState() React hook, which you can destructure into two variables: the value of the state variable and the setter function that will update that state variable. You can also pass the useState() the initial value of that state variable as an argument:

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

It's common practice to follow the x, setX naming convention here.

When a state variable gets updated in React, that update happens in three steps:

  1. Trigger: the state variable is updated
  2. Reconciliation/Render/Re-render: React figures out all of the places in the DOM where that state variable is used, and compares previous versions of components and state
  3. Commit: If React determines that any DOM nodes need to be updated, then it commits the changes to those DOM nodes and allows the browser to re-paint the DOM nodes. React does not touch the DOM if the rendering result is the same as last time.

State variables also happen asynchronously. React will wait for the full context surrounding the state setter function to complete before the next render. If you need the new value at the time it gets updated, you can use a temporary variable within the setter function. If you are following the x, setX naming convention, then I suggest naming this temporary variable nextX to make it clear what state variable it is associated with.