tutorial // Jul 08, 2021

How to Build a Select All List Component in React with Next.js

How to build a select-all list component using React in a Next.js app.

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:

  1. Select and deselect individual users one at a time.
  2. 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:

  1. A user has not had their checkbox checked and needs to be added to the selectedUsers array.
  2. 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 _ids 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.

Written By
Ryan Glover

Ryan Glover

CEO/CTO @ CheatCode