Pagination Component

Divide random fetched users into pages.


Environment

Use the environment you are most comfortable with. I recommend using create-react-app to create a local version of the project so that you can inspect the DOM easily from your browser when debugging. Alternatively, you can work from a blank React template in CodeSandbox. Starter code is provided after the problem specification.

Specification

Write two functional components to display a list broken into pages that are traversable via a list of clickable page numbers below the list. The first component — the parent component — should fetch a list of users and conditionally render a loading status or the next component, the actual list generated from the data. The user array fetched in the parent component should be passed to the child component as a prop along with the number of items that should be displayed on each page. The child component should display the first page of items and clickable links to the remaining pages of users. Here is an image of the final product:

The finished component
The finished component

In the image above, the itemsPerPage prop is set to 5. You may fetch users from randomuser.me, a free API that provides random user data. To query the API, issue a get request to https://randomuser.me/api and include the number of users that you would like returned as a query parameter. For example, issuing a get request to the following URL will return a list of 19 users: https://randomuser.me/api?results=19. The solution I provide later on uses the axios library to issue the request, but you may use fetch or any library that you are comfortable with to issue the request. The object returned by the request takes the following form (in this case, the results parameter was set to 1):

{
  "results": [
    {
      "gender": "male",
      "name": {
        "title": "Mr",
        "first": "Gustav",
        "last": "Christiansen"
      },
      "location": {
        "street": {
          "number": 64,
          "name": "Skovbrynet"
        },
        "city": "Askeby",
        "state": "Danmark",
        "country": "Denmark",
        "postcode": 67407,
        "coordinates": {
          "latitude": "-36.0259",
          "longitude": "-50.4015"
        },
        "timezone": {
          "offset": "0:00",
          "description": "Western Europe Time, London, Lisbon, Casablanca"
        }
      },
      "email": "gustav.christiansen@example.com",
      "login": {
        "uuid": "5160b12a-bcd1-4e33-94e2-03a6a1a88364",
        "username": "tinydog101",
        "password": "deluxe",
        "salt": "TVZh5olg",
        "md5": "779faf341daac5cebb4033a49e492def",
        "sha1": "d96ff3e60731a11aa8550be0d655dc1361051e4b",
        "sha256": "4216310874a49ce2ddf9bee9fc3803e55558fa209cf602c4b388182b1445134d"
      },
      "dob": {
        "date": "1957-05-22T19:09:09.258Z",
        "age": 65
      },
      "registered": {
        "date": "2013-03-10T08:23:54.944Z",
        "age": 9
      },
      "phone": "61655243",
      "cell": "92661700",
      "id": {
        "name": "CPR",
        "value": "220557-1358"
      },
      "picture": {
        "large": "https://randomuser.me/api/portraits/men/43.jpg",
        "medium": "https://randomuser.me/api/portraits/med/men/43.jpg",
        "thumbnail": "https://randomuser.me/api/portraits/thumb/men/43.jpg"
      },
      "nat": "DK"
    }
  ],
  "info": {
    "seed": "06d65bd1ae788c54",
    "results": 1,
    "page": 1,
    "version": "1.3"
  }
}

I also encourage you to try the API in your browser and observe the structure of the result; it may be helpful to specify an additional query parameter, format=pretty, so that the response is more readable. The parent component that fetches the random users should clean the data before passing it to the child component. The cleaned prop passed to the child component should be an array of objects, each of which contains the name, age, and email of a user. The name should be aggregated from the name pieces that are separated in the result of the request. For clarity, the cleaned version of the array above would

[
  {
    "name": "Gustav Christiansen",
    "age": 5,
    "email": "gustav.christiansen@example.com"
  }
]

The child component should not rely on this structure, however. It may only assume that the prop is an array of objects with no nested objects, and it should render a row of column titles from the field titles of the objects. The rows following the title row should include the actual user data.

Starter Code

Here is some code to get you started:

import React, { useEffect, useState } from "react";
import axios from "axios";

function App() {
  return <Pagination />;
}

const Pagination = () => {
  // YOUR CODE HERE
};

// generic component for displaying table from array
// of objects where fields of objects are columns
const Pages = ({ content, itemsPerPage }) => {};

export default App;

Solution

import React, { useEffect, useState } from "react";
import axios from "axios";

function App() {
  return <Pagination />;
}

const Pagination = () => {
  const [users, setUsers] = useState();

  useEffect(() => {
    const fetchUsers = async () => {
      try {
        const res = await axios.get("https://randomuser.me/api?results=19");
        const userArray = res.data.results;
        const cleanArray = userArray.map((user) => {
          return {
            name: user.name.first + " " + user.name.last,
            age: user.dob.age,
            email: user.email,
          };
        });
        setUsers(cleanArray);
      } catch {
        alert("User request failed");
      }
    };

    fetchUsers();
  }, []);

  return users ? (
    <Pages content={users} itemsPerPage={5} />
  ) : (
    <h1>Loading users...</h1>
  );
};

// generic component for displaying table from array
// of objects where fields of objects are columns
const Pages = ({ content, itemsPerPage }) => {
  const [page, setPage] = useState(0);
  const start = itemsPerPage * page;
  const end = start + itemsPerPage;

  return (
    <div
      style={{
        display: "flex",
        flexDirection: "column",
      }}
    >
      <div
        style={{
          display: "flex",
          flexDirection: "row",
          justifyContent: "center",
        }}
      >
        {Object.keys(content[0]).map((key) => (
          <h3 style={{ width: 160, fontWeight: "bold" }}>{key}</h3>
        ))}
      </div>
      {content.slice(start, end).map((obj) => {
        return (
          <div
            style={{
              display: "flex",
              flexDirection: "row",
              justifyContent: "center",
            }}
          >
            {Object.values(obj).map((value) => (
              <div style={{ width: 200 }}>{value}</div>
            ))}
          </div>
        );
      })}
      <div
        style={{
          display: "flex",
          flexDirection: "row",
          justifyContent: "center",
        }}
      >
        {Array.from(Array(Math.ceil(content.length / itemsPerPage)).keys()).map(
          (number) => (
            <p1
              style={{
                width: 30,
                color: "blue",
                marginTop: 20,
                cursor: "pointer",
                fontWeight: page == number ? "bold" : "normal",
              }}
              onClick={() => setPage(number)}
            >
              {number + 1}
            </p1>
          )
        )}
      </div>
    </div>
  );
};

export default App;

In the parent component, I make use of the useEffect() hook to fetch users and update the component's state to the list of newly fetched users. Importantly, asynchronous functions cannot be passed directly to useEffect() because such functions return a promise, and functions passed to useEffect() may only return a cleanup function or nothing at all. Thus, we instead pass a function that defines an asynchronous function and calls said function. Next, the parent component conditionally renders a loading state or, if the user list has been successfully fetched, the Pages component. Pages uses the size of the content array and the number of items per page to calculate the index of the first and last (technically, last plus 1) element to display on the current page. These indices are passed to Javascript’s native Array.slice() function to copy a fragment of an array, which is then mapped to a set of divs to fill the table’s rows. Finally, to display each field in each row of the table, we make use of Object.values(), a built-in function to generate an array of the values contained in an object of key-value pairs.

For an additional challenge, add a form field to allow the user to change how many users are displayed on each page. Also, add a button to force a new set of users to be fetched along with a form field to specify how many users to fetch.


useState
useEffect
axios