Apollo/GraphQL Pagination?

Hey guys, I have a working query in TypeGraphQL on the server:

// User query arguments for pagination
@ArgsType()
class GetUsersArgs {
  @Field()
  role: string;

  @Field(() => Int, { defaultValue: 0 })
  @Min(0)
  skip: number;

  @Field(() => Int)
  @Min(1)
  @Max(12)
  take = 12;

  // helpers - index calculations
  get startIndex(): number {
    return this.skip;
  }
  get endIndex(): number {
    return this.skip + this.take;
  }
}

// User Resolver
@Resolver()
export class UserResolver {
  // Query for all users
  @Query(() => [User])
  @UseMiddleware(isAuth)
  async users(@Args() { role, startIndex, endIndex }: GetUsersArgs) {
    // Grab all users
    let users = await User.find();

    if (role !== 'admin') {
      throw new Error('Unauthenticated');
    } else {
      return users.slice(startIndex, endIndex);
    }
  }

This works in the playground:

{
  users(role:"admin", skip:0, take:12) {
    id
    username
    email
    role
  }
}

So, the idea is to refetch the variables object and change the value of skip on the button click for pagination. This is what I have currently on the frontend:

import React from 'react';
import { useUsersQuery, useRemoveUserMutation } from '../../generated/graphql';

import './UserList.css';

interface Props {
  myRole: string;
}

export const UserList: React.FC<Props> = ({ myRole }) => {
  const { data, loading, error, refetch } = useUsersQuery({
    fetchPolicy: 'network-only',
    variables: {
      role: myRole,
      skip: 0,
      take: 12,
    },
  });

  const [removeUser] = useRemoveUserMutation();

  if (loading) {
    return <div>Loading...</div>;
  }

  if (error) {
    console.log(error);
    return <div>You are not the admin — Unauthenticated.</div>;
  }

  if (!data) {
    return <div>No data</div>;
  }

  return (
    <div>
      <div className="page-sub-title">Site Users:</div>

      <ul className="site-users-list">
        {data.users.map((user) => {
          return (
            <li key={user.id}>
              <div className="user-info">
                <p>
                  ID: <span className="user-id">{user.id}</span>
                </p>
                <p>
                  <span className="user-username">{user.username}</span>
                </p>
                <p>
                  <span className="user-role">{user.role}</span>
                </p>
              </div>

              <div className="admin-btns-container">
                <button
                  className="secondaryBtn"
                  onClick={async (e) => {
                    e.preventDefault();
                    const response = await removeUser({
                      variables: {
                        id: user.id,
                      },
                    });

                    if (response) {
                      alert(`Removed User:${user.username} ID:${user.id}`);
                    }
                  }}
                >
                  Delete User
                </button>
              </div>
            </li>
          );
        })}
      </ul>
      <button onClick={() => refetch()}>Next</button>
    </div>
  );
};

UserList.defaultProps = {
  myRole: '',
};

This is the article I was following for the refetch method to refetch a different variables object. Any help is greatly appreciated.

So this is working, now I just need to deal with the length of the data and not paginate past the length of the array.

import React, { useState } from 'react';
import { useUsersQuery, useRemoveUserMutation } from '../../generated/graphql';

import './UserList.css';

interface Props {
  myRole: string;
}

export const UserList: React.FC<Props> = ({ myRole }) => {
  const [skipUsers, setSkipUsers] = useState(0);
  const { data, loading, error, refetch } = useUsersQuery({
    fetchPolicy: 'network-only',
    variables: {
      role: myRole,
      skip: skipUsers,
      take: 12,
    },
  });

  const [removeUser] = useRemoveUserMutation();

  if (loading) {
    return <div>Loading...</div>;
  }

  if (error) {
    console.log(error);
    return <div>You are not the admin — Unauthenticated.</div>;
  }

  if (!data) {
    return <div>No data</div>;
  }

  const prevClick = (role: string, skip: number, take: number) => {
    console.log('prevClick');
    console.log('skip>>', skip);
    setSkipUsers(skipUsers - 12);
    refetch({ role, skip, take });
  };

  const nextClick = (role: string, skip: number, take: number) => {
    console.log('nextClick');
    console.log('skip>>', skip);
    setSkipUsers(skipUsers + 12);
    refetch({ role, skip, take });
  };

  return (
    <div>
      <div className="page-sub-title">Site Users:</div>

      <ul className="site-users-list">
        {data.users.map((user) => {
          return (
            <li key={user.id}>
              <div className="user-info">
                <p>
                  ID: <span className="user-id">{user.id}</span>
                </p>
                <p>
                  <span className="user-username">{user.username}</span>
                </p>
                <p>
                  <span className="user-role">{user.role}</span>
                </p>
              </div>

              <div className="admin-btns-container">
                <button
                  className="secondaryBtn"
                  onClick={async (e) => {
                    e.preventDefault();
                    const response = await removeUser({
                      variables: {
                        id: user.id,
                      },
                    });

                    if (response) {
                      alert(`Removed User:${user.username} ID:${user.id}`);
                    }
                  }}
                >
                  Delete User
                </button>
              </div>
            </li>
          );
        })}
      </ul>
      <button onClick={() => prevClick('admin', 0, 12)}>Prev</button>
      <button onClick={() => nextClick('admin', 12, 12)}>Next</button>
    </div>
  );
};

UserList.defaultProps = {
  myRole: '',
};

Solution:

import React, { useState } from 'react';
import { useUsersQuery, useRemoveUserMutation } from '../../generated/graphql';

import './UserList.css';

interface Props {
  myRole: string;
}

export const UserList: React.FC<Props> = ({ myRole }) => {
  const [skipUsers, setSkipUsers] = useState(0);
  const { data, loading, error, refetch } = useUsersQuery({
    fetchPolicy: 'network-only',
    variables: {
      role: myRole,
      skip: skipUsers,
      take: 12,
    },
  });

  const [removeUser] = useRemoveUserMutation();

  if (loading) {
    return <div>Loading...</div>;
  }

  if (error) {
    console.log(error);
    return <div>You are not the admin — Unauthenticated.</div>;
  }

  if (!data) {
    return <div>No data</div>;
  }

  const prevClick = (role: string, skip: number, take: number) => {
    console.log('prevClick');
    setSkipUsers(skipUsers - 12);
    refetch({ role, skip, take });
  };

  const nextClick = (role: string, skip: number, take: number) => {
    console.log('nextClick');
    setSkipUsers(skipUsers + 12);
    refetch({ role, skip, take });
  };

  return (
    <div>
      <div className="page-sub-title">Site Users:</div>

      <ul className="site-users-list">
        {data.users.map((user) => {
          return (
            <li key={user.id}>
              <div className="user-info">
                <p>
                  ID: <span className="user-id">{user.id}</span>
                </p>
                <p>
                  <span className="user-username">{user.username}</span>
                </p>
                <p>
                  <span className="user-role">{user.role}</span>
                </p>
              </div>

              <div className="admin-btns-container">
                <button
                  className="secondaryBtn"
                  onClick={async (e) => {
                    e.preventDefault();
                    const response = await removeUser({
                      variables: {
                        id: user.id,
                      },
                    });

                    if (response) {
                      alert(`Removed User:${user.username} ID:${user.id}`);
                    }
                  }}
                >
                  Delete User
                </button>
              </div>
            </li>
          );
        })}
      </ul>
      {skipUsers !== 0 && (
        <button onClick={() => prevClick('admin', 0, 12)}>Prev</button>
      )}
      {data.users.length === 12 && (
        <button onClick={() => nextClick('admin', 12, 12)}>Next</button>
      )}
    </div>
  );
};

UserList.defaultProps = {
  myRole: '',
};

Solved this by setting the variable skip inside the state. Only small issue is having the next button show up if there are 12 users for one page, and then able to click to an empty page afterwards. I’m sure I’ll get there.