React State

Chapter 16 21 mins

Learning outcomes:

  1. What is state in React
  2. What is the useState() hook
  3. A simple example of useState()
  4. How React preserves state between re-renders
  5. Calling useState() multiple times
  6. Lifting the state up

Introduction

As we've been witnessing thus far in this course, and as will be the case as soon as you start executing even the simplest of apps, state is a really important concept in React. We can hardly find any React that doesn't use state at all — it's that common!

In this chapter, we shall explore state in more detail. Particularly, we'll consider the useState() hook, learn how it works, and how React preserves state between re-renders. We'll also understand the idea of batching state updates and then flushing them at once after the repaint.

Not only this, but we'll also talk about the idea of lifting the state up from descendant components to ancestor components so that it could be shared between given elements.

There's simply just a lot to cover in this chapter, so let's get going.

What is state?

At the very core:

State is simply any data tied to a component instance in React that is meant to trigger a re-render.

As we shall learn later on in this course, there is another way to store data for a given component instance that is not meant to trigger a re-render; it's called refs.

Basically, whenever we needd to change the UI in response to an event or any other action, such as the completion of a timer, we have to use the state. The state is the only way to get the UI to change, since the state is the only way to trigger a render in React.

Precisely speaking, we can render things in React by manually calling the render() method from React DOM, but as you have already learnt, this isn't how React is meant to work!

State can only be stored for React elements representing component instances, not the ones representing DOM elements. This makes perfect sense because it's only in a component that we can create and work with state data.

How exactly to create and work with state data depends on what kind of components we use. If we use function components, we ought to use the useState() hook. But if we use class components, we ought to use the state property along with the class's setState() method.

In the following section, we take a deep dive into the useState() hook.

The useState() hook

Hooks came into React with its version 16.8. They serve to make function components have access to all of the utilities that were otherwise present in class components in prior versions. One of the most commonly-used hooks is useState().

We've already used and discussed about useState() a couple of times, but let's quickly review it one more time.

The useState() hook lets a function component 'hook' into the state utility of React.

More specifically, useState() creates a new piece of state data for a component instance.

The signature of useState() is as follows:

useState(initialState)

It takes in a single optional initialState argument to specify the initial state value.

Once complete, useState() returns back an array of two elements:

  1. The first one is the state datum.
  2. The second one is a function to update this state datum.

Customarily, this return value is destructured into two local constants of the function component: the first one naming the state datum, while the second one beginning with the word 'set' followed by the name of the state datum.

A state constant is simply a constant that holds a state datum.

If we want to, we can call useState() multiple times inside a component. Each call will have its own respective association to the state of a component instance. We'll learn more about this later on in this chapter.

The way useState() works is seriously quite remarkable.

When state is set for the first time on a component instance, via a call to useState() with a given argument, that call resolves with an array whose first element is that same argument.

But on subsequent renders of the component, when the same useState() function is invoked, the given argument is ignored, because the state is now instead obtained from internal cells created to store the state of component instances.

This is essentially how the same useState() call is able to be written as it is, inside a component, without the need for any conditionals wrapping it — on the very first call, it initializes the state (and then returns it) and then on subsequent calls, it just returns it back.

What a great design!

Remember that whenever a new component instance is created, its state data, if set, would get initialized to whatever value is provided at the time of invoking useState(). But then on subsequent calls, the existing state would be consumed, and the value provided to useState() simply ignored.

Let's now consider a handful of examples of working with the state.

A simple example

We'll start with a super elementary counter. An <h1> would display the count while a button would provide the functionality to increment that count.

To begin with, since we'll need to possibly store some state data to create this program, we'll create a component, and name it Counter.

function Counter() {
   // Counter's code will go here.
}

A <Counter> element would, likewise, denote a concrete instance of a Counter.

The next step is to think about what state does a counter need to maintain. This thought process would eventually hint us at how many useState() calls to make inside the component.

In this case, we can reason that a counter would only need to maintain its count. Therefore, let's define the count as a state value inside the component, and name it count.

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

Because the count begins at 0, we call useState(0) to initialize the count state to 0.

Now, let's build the rest of the counter:

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

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

The onClick handler of the button is another crucial thing to consider. It increments the counter by updating the count to the next value via setCount() (i.e. the state-updating function) and therefore triggers a re-render.

Once the counter is re-rendered, it has the incremented count value which then gets displayed inside the <h1>.

Live Example

Simple.

If we want the counter to increment by 2 each time the button is clicked, we just need to modify the setCount() invocation. The next value should be count + 2, likewise we ought to call setCount() with this expression:

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

   return (
      <>
         <h1>{count}</h1>
         <button onClick={() => setCount(count + 2)}>Increment by 2</button>
      </>
   );
}

Live Example

It's that simple to modify the increment count.

Preserving the state between re-renders

It's really useful to understand how exactly state internally works in React. In this section, we'll do so in detail.

So how exactly does React make sure that the state of elements remains preserved between re-renders?

Well, when React performs a re-render, as we know by now, it diffs the new virtual DOM tree with the previous one to determine what has changed and what not. This diffing plays an integral role in making sure that the state of elements remain in tact.

When the same type of element exists in the exact same location as before, diffing asserts that the element hasn't changed and so in the subsequent re-render, the element remains in tact. By 'in tact', we meant that the state of the element remains preserved, i.e. it isn't thrown away, and used by the call to useState() if that element represents a component instance.

Talking about how state is stored internally, React creates memory cells tied to each element in the virtual DOM tree. When the element is created for the very first time, and if it does have state data, that data is stored for the very first time in this block of memory, alotted to the element.

This block of memory might be an array, an iterator object, or some other kind of an object — that's purely implementation-dependent.

Then on subsequent renders, given that the exact same element occurs in the exact same position, the existing state data is retrieved from these memory cells and used for the element.

Simple?

If the element changes, i.e. its type is different, all the state data tied to it is effectively thrown away and all these memory cells are cleared up.

Moreover, whenever updating the state via a call to the setState() function, i.e. the second argument of the array returned by useState(), the state isn't modified right away. In other words, all state updates are batched and then applied asynchronously on the next run of the event loop, after the next browser repaint.

If state updates weren't batched, it would've meant that each update (to the same state value) would be applied independently, which could've further meant that n updates would've caused n re-renders.

Clearly, this wouldn't have made any sense at all. Batching makes sure that all state updates are grouped together and then performed at once before triggering the next re-render. This is both sensible and efficient.

Calling useState() multiple times

There's nothing in React that says that we can't use useState() more than once.

In fact, the whole idea behind useState() is to represent a related unit of the state — if we turn out to have two unrelated pieces of state, then we must create them as two separate units of state via two separate calls to useState().

It's important to remember that useState(), like all hooks in React, gets matched with the state data based on its invocation order. That is, the first useState() call would be tied to the first piece of state data, and the second useState() call would be tied to the second piece of state data, and so on.

When React processes a call to useState(), it automatically increments an internal pointer to the next state datum in the internal state store associated with the given element, so that the next call to useState() works on the next piece of state, not the one that the current useState() call worked over.

Let's consider an example of using more than one piece of state data in a component.

Below we try to add some spice to the Counter component shown above.

We could now configure the increment step count of the counter to anything besides 1, using an <input> field. The field's value is tied to a state constant called step via the onChange handler on it.

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

   return (
      <>
         <h1>{count}</h1>
         <input type="number" value={stepCount} onChange={(e) => setStepCount(+e.target.value)}>
         <button onClick={() => setCount(count + stepCount)}>Increment</button>
      </>
   );
}

Live Example

As you can see here, there are two state values in Counter this time. One specifies the current count of the counter (as before), whereas the other one specifies the step count.

If we want to, we can make this counter way more sophisticated by making the button kind-of like a hold-to-increment button. As you can guess, this will require a couple of timer functions, both setTimeout() and setInterval(), and a great deal of other awesome features of React.

In the coming chapters, as we'll learn more and more ideas in React, we'll make sure to evolve this still-simple counter to encompass the real potential of React.

The onChange handler in React

In JavaScript, the change event gets fired every time we get an input field to lose its focus, while at the same time its value is turns out to be different than the previous one stored in there.

In React, the onChange handler doesn't work the same way. The onChange handler instead gets called whenever the input field's value changes, even as we type into it.

If you're an experienced JavaScript programmer, you'll be immediately able to say that onChange in React resembles oninput in JavaScript, and that's absolutely true.

Lifting the state up

Working with state in React is super simple. But it's not as straightforward to think about where to put state in a React application. Sometimes, it might not make sense at all!

As we learnt in the introductory chapter, React abides by a unidirectional data flow architecture. More squarely, this means that data can't directly travel up in the virtual DOM tree — it can only travel downwards, from ancestors to descendant elements.

Hence, state data can't go from component instances to siblings or parents. This data flow leads to one pretty interesting concept in React; that of lifting the state up.

Lifting the state up is merely a pattern, an approach to bring state data out of a component into its parent, or grandparent, from where that data could be shared between siblings.

Let's see an example to help understand this better.

Suppose we have an input field where we could enter any data and then press Enter or click an add button to add that data as a new item of a list.

Here's the complete code of the program:

function App() {
   const [items, setItems] = useState([]);
   const [inputValue, setInputValue] = useState();

   function onSubmit(e) {
      e.preventDefault();
      inputValue && setItems([...items, inputValue]);      
   }

   return <>
      <form onSubmit={onSubmit}>
         <input onChange={e => setInputValue(e.target.value)} placeholder="Item name" />
         <button>Add item</button>
      </form>
      <ol>
         {items.map(item => (
            <li>{item}</li>
         ))}
      </ol>
   </>;
}

For now, we don't create any new component for the program — everything goes directly inside the App component.

There are two pieces of state data inside App:

  1. items holds a list of all the items to be rendered inside the given <ol>.
  2. inputValue holds the value of the <input> element so that we always have access to it when we are about to add a new item to the list.

The addition of a new item is handled by the onSubmit handler on the <form> element. The preventDefault() method, called inside the handler, serves to prevent the default behavior associated with form submission, i.e. to reload the current web page.

The following statement in the handler, in line 7, serves to check whether inputValue is non-empty and if it is, adds it as a new item of the <ol> list.

Overall, the code isn't really that complicated to understand.

Live Example

Now, let's try refactoring this code a little bit by putting the <form> inside a separate component InputForm and the <ol> element into a component called List.

Here's the definition of InputForm:

function InputForm() {
   function onSubmit(e) {
      e.preventDefault();
      inputValue && setItems([...items, inputValue]);      
   }

   return (
      <form onSubmit={onSubmit}>
         <input onChange={e => setInputValue(e.target.value)} placeholder="Item name" />
         <button>Add item</button>
      </form>
   );
}

And here's the definition of List:

function List() {
   return (
      <ol>
         {items.map(item => (
            <li>{item}</li>
         ))}
      </ol>
   );
}

So far, so good.

And here's the definition of App:

function App() {
   return <>
      <InputForm/>
      <List/>
   </>
}

Now the main question that stands is: where to put the state data from our previous code? Where should the items and inputValue state constants go?

Remember that as of yet, both the components above are invalid because they refer to identifiers that don't exist inside them.

Let's solve this problem thoughtfully.

As inputValue, i.e. the value of the <input> element, changes, do we need to change the list as well? Well, clearly no. As the input's value changes, we only need to store the latest value of the input as the component instance's state; not to render anything inside the list.

Thus, inputValue could easily go inside InputForm. The outer world won't know the value of the input field unless InputForm provides it itself. The value of the input remains nicely encapsulated within the InputForm component.

Let's get done with the InputForm component thus far:

function InputForm() {
   const [inputValue, setInputValue] = useState();

   function onSubmit(e) {
      e.preventDefault();
      inputValue && setItems([...items, inputValue]);      
   }

   return (
      <form onSubmit={onSubmit}>
         <input onChange={e => setInputValue(e.target.value)} placeholder="Item name" />
         <button>Add item</button>
      </form>
   );
}

Over to the next piece of state.

The items state, i.e. an array holding all the items of the rendered <ol> element, might seem as if it could go inside <List>. But there'll be a problem if we do this.

When a value is input into the input field and then the form submitted, we would want to be able to change the state of <List> in order to get it to be re-rendered. But this ain't going to be possible! We can't change the state of an element from a parent component, at least no without some effort.

So what could we do in this case?

Well, we need to lift the state up from the List component to the containing App component. In this way, the state value items, along with its updater function setItems(), could be provided to InputForm as a prop, which can then call it in order to update the items state datum and thus cause a re-render of the entire App.

Let's do this now.

Here's the App component, defining the items state datum:

function App() {
   const [items, setItems] = useState([]);

   return <>
      <InputForm items={items} setItems={setItems} />
      <List items={items} />
   </>
}

And here's InputForm, using the items prop passed on to it:

function InputForm({items, setItems}) {
   const [inputValue, setInputValue] = useState();

   function onSubmit(e) {
      e.preventDefault();
      inputValue && setItems([...items, inputValue]);      
   }

   return (
      <form onSubmit={onSubmit}>
         <input onChange={e => setInputValue(e.target.value)} placeholder="Item name" />
         <button>Add item</button>
      </form>
   );
}

And so is the List component:

function List({items}) {
   return (
      <ol>
         {items.map(item => (
            <li>{item}</li>
         ))}
      </ol>
   );
}

As we run the app, it works flawlessly just as before.

Live Example

Amazing.