Icon of sun for "Light Mode" theme

States of React State (Part 1)

Working with State in Class Components vs. Functional Components

Image of Triangles and circles in a firewood stack by Dan Burton by UnsplashImage of Triangles and circles in a firewood stack by Dan Burton by Unsplash

Photo by Dan Burton on Unsplash

React Hooks have been around for about a year now and they’re a fantastic addition to the library. For those of you new to hooks, they allow you to hold state and do things like key into lifecycle methods from within lighter-weight functional components instead of having to use class components. If you had to choose one type of component to invest your time into learning it’s probably safe to say that it’s functional components with hooks, but class components are still very relevant today since there are sure to be many apps written in classes that work perfectly well and don’t need to change, and others that will probably continue to make transitions to hooks for years to come (hooks can be incrementally adopted so it's not like you have to only use classes or hooks), so at least for the time-being, React developers should be comfortable working with both types of components. This can of course be a little burdensome for newer React developers who have to simultaneously learn the two different formats, so in this pair of blog posts I wanted to briefly go over state in React, highlight some of the differences between working with functional and class components, and also show you some of the common patterns used when setting state in React. These posts will primarily be aimed toward newer React and JavaScript developers, but they should also serve as a handy reference for anybody trying to remember how state works in each type of component and how to set state in its various forms. Note that I won’t be discussing more advanced forms of state management like React Context or Redux-like patterns, nor will I discuss component lifecycle in very much detail, but keep in mind that these are also important topics to learn when considering how to best manage React state so you should learn about them as well.
State, in programming, can refer to a number of different things, but it generally refers to the current condition, shape, or order of part of a program. In React, state is where you keep your variables and the values they hold, which React then uses to determine how to render the user interface and interact with the rest of your app. React state has an initial, default value that will typically later be updated any number of times as the user interacts with the page. Once state is updated (i.e. “set”), other components and parts of the site will react to those changes, whether that includes something like rendering new content to the page, fetching new data from an API, or updating a record being stored in a database. Before hooks were released, React state had to be stored in ES6 class components, which are great but they can often be bulky with a lot going on. Now that we have the useState hook we can manage state in more simplified functional components and generally keep our front-end components easier to use, reuse, reason about, and test.
The syntax you use for working with state in classes and functional components is different, but many of the general principles are the same, so once you learn one it’s pretty easy to apply those principles to the other. The types of code patterns you use for setting state will apply to either form, which is nice, but to further add confusion for newer React developers, there are many different ways to set state depending on if you’re using newer ES6 syntax like the spread operator or more traditional methods. The good news is that these patterns are really interchangeable between the two types of components as long as you have a tool like Babel (apps created with Create React app use Babel by default) that converts the more modern JavaScript syntax into a form that older browsers can understand.
When working with state in React it’s usually recommended that you adhere to keeping your state immutable, which means that you shouldn’t directly update state objects once they’ve been created. Immutability has a host of potential benefits but one of the primary reasons React places emphasis on it is because it helps React efficiently determine when it needs to re-render components. Primitive JavaScript values like strings, numbers, and booleans, are already immutable, but when you’re working with state that’s stored in a referenced data-structure like objects and arrays (arrays are just a type of object) you should try to ensure that you first make a copy of the data, then make modifications to that copy, and then set the new state with the modified copy. The way you go about immutably updating state often requires more care than you ordinarily would use if you were just modifying an object or array directly. All of the code examples in these blog posts will have immutable state updates, and in the next post in this series I’ll be focusing more on showing you some of the patterns you tend to see when setting state across different shapes of data that state is commonly held in. For now, I’d like to go over class and functional components and highlight some of the differences.

CLASS COMPONENTS

The following is an example of a simple class component:
1import React, { Component } from "react";
2
3export default class CreateUsername extends Component {
4 constructor(props) {
5 super(props);
6 this.state = {
7 userName: "",
8 };
9 this.handleInputChange = this.handleInputChange.bind(this);
10 }
11
12 handleInputChange(e) {
13 this.setState({
14 userName: e.target.value
15 });
16 }
17
18 render() {
19 return (
20 <input
21 type="text"
22 onChange={this.handleInputChange}
23 value={this.state.userName}
24 />
25 );
26 }
27}
info iconI’m explicitly importing Component from the React library by name, but you can also just use it without explicitly importing it by adding the Component property onto the main React import like React.Component
In this example I have one state value called userName and when the user types text into the input field the handleInputChange function handles taking that value and updating the userName state. Note that I’ve defined the state inside of the class constructor, which is a commonly used way of writing a class component, but it should be noted that you’d really only use a constructor when you need to bind class methods but you don’t want to use auto-binding ES6 arrow functions (I am using traditional binding in the example). The constructor is often used to initialize state (this.state = …) but you technically don’t need the constructor to do this as long as Babel is set up to handle compilation and add the constructor “behind-the-scenes”. This next example shows how the component above would look without a constructor:
1import React, { Component } from "react";
2
3export default class CreateUsername extends Component {
4 state = {
5 userName: ""
6 };
7
8 handleInputChange = (e) => {
9 this.setState({
10 userName: e.target.value
11 });
12 };
13
14 render() {
15 return (
16 <input
17 type="text"
18 onChange={this.handleInputChange}
19 value={this.state.userName}
20 />
21 );
22 }
23}
Notice that the only things that have changed now that the constructor is gone are the state, which is now a property directly on the class (this “class property” syntax is currently in the final stages of refinement before it will formally be added to JavaScript but you can use it now because of tools like Babel), the this keyword is no longer before state, and instead of binding the handleInputChange function in the constructor we use an arrow function to bind it.
When it comes to setting state in a class component you’ll typically be calling React’s setState function. The syntax is setState(stateUpdater, [optionalCallback]). I’ll go over the different forms of using the API shortly, but the most common way to use it is to pass it the stateUpdater argument an object with the properties you want to update. In the following example, we’re passing it { userName: e.target.value } so the userName state value will be updated to hold the result of what the user typed into the input field:
1handleInputChange = (e) => {
2 this.setState({
3 userName: e.target.value
4 });
5};
A common mistake to be aware of in React is that if you attempt to set state directly without using the setState function (e.g. in a class component doing something like this.state.userName = ‘Jason’), it will still technically update the state, but since you didn’t use setState React won’t know that anything was updated so your components won’t rerender to show updated state values and you’ll violate the principles of immutability that I mentioned earlier. The only time it’s OK to not use setState is when initializing state with a value when the component first loads:
1this.state = {
2 userName: ‘Jason’
3}
Another thing to be mindful of when setting state in React is that it’s an asynchronous operation, so the state isn’t always guaranteed to be updated before you attempt to do something else in your code where you’re relying on the state being updated, particularly when you’re making multiple setState calls in close succession. For instance, if you make two back-to-back calls, React might batch the first call with the second so that once the second runs, the first state call may not have updated state yet. More often than not you'll start out just using the first form of setState where you pass it an object with the updated state value, but for best practices, there are a couple of different ways you can take advantage of the API to help ensure that your state has been updated before you act on it. The first way is that you can pass in a function as the first argument to setState (this is what updaterFunction represents in the syntax above). React will then pass this updaterFunction the previous state as the first parameter (you will often see this parameter referred to as “previous state”, “current state”, or just “state”, but in the context of the they all mean the same thing: state before the current setState call has updated it). You can then calculate the new state value based on the previous state, and then you just need to ensure that this function returns an object with the new state value. A common example of employing the updaterFunction is a counter component that keeps track of a count and uses setState to increment the count based on the previous count:
1this.setState((previousState) => {
2 return {
3 count: ++previousState.count
4 }
5});
The API also allows you to pass it an optional callback function as the second argument, and this function will run once the state has been updated:
1this.setState({
2 stateValue: ‘updated’
3}, this.optionalCallback());
When learning about React classes you may also see better developers than myself recommend using instead the componentDidUpdate lifecycle method API instead of the callback to setState. I’ve used both approaches and honestly I'm not very familiar with the differences between the two but I did find this Stack Overflow answer that seems to do a good job of explaining some of the nuances and edge cases where it’s more appropriate to use one over the other. As I mentioned before, when you're a newer React developer than isn't as familiar with all of the different React APIs, you can often get away with just setting the state object directly without worrying too much about if you need to use the updaterFunction, optionalCallback, or componentDidUpdate, but if you ever find yourself in a place where you’re experiencing problems with your state not updating correctly even though you’ve debugged the hell out of everything and it all looks correct, then you could very well have an issue with setState being a mischievous little asynchronous rascal.

FUNCTIONAL COMPONENTS

Now that we’ve discussed working with state in class components we can move our discussion to working with functional components and the useState hook. Before hooks were introduced, functional components were usually pretty basic components (often affectionately referred to as “dumb components”) that were unable to hold state and were only responsible for the presentation and rendering the user interface based on state passed down as props from parent components. Now that we have the useState hook we still keep all the benefits of functional components but we can also store state in them instead of being using class components. To start using the useState hook you must first import it:
1import React, { useState } from ‘react’;
info iconSimilarly to the Component property of React, instead of explicitly importing useState by name you could also just use React.useState
Once you have access to useState you can write a basic JavaScript function, usually in the form of a regular function declaration or an arrow function, and then you can add the useState hook to setup your state with the name of the state variable, the name of the function that updates the corresponding variable, and the initial value for the variable. The syntax is const [stateValue, setStateValue] = useState(initialValue);. This syntax can seem weird at first for newer JavaScript developers but it’s just using ES6 array destructuring to return an array with two values from the useState function call: the first index of the returned array is the state value while the second is the function you use to update the first value. The function from the second index is similar to the setState function that you use with class components except now each state variable is declared from a separate useState call and gets its own dedicated state-setting function. Since these returned values are being destructured you can name them anything you want, but the convention is to name the state-setting functions the same as the corresponding state values but prefixed with “set”. For instance, if you have a state variable named “age” then you should try to name the function “setAge”. Here’s a comparison between defining some state variables in a class versus a function:
1// functional component
2const [valueA, setValueA] = useState(‘’);
3const [valueB, setValueB] = useState({});
4
5// class component
6this.state = {
7 valueA: ‘’,
8 valueB: {}
9}
info iconRegardless of if you use a class component or a functional component you can track as many state variables as you want, but you should generally try to limit the amount for each component so you can keep your components smaller so that they’re easier to reason about, test, and reuse.
Here’s an example of writing the CreateUsername component from the class component examples earlier but instead with a functional component and the useState hook:
1import React, { useState } from "react";
2
3export default function CreateUsername() {
4 const [userName, setUserName] = useState("");
5
6 const handleInputChange = (e) => {
7 setUserName(e.target.value);
8 }
9
10 return (
11 <input
12 type="text"
13 onChange={handleInputChange}
14 value={userName}
15 />
16 );
17}
Note some of the key differences between the class and functional forms:
  • there’s no longer any constructor function
  • instead of a generic state object that holds the userName state value as a property you just declare the userName state value directly as its own variable
  • instead of defining the handleInputChange method with just the name of the method we declare it as an arrow function expression, although you can also define it as a function declaration like function handleInputChange(e) {…}
  • since we’re no longer working with classes there are no references to the this keyword
  • there’s no longer any render function
You can see that things are quite a bit cleaner with functional components, but the difference becomes even more apparent with more real-world components that have a lot more going on than this basic example.
In the previous examples, we’ve just been updating state with a basic string value but it’s important to keep in mind that when you’re updating an object, class components will shallowly merge updates into the state object for you while with functional components you need manually merge state objects or else your results might not be what you expect:
1// class component
2this.state = {
3 a: ‘initial a’,
4 b: ‘initial b’
5}
6
7this.setState({
8 a: ‘updated a’
9});
10
11console.log(this.state); // {a: ‘updated a’, b: ‘initial b’}
12// React merges the new `a` value into the
13// existing state and leaves `b` alone
14
15
16// functional component
17const [myState, setMyState] = useState({
18 a: ‘initial a’,
19 b: ‘initial b’
20});
21
22setMyState({a: ‘updated a’});
23console.log(myState); // {a: ‘updated a’}
24// React doesn’t merge the objects so
25// we lose the `b` property
To ensure that you keep things immutable and merge the objects without losing properties you should use the spread operator or JavaScript’s Object.assign method (we’ll see more examples in the next blog post). Just like with the class-based setState function, your hook-based state-setting functions also accept an updaterFunction as the first argument, and when you’re merging objects it’s usually advisable to use that function so that you can reliably have access to the current state values:
1setMyObject((previousObj) => {…previousObj, a: ‘updated value’});
2
3setMyArray((previousArr) => […previousArr, ‘updated value’];
Often when developers go from working with state in classes to functional components it seems natural to jam all of your state values into a single state object, similarly to the one state object that you have in classes, but with hooks you generally will want create separate variables that make separate calls to useState (remember you can have more than one state value in functional components). You can certainly emulate the state object from class components by just using a state object in your functional components, but if you do then it should really only be for state values that are directly related (e.g. address field: const [address, setAddress] = useState({ street: ‘’, city: ‘’, state: ‘’, zip: ‘’ });) and you just have to remember that the onus is on you to merge the data when updating.
Earlier I mentioned that the useState state-setting functions accept an updaterFunction similarly to how setState does, but unlike with setState there’s no second argument that accepts a callback that runs after the state has guaranteed to have been updated. That being said, you can accomplish the same thing by turning to useState’s pal, the useEffect hook. Without going into very much detail, useEffect allows you to perform side-effects so that you can accomplish the same types of things that you can with a the more useful lifecycle methods that are specifically used with class components. All you really need to know about useEffect in order to emulate the behavior of a class component’s setState callback or the componentDidMount lifecycle method, is that the useEffect hook accepts an array of dependencies as the last argument, and when React sees that a value in that array has changed it will rerun the corresponding useEffect. This means you can put a state value in this array to ensure that code within the useEffect hook runs once that state is updated. For instance, in this example when the isLoggedIn state changes to either true or back to false then the useEffect hook will run along with the appropriate conditional code within:
1import React, { useState, useEffect } from 'react';
2
3function UserLogin(props) {
4 const [isLoggedIn, setIsLoggedIn] = useState(false);
5 useEffect(() => {
6 if (isLoggedIn === true) {
7 renderUserDashboard();
8 } else {
9 renderLoginForm();
10 }
11 }, [isLoggedIn]);
12}
The last thing I wanted to mention about useState, and React hooks in general, is that you should only call hooks from the “top-level” of your React functions, which means that you shouldn’t use hooks inside of loops, conditionals (if/else, etc.) or nested functions, and you also shouldn’t call hooks from regular JavaScript functions, but rather only from React functional components or custom React hook functions (see the docs for more info).

CONCLUSION

That’s it for this post, but I hope to see you back soon for the next post where I’ll be focusing more on some of the patterns you can use when setting state across different shapes of data.