tutorial // Jul 16, 2021
How to Modify an Existing Object in a JavaScript Array
A pattern for modifying a specific object in an array using JavaScript.
Getting Started
For this tutorial, we're going to use the CheatCode Next.js Boilerplate to give us a starting point for our work. First, let's clone a copy:
Terminal
git clone https://github.com/cheatcode/nextjs-boilerplate.git
Next, install the dependencies for the boilerplate:
Terminal
cd nextjs-boilerplate && npm install
Finally, start up the development server:
Terminal
npm run dev
With that, we're ready to get started.
Building a React component for testing
To contextualize our work, we're going to build a simple, class-based React component. This will give us a situation where using the pattern we'll learn will make more sense.
/pages/index.js
import React from "react";
import PropTypes from "prop-types";
import usersFixture from "../lib/users";
class Index extends React.Component {
state = {};
render() {
return <div></div>;
}
}
Index.propTypes = {};
export default Index;
Here, we're just creating the scaffold for a class component in React. The part we want to pay attention to is the name of the component Index
and the path for that file /pages/index.js
. Because we're using Next.js, here, we're relying on the Next.js router by placing our component file inside of the framework's /pages
directory.
Files and folders here are automatically converted to routes. Because we've placed this in the root of our /pages
folder as index.js
, this will render at the root URL for our application, or, http://localhost:5000/
.
Next, let's take a quick look at that usersFixture
file we've imported up top:
/lib/users.js
const users = [
{
_id: "f91bbFE72aaDDd8c",
emailAddress: "Floyd33@hotmail.com",
name: { first: "Phoebe", last: "Schamberger" },
address: {
streetAddress: "39473 David Mill",
city: "Stammbury",
state: "Michigan",
zipCode: "91802",
},
},
{
_id: "E8c8f6d3fE6761dd",
emailAddress: "Torey.Konopelski@hotmail.com",
name: { first: "Orin", last: "Balistreri" },
address: {
streetAddress: "27846 Collier Roads",
city: "Schneiderton",
state: "Kansas",
zipCode: "49705-7399",
},
},
{
_id: "Cd9caEcb4fB1D558",
emailAddress: "Frederic.Kovacek@yahoo.com",
name: { first: "Chanelle", last: "Oberbrunner" },
address: {
streetAddress: "638 Fadel Cliffs",
city: "Lake Thorahaven",
state: "West Virginia",
zipCode: "12349-0480",
},
},
{
_id: "BAf1DcEec4b4DBAc",
emailAddress: "Ottis_Hansen@hotmail.com",
name: { first: "Briana", last: "White" },
address: {
streetAddress: "0540 Brown Meadow",
city: "Port Jerad",
state: "Oklahoma",
zipCode: "14368",
},
},
{
_id: "1c4E8Aa24c37cBFA",
emailAddress: "Uriah49@gmail.com",
name: { first: "Vidal", last: "Stokes" },
address: {
streetAddress: "31028 Marquardt Forest",
city: "North Bethany",
state: "Indiana",
zipCode: "32632",
},
},
];
export default users;
Here, we have a static list of "users" (these are made up of fake data). Our goal is to load this array of users into our component and then make changes to the objects in the array via JavaScript.
/pages/index.js
import React from "react";
import PropTypes from "prop-types";
import usersFixture from "../lib/users";
class Index extends React.Component {
state = {
users: usersFixture,
};
render() {
const { users } = this.state;
return (
<div>
<header className="page-header">
<h4>Test</h4>
</header>
<div className="responsive-table">
<table className="table align-middle">
<thead>
<tr>
<th>Name</th>
<th>Email Address</th>
<th>Address</th>
<th />
</tr>
</thead>
<tbody>
{users.map(({ _id, name, emailAddress, address }) => {
return (
<tr key={_id}>
<td>
{name?.first} {name?.last}
</td>
<td>{emailAddress}</td>
<td>
{address?.streetAddress} {address?.city}, {address?.state}{" "}
{address?.zipCode}
</td>
<td>
<button
disabled={editingUser}
className="btn btn-primary"
onClick={() => {
this.setState({ editingUser: _id });
}}
>
Edit
</button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
);
}
}
Index.propTypes = {};
export default Index;
Back in our component, now, we've taken the usersFixture
we imported earlier and are setting on our component's state
value as users
. Down in the render()
function, we've returned some HTML to render out our users list in a table. Here, the CSS class names you see are taken from the Bootstrap CSS framework. The usage of these classes here doesn't impact our actual work—they're just used for presentation.
The part we care about is when we .map()
over the users
value we placed onto state (again, this is our static array of user objects). Inside of our render()
method, we use JavaScript destructuring to "pluck off" users
from this.state
and then in the returned HTML markup (technically, JSX which is a React-specific language that looks like HTML) we render a <table></table>
with our users
being listed out in the body.
For the "listing," we keep things simple. Here, we render out a <tr></tr>
table row for each user, displaying their name
, emailAddress
, and physical address
. Again, these values are just test data to help us contextualize our work modifying objects in an array.
Finally, for each user, we've added a <button></button>
which when clicked will set that user as the editingUser
on state. Here, we pass the user's _id
(their unique ID in our "database") to say "we're currently editing the user with this _id
.
Next, let's wire up that editing process.
Editing a user object in an array
With our base component set, now, let's add in the editing functionality we hinted at above:
/pages/index.js
import React from "react";
import PropTypes from "prop-types";
import usersFixture from "../lib/users";
class Index extends React.Component {
state = {
editingUser: null,
users: usersFixture,
};
renderUserEditor = () => {
const { editingUser, users } = this.state;
const user = users.find(({ _id }) => _id === editingUser);
return (
<div
className="edit-user"
style={{
border: "1px solid #ddd",
padding: "20px",
borderRadius: "3px",
marginTop: "40px",
marginBottom: "40px",
}}
>
<form onSubmit={this.handleUpdateUser}>
<div className="row">
<div className="col-xs-12 col-sm-3">
<div className="mb-3">
<label className="form-label">First Name</label>
<input
type="text"
className="form-control"
defaultValue={user?.name?.first}
name="firstName"
/>
</div>
</div>
<div className="col-xs-12 col-sm-3">
<div className="mb-3">
<label className="form-label">Last Name</label>
<input
type="text"
className="form-control"
defaultValue={user?.name?.last}
name="lastName"
/>
</div>
</div>
<div className="col-xs-12 col-sm-6">
<div className="mb-3">
<label className="form-label">Email Address</label>
<input
type="text"
className="form-control"
defaultValue={user?.emailAddress}
name="emailAddress"
/>
</div>
</div>
</div>
<div className="row">
<div className="col-xs-12 col-sm-5">
<label className="form-label">Street Address</label>
<input
disabled
type="text"
className="form-control"
defaultValue={user?.address?.streetAddress}
name="streetAddress"
/>
</div>
<div className="col-xs-12 col-sm-3">
<label className="form-label">City</label>
<input
disabled
type="text"
className="form-control"
defaultValue={user?.address?.city}
name="city"
/>
</div>
<div className="col-xs-12 col-sm-2">
<label className="form-label">State</label>
<input
disabled
type="text"
className="form-control"
defaultValue={user?.address?.state}
name="state"
/>
</div>
<div className="col-xs-12 col-sm-2">
<label className="form-label">Zip Code</label>
<input
disabled
type="text"
className="form-control"
defaultValue={user?.address?.zipCode}
name="zipCode"
/>
</div>
</div>
<footer className="mt-4">
<button type="submit" className="btn btn-success">
Save
</button>
<button
type="button"
className="btn btn-default"
onClick={() => this.setState({ editingUser: null })}
>
Cancel
</button>
</footer>
</form>
</div>
);
};
render() {
const { editingUser, users } = this.state;
return (
<div>
<header className="page-header">
<h4>Test</h4>
</header>
{editingUser && this.renderUserEditor()}
<div className="responsive-table">
<table className="table align-middle">
<thead>
<tr>
<th>Name</th>
<th>Email Address</th>
<th>Address</th>
<th />
</tr>
</thead>
<tbody>
{users.map(({ _id, name, emailAddress, address }) => { ... })
</tbody>
</table>
</div>
</div>
);
}
}
Index.propTypes = {};
export default Index;
Going a bit further, now, we've added editingUser
and set it to null
on our default state
object at that top of our component class. Next, in our render()
function, we've added a call to this.renderUserEditor()
and have added in the function. The idea here is that, when we click on the "Edit" button for a user, we'll set their _id
on state (taken from their user object in the users
array) and then simultaneously toggle the rendering of the user editor and disable all of the edit buttons for users until the user editor is closed (either by saving changes or canceling the changes).
Assuming we have an editingUser
set and renderUserEditor()
has been called, looking at that function, the part we care about is the top:
const { editingUser, users } = this.state;
const user = users.find(({ _id }) => _id === editingUser);
Remember: we're dealing with a static array of users on state. Instead of fetching data from a server, here, we're saying "pluck off the editingUser
and users
array from state and then use a JavaScript .find()
on the users
array to find the user who has an _id
matching the editingUser
we set on to state." So, when we click a user's "Edit" button, they will become the user
that we retrieve here.
Once retrieved inside of renderUserEditor()
, we render a form that will be responsible for allowing us to make changes to that user. Here, we can see that our form—again, using Bootstrap CSS to clean up our presentation—lists out each of the fields available in the user object as inputs with their defaultValue
set to that field's value on the user
object. To keep things simple, we're only allowing edits on the name.first
, name.last
, and emailAddress
for the user; the other fields are disabled.
Two more things. First, at the bottom of renderUserEditor()
, we return a <footer></footer>
with two buttons, a "Save" button and a "Cancel" button. The "Cancel" button here is responsible for clearing out the editingUser
on state when it's clicked (remember, this toggles the rendering of the user editor and the disabled state of the edit buttons for users in our list). The more important button, "Save," is set to a type="submit"
, which means that when clicked it will trigger the onSubmit
event for the <form></form>
that's wrapping it.
Here, we can see that <form></form>
has an onSubmit
set to a function this.handleUpdateUser
. Let's wire that function up now and see how it plays into modifying our array.
/pages/index.js
import React from "react";
import PropTypes from "prop-types";
import usersFixture from "../lib/users";
class Index extends React.Component {
state = {
editingUser: null,
users: usersFixture,
};
handleUpdateUser = (event) => {
event.preventDefault();
const { editingUser, users } = this.state;
const updatedUsers = [...users];
let userToUpdate = updatedUsers.find(({ _id }) => _id === editingUser);
if (userToUpdate) {
userToUpdate.name = {
first: event.target.firstName.value,
last: event.target.lastName.value,
};
userToUpdate.emailAddress = event.target.emailAddress.value;
}
this.setState({ users: updatedUsers, editingUser: null });
};
renderUserEditor = () => {
const { editingUser, users } = this.state;
const user = users.find(({ _id }) => _id === editingUser);
return (
<div
className="edit-user"
style={{
border: "1px solid #ddd",
padding: "20px",
borderRadius: "3px",
marginTop: "40px",
marginBottom: "40px",
}}
>
<form onSubmit={this.handleUpdateUser}>
...
</form>
</div>
);
};
render() {
const { editingUser, users } = this.state;
return (
<div>
<header className="page-header">
<h4>Test</h4>
</header>
{editingUser && this.renderUserEditor()}
<div className="responsive-table">
<table className="table align-middle">
<thead>
<tr>
<th>Name</th>
<th>Email Address</th>
<th>Address</th>
<th />
</tr>
</thead>
<tbody>
{users.map(({ _id, name, emailAddress, address }) => { ... })}
</tbody>
</table>
</div>
</div>
);
}
}
Index.propTypes = {};
export default Index;
Inside of our new handleUpdateUser()
function, first, we take in the submit event
as an argument and immediately call to its .preventDefault()
method. This is important as we don't want our submit event to trigger a browser refresh—this stops it.
Next, we see something similar to what we saw in renderUserEditor()
. This is the part we care about in this tutorial. Here, we're plucking off the editingUser
and users
array from this.state
again. Remember, our goal is to edit an object that exists in an array. In order to do that, we need to know two things:
- What array are we looking in?
- How do we find the object to update in that array?
Here, we'll use the _id
we set on editingUser
when we clicked the "Edit" button next to one of our users. Now, again, we need to stress that our goal is to edit an object as it exists in an array. In this example, the array is our users
array.
First, before we "find" our user, we create a copy of the users
array on state (it's beyond the scope of this tutorial but the golden rule in React is that you don't want to mutate state values directly) with [...users]
. Here, in one line we're saying "create a new array []
and then use the ...
spread operator to "unpack" or copy the contents of users
into that new array." This is the array that we'll modify.
Next, again using a JavaScript .find()
on our new updatedUsers
array, we run the same test we used earlier to say "find us a user with an _id
that matches editingUser
." Assuming that we do find that user, we start to make changes to them. Here, we're setting the name
object and the emailAddress
field on the userToUpdate
.
Though it may not look like it, because we used a JavaScript .find()
here, we're actually modifying the matching userToUpdate
object as it exists in the updatedUsers
array in memory. What this means is that even though our code is making changes to userToUpdate
, ultimately, it's making changes to updatedUsers
.
Once these changes are complete (here, we're just setting the aforementioned values to the corresponding inputs in our form using event.target.<fieldName>.value
where <fieldName>
is the name=""
attribute on the input), we overwrite our users
value on state with this.setState()
, passing our updatedUsers
array.
The end result? We'll see our users
list update in our table, successfully demonstrating that we updated an object inside of an array.
Wrapping Up
In this tutorial, we learned how to modify an object in a JavaScript array. To contextualize our work, we built a React component that modified a list of users in an array on state and then set it back on state, rendering the updated list in an HTML table.