How to Build a Select All List Component in React with Next.js
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, clone the boilerplate:
Terminal
git clone https://github.com/cheatcode/nextjs-boilerplate.git
Next, cd
into the project and install its dependencies:
Terminal
cd nextjs-boilerplate && npm install
Next, add the dependency faker
which we'll use to generate some realistic test data for our select-all list:
Terminal
npm i faker
Finally, start up the app:
Terminal
npm run dev
With the app up and running, we're ready to get started.
Building a base component
To get started, first, we're going to create a basic React component. Because our UI is fairly simple, we're going to use the function component pattern in React:
/pages/users/index.js
import React, { useState } from "react";
const Users = () => {
const [users] = useState([]);
const [selectedUsers, setSelectedUsers] = useState([]);
return (
<div className="responsive-table">
<table className="table">
<thead>
<tr>
<th>
<input
type="checkbox"
/>
</th>
<th className="text-left">Name</th>
<th className="text-left">Email Address</th>
<th className="text-center">Last Seen</th>
</tr>
</thead>
<tbody>
{users.map(({ _id, name, emailAddress, lastSeen }) => {
return (
<tr key={_id}>
<td>
<input
type="checkbox"
value={_id}
/>
</td>
<td className="text-left">{name}</td>
<td className="text-left">{emailAddress}</td>
<td className="text-center">{lastSeen}</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
};
Users.propTypes = {};
export default Users;
Here, we're setting up a React component that will render a <table></table>
of users, with each user getting a checkbox that we can use to mark them as selected in the first column of their row. In the <thead></thead>
of the table, we add an <input type="checkbox" />
that will serve as the "select all" checkbox for the entire list.
At the top of our component, we use the useState()
hook function from React to create two dynamic values: a list of users
which we'll render into our <tbody></tbody>
and then a list of selectedUsers
which we'll use to mark the checkboxes in our table to designate which users are currently selected in the UI.
For now, we're setting the default value for these to an empty array (the []
that we pass to useState()
). Next, to actually show some users in our list, we're going to learn how to generate some test data on the fly using the faker
dependency that we installed earlier.
Generating a list of test users
Outside of our Users
component function, let's add a new variable, testUsers
:
/pages/users/index.js
import React, { useState } from "react";
import faker from 'faker';
import { monthDayYearAtTime } from "../../lib/dates";
const testUsers = [...Array(100)].map((item, index) => {
return {
_id: `user_${index}`,
name: faker.name.findName(),
emailAddress: faker.internet.email(),
lastSeen: monthDayYearAtTime(faker.date.past()),
};
});
const Users = () => {
const [users] = useState(testUsers);
const [selectedUsers, setSelectedUsers] = useState([]);
return (
<div className="responsive-table">
<table className="table">
<thead>
<tr>
...
</tr>
</thead>
<tbody>
{users.map(({ _id, name, emailAddress, lastSeen }) => {
return (
<tr key={_id}>
...
</tr>
);
})}
</tbody>
</table>
</div>
);
};
Users.propTypes = {};
export default Users;
At the top of our file, we've imported two things: faker
from the faker
dependency that we installed earlier and a helper function built into the boilerplate we're using monthDayYearAtTime
which will help us generate a human-friendly "last seen" date for each of our users.
Next, we've added a new variable testUsers
which is being set to an empty array []
that contains the statement ...Array(100)
. Here, the ...
is known as the spread operator in JavaScript. This takes the value that immediately follows it—here, Array(100)
—and spreads it out in the parent context, or, in this case our empty array.
The Array(100)
part here is a neat trick using the JavaScript Array
function to create an array of 100 elements. By passing the number 100, we'll get back an array of 100 undefined
values. Each undefined
value is recognized as a value in JavaScript, which means, if we .map()
over that array like we are here, we can treat those undefined
values like placeholders and return a real value in the .map()
function's callback.
Here, we do just that, returning an object to replace the undefined
created by the Array(100)
. On each object, we add an _id
equal to user_
concatenated with the current array index (some number from 0-99). Next, using the faker
library we imported up top, we generate a name for the user with faker.name.findName()
to get back a full name string and an email address with faker.internet.email()
.
Next, we set a lastSeen
timestamp (mostly for fun and to make our example more concrete), again using faker
to get a random date in the past with faker.date.past()
. We pass that random date to the monthDayYearAtTime()
function we imported up top to convert the date we generate into a human-readable string.
Finally, with our testUsers
value generated, we replace the []
that we passed to the useState()
definition for our users
list with our new testUsers
variable. Now, when we render our component, we should see a list of our test users on-screen.
Selecting individual users
Now that we have a user list, we can implement user selection. Our goal is to make it so that we can do one of two things:
- Select and deselect individual users one at a time.
- Select or deselect all users at once.
To handle the first option, we're going to modify the <input type="checkbox" />
to include an onChange
handler and a way to set its checked
status:
/pages/users/index.js
import React, { useState } from "react";
import faker from "faker";
import { monthDayYearAtTime } from "../../lib/dates";
const testUsers = [...Array(100)].map((item, index) => { ... });
const Users = () => {
const [users] = useState(testUsers);
const [selectedUsers, setSelectedUsers] = useState([]);
const handleSelectUser = (event) => {};
return (
<div className="responsive-table">
<table className="table">
<thead>
...
</thead>
<tbody>
{users.map(({ _id, name, emailAddress, lastSeen }) => {
return (
<tr key={_id}>
<td>
<input
type="checkbox"
value={_id}
checked={selectedUsers.includes(_id)}
onChange={handleSelectUser}
/>
</td>
...
</tr>
);
})}
</tbody>
</table>
</div>
);
};
Users.propTypes = {};
export default Users;
Here, looking at the <input type="checkbox" />
rendered at the start of each user row, we've added two attributes: checked
and onChange
.
For checked
, we set it equal to a JavaScript expression which takes our selectedUsers
state value and, using the JavaScript array .includes()
method, checks for the _id
of the user we're currently mapping over in the list. If their _id
appears in that array, that means they're a selected user, or, that their checkbox should appear "checked."
Next, for the onChange
, we pass a function we've stubbed out just above our return
statement called handleSelectUser
. Whenever the checkbox is checked or unchecked, this function will be called. Let's set it up now to actually handle managing a user's selected status.
/pages/users/index.js
import React, { useState } from "react";
import faker from "faker";
import { monthDayYearAtTime } from "../../lib/dates";
const testUsers = [...Array(100)].map((item, index) => { ... });
const Users = () => {
const [users] = useState(testUsers);
const [selectedUsers, setSelectedUsers] = useState([]);
const handleSelectUser = (event) => {
const userId = event.target.value;
if (!selectedUsers.includes(userId)) {
setSelectedUsers([...selectedUsers, userId]);
} else {
setSelectedUsers(
selectedUsers.filter((selectedUserId) => {
return selectedUserId !== userId;
})
);
}
};
return (
<div className="responsive-table">
<table className="table">
<thead>
...
</thead>
<tbody>
{users.map(({ _id, name, emailAddress, lastSeen }) => {
return (
<tr key={_id}>
<td>
<input
type="checkbox"
value={_id}
checked={selectedUsers.includes(_id)}
onChange={handleSelectUser}
/>
</td>
...
</tr>
);
})}
</tbody>
</table>
</div>
);
};
Users.propTypes = {};
export default Users;
Looking at the handleSelectUser
function, we begin by taking in the onChange
event object as an argument, using it to retrieve the _id
of the user the checkbox being checked belongs to with event.target.value
and store it in a variable const userId
.
Next, our real work. Remember there are two scenarios to consider:
- A user has not had their checkbox checked and needs to be added to the
selectedUsers
array. - A user already has their checkbox checked and needs to be removed from the
selectedUsers
array.
Here, we do just that. For the first case, we check to see if selectedUsers
already .includes()
the userId
we pulled from event.target.value
. If it does not, we use the setSelectedUsers
method we get back from useState()
, passing it an array whose first value is a ...
spread of the existing selectedUsers
and a second value of the userId
we want to add. In turn, we set our array of selectedUsers
back on state, including the userId
we checked.
For the second scenario—here, in the else
statement—we call to setSelectedUsers
again, but this time use the JavaScript .filter()
method to filter our selectedUsers
array, excluding the checked userId
from the array. Here, selectedUserId !== userId
will only evaluate to true
if the userId
does not match the selectedUserId
currently being looped over in the .filter()
.
Because we're passing this to setSelectedUsers()
, we'll get the filtered selectedUsers
set onto state when this runs.
Selecting all users
Next, selecting all users requires a similar approach, but a little bit simpler...
/pages/users/index.js
import React, { useState } from "react";
import faker from "faker";
import { monthDayYearAtTime } from "../../lib/dates";
const testUsers = [...Array(100)].map((item, index) => { ... });
const Users = () => {
const [users] = useState(testUsers);
const [selectedUsers, setSelectedUsers] = useState([]);
const handleSelectAllUsers = () => {
if (selectedUsers.length < users.length) {
setSelectedUsers(users.map(({ _id }) => _id));
} else {
setSelectedUsers([]);
}
};
const handleSelectUser = (event) => { ... };
return (
<div className="responsive-table">
<table className="table">
<thead>
<tr>
<th>
<input
type="checkbox"
checked={selectedUsers.length === users.length}
onChange={handleSelectAllUsers}
/>
</th>
<th className="text-left">Name</th>
<th className="text-left">Email Address</th>
<th className="text-center">Last Seen</th>
</tr>
</thead>
<tbody>
{users.map(({ _id, name, emailAddress, lastSeen }) => {
return (
<tr key={_id}>
...
</tr>
);
})}
</tbody>
</table>
</div>
);
};
Users.propTypes = {};
export default Users;
Because our goal is to select all of our users, first, on the <input type="checkbox" />
in our <thead></thead>
, we set the checked
attribute equal to selectedUsers.length === users.length
. If this is true, that means we've selected all of the users available.
Again, like we saw before, we add an onChange
attribute set to a function, this time defining a new one called handleSelectAllUsers
.
Looking at that function, just like we learned before we have to handle the unchecked -> checked state as well as the checked -> unchecked state. To do it here, for the first case, if selectedUsers.length
is less than users.length
, we setSelectedUsers
equal to a new array created by .map()
ing over our users
array, plucking off the _id
field on each user. This puts all of our user's _id
s into selectedUsers
, which means the selectedUsers.includes(userId)
next to each user will be true, showing the user as checked.
Next, in the else
statement, the inverse here is as easy as calling to setSelectedUsers()
, passing it an empty array []
which signifies that no users are currently selected.
Wrapping up
In this tutorial, we learned how to build a select-all component using React.js. We learned how to define a React component that renders a table of dynamically generated users, with a checkbox for selecting next to each user and a "select all" checkbox at the top of that table.
We also learned how to write two functions: one for selecting and deselecting individual users and one for selecting and deselecting all users.