States of React State (Part 2)
State-Setting Code Patterns
January 9, 2021
Photo by Clark Van Der Beken on Unsplash
In the first post in this pair on React state I gave a general overview of state and discussed some of the key differences when working with state in functional components and class components. In this post, I’m going to be focusing more on some of the common code patterns that you can use when setting state across different shapes of data, including basic primitives, arrays, objects, and arrays of objects. This post will be pretty example-heavy, so in an effort to try and narrow our focus, here are some things to keep in mind about the code examples I’ll be using:
- I’ll exclusively be using functional components with the useState hook but many of the patterns are quite similar when you’re using class components so it generally isn't too difficult to translate.
- Whenever I’m working with state held in objects and arrays I’ll be making immutable updates to the state by making copies of the state before making the updates. I’ll be using the ES6 spread operator to create the copies but in codebases out in the wild I'm sure it’s not uncommon to see copies being made with one of the following, older methods instead:
const objectCopy = Object.assign({}, originalObject);const arrayCopySlice = originalArray.slice();const arrayCopyConcat = [].originalArray.concat();
- I'll be focusing on the different shapes of state data and the code that's responsible for setting the state but I’ll be omitting some of the other component code that would ordinarily be necessary to make things fully functional. There will be some code inside functions where you can assume the functions are being called and passed the parameters shown, and there are others where you can assume that you're operating on values that are being held in state or being passed in as props. It's all really interchangeable and just depends on how you structure your components and if you do inline functions or declare the functions on the component.
- As always in programming, there are many different ways to accomplish the same things that I'll be showing you, particularly when working with state in objects and arrays. The examples I'll be using will mostly use more modern syntax and more concise code patterns.
With that out of the way, let's start out working with basic primitive values.
PRIMITIVES
There are a number of different primitive value types in JavaScript, but for this article, we’ll just be focusing on strings, numbers, and booleans since these are the primary primitive types used. Primitive values are typically easiest to work with because they’re naturally immutable, which means that you can’t change a primitive value once it’s been assigned to a variable (although you can typically can reassign a variable to a new value). For our examples, let’s assume we’ve initialized the following string, number, and boolean type state variables:
// stringconst [userName, setUserName] = useState('');// numberconst [userAge, setUserAge] = useState('');// booleanconst [isLoggedIn, setIsLoggedIn] = useState(true);
The initial userAge state variable is defined as an empty string because we’re using that state value as the input field’s value (what React calls a Controlled Component) so it needs to be a string in the browser, however, you’ll often need to convert input numbers from strings to numbers, particularly if you want to be able to use the number in calculations or when saving the number to a database. Common exceptions to this are phone numbers and unique IDs, which you’ll typically want as strings.
At the end of the day, whenever you want to set state, you just need to call the corresponding state-setting function with the value you want to set the state to, so for our basic primitives it's as easy as doing something like this:
setUserName('Jason');setUserAge(36);setIsLoggedIn(false);
If you have some additional logic that you need to perform before setting the new state value you might prefer to make your state-setting call inside of a separate function, but in many situations, like when you’re just setting state directly from user input without any client-side validation, it might be perfectly fine to just set the state from inline-functions defined directly on the element that is firing the related event. For instance:
<inputtype='text'value={userName}placeholder='Enter name'onChange={(e) => setUserName(e.target.value)}/><inputtype='number'value={userAge}placeholder='Enter age'onChange={(e) => setUserAge(parseInt(e.target.value))}/><button onClick={() => setIsLoggedIn(false)}>Log Out</button>
If you wanted to handle all the logic and setting of these pieces of state from separate functions then you could do this for the event handlers instead:
onChange={handleSetUserName}onChange={handleSetUserAge}onClick={handleLogOut}
In JavaScript any time you have a function that is handling an event, whether it’s an inline function or a reference to a function that exists elsewhere, the function will automatically be passed an event object that contains all the information about the type of event and where it occurred. In the inline functions above, you can see I’m referencing the event object as e, but even in this second set of examples where we’re just referencing the functions defined elsewhere, we’re not explicitly passing the functions anything but those functions will still automatically be passed the event object. If you need to pass the function a piece of data (e.g. an ID so you know what to target and update) then you’ll probably want to use an arrow function or a pre-ES6 anonymous function:
// ES6 arrow function:onChange={(e) => handleSetUserName(e, userId)}// good ol' anonymous function:onChange={function(e) {handleSetUserName(e, userId);}}
In situations where you don't need the event object you can omit it. For instance, if you just wanted to pass in the examples above you would just delete e and only pass userId to the function.
While you ultimately just need to pass your state-setting function the value that you want to update a state variable to, note that you can also put JavaScript expressions inside of your state-setting functions so that you can handle logic and calculate the new state directly inside of that function, which helps keep your code self-contained and concise. Here are some examples:
const [isTrue, toggleIsTrue] = useState(false);const [counter, setCounter] = useState(0);<button onClick={() => toggleIsTrue(isTrue => !isTrue)}>Toggle Truthiness</button><button onClick={() => setCounter(count => count + 1)}>+</button><button onClick={() => setCounter(count => count - 1)}>-</button>
Note that the examples above use a callback function, often referred to as the “updater function”, which is a function the React API gives you access to inside of the state-setting function. The argument React passes to this updater function is the previous state value for the corresponding state variable so that you can use that value to ensure that your state is getting updated correctly when the new state is calculated based on the prior state. Instead of isTrue and count you could technically name the variables anything you want and that variable will represent the previous state value before the current state-setting call updates it. You can use the updater function anywhere you’re setting state, and you’re not required to use it when using inline state-setting functions; it’s just a best practice for components like counters and togglers where you need to rely on the state being accurate before making updates. Without the updater function, you could write something like the examples below and it will often still work just fine, but due to the asynchronous nature of how React batches state-setting calls, you’re susceptible to getting bugs that cause inaccurate state values when the new state value is dependent on the prior state value being accurate.
<button onClick={() => toggleIsTrue(isTrue ? false : true)}>Toggle Truthiness</button><button onClick={() => setCounter(counter + 1)}>+</button><button onClick={() => setCounter(counter - 1)}>-</button>
In these examples isTrue and counter refer directly to the variables that you defined in the useState calls as opposed to arguments passed in by React that reflect the guaranteed updated state.
If you have more logic and calculation that needs to occur before you set state you can also add curly brackets and break the inline function onto multiple lines like below, but you’ll probably want to be cautious when doing this because it can muddy up your JSX code quickly, making your code difficult to read and less reusable:
<button onClick={() => {// do some calculations to// determine increment amount…// then set statesetCounter(count => count + incrementAmount)}}>Increment</button>
OBJECTS
Back before we had React Hooks, each class component had one state object that would hold all the necessary state properties for that component and its related components. With React Hooks it’s generally recommended that you separate each state property out into its own dedicated variable, but if you want to you can still store multiple state properties in one object in a similar fashion to class components. For example, let’s make a User component that stores a userName string value and an array of friends:
function User() {const [userProfile, setUserProfile] = useState({userName: '',email: '',friends: []});// If you separate out each variable// you'd have something like this:// const [userName, setUserName] = useState(‘’);// const [email, setEmail] = useState(‘’);// const [friends, setFriends] = useState([]);}
Update Existing Primitive Object Value / Add New Primitive Value
function handleUpdateProperty(email, gender) {setUserProfile({...userProfile,// update existing propertyemail: email// add new propertygender: gender});}
Unlike when working with state in class components, when you’re updating a state object in functional components you need to manually merge updates. In addition to preserving immutability, this is why I use the spread operator in the object above; it copies over all of the existing properties of the state object and adds/overrides the additional properties that are passed to the function. When you’re setting state in class components you only need to specify the properties that are being updated.
Here’s a more reusable function that is similar to the functions above, but it allows you to dynamically make updates based on values passed into the function:
function handleUpdateProperty(key, value) {setUserProfile({...userProfile,[key]: value});}
Add something to an array nested one-level down in object
function handleAddFriend(friend) {setUserProfile({...userProfile,friends: [...userProfile.friends,friend]});}
You can also do something similar to the pattern above with nested objects.
Delete object property
function handleDeleteProperty(propertyName) {const stateCopy = {...userProfile};delete stateCopy[propertyName];setUserProfile(stateCopy);}
ARRAYS
When working with arrays outside of React most JavaScript developers are comfortable using built-in array methods such as push, pop, and splice, but the problem with using methods like these in React is that they mutate (i.e. change) the original array. so now that we’ve moved on to working with arrays and objects, which are referenced, it’s necessary for us to ensure that we keep our state data immutable by making copies of the data before making updates to it. To achieve this we’ll be using the spread operator and array methods like slice, map, and filter that create a new array instead of mutating the original. For our examples let’s work with an array of users:
const [users, setUsers] = useState([]);
Add to end of array
setUsers([...users, newUser]);
Add to beginning of array
setUsers([newUser, ...users]);
Add to a specific index of array
setUsers([...users.slice(0, indexToAdd),newUser,...users.slice(indexToAdd)]);
Update item at a specific index
setUsers([...users.slice(0, indexToUpdate),updatedUser,...users.slice(indexToUpdate + 1)]);
Remove first item of array
setUsers([...users.slice(1)]);
Remove last item of array
setUsers([...users.slice(0, users.length - 1)]);
Remove item at a specific index
setUsers([...users.slice(0, indexToRemove),...users.slice(indexToRemove + 1)]);
All of the patterns above can be used on either arrays of primitive values or arrays of objects (arrays of pretty much anything, really, but primitives and objects are probably most common), but for the examples below we'll just work with arrays of objects. Let's assume these objects store state properties of id, userName, and email. Since working with arrays of objects adds some code and complexity I’ll have each of the examples in a dedicated function with all of the associated logic:
Check if an object exists before adding
function handleAddUniqueUser(user) {// here I’m using the `some` method to determine// if at least one object in the array matches// the user’s ID:const userExists = users.some(_user => {return _user.id === user.id);}if (!userExists) {setUsers(user);} else {return 'An account for this user already exists!'}}
Update an object property based on ID
function handleSaveUpdatedName(userId, name) {const updatedUsers = users.map(user => {if (user.id === userId) {return {...user,userName: userName// since the `userName` property name is the// same as the value you could also// just list `userName` once without the colon}}return user;})setUsers(updatedUsers);}
Delete an object from the array
function handleDeleteUser(userId) {const updatedUsers = users.filter(user => {return user.id !== userId})setUsers(updatedUsers);}
CONCLUSION
That’s all I wanted to discuss about React state for now, but I hope, dear reader, that you gleaned something useful from this pair of posts. Thanks for reading and may your powers of React proliferate in 2021 and beyond!