26
loading...
This website collects cookies to deliver better user experience
npx create-react-app fauna-pagination
cd fauna-pagination && yarn start
src
file, create a new file called schema.gql
, this file will contain all our schemas for our project to be uploaded to Fauna. Inside the schema.gql
file, let’s add the code block belowtype Teacher {
name: String
students: [student] @relation
}
type Student {
name: String
tests: [Test] @relation
}
type Test {
name: String
student: Student!
teacher: Teacher!
}
type Query {
teachers: [Teacher]
tests: [Test]
}
Schema.gql
filefindTeachertByID
, findStudentByID
and findTestByID.
Next, navigate to the Security tab of your dashboard and create a key, make sure to save your access keys somewhere safe and install faunadb on your project directory using the command below:yarn add faunadb
query TestByStudent {
findTestByID(id:<test_id>) {
student: {
data: {...}
}
}
}
schema.gpl
file.type Query {
...
getTestsByStudent(id: ID): [Student] @resolver(name: "tests_by_student", paginated: true)
}
tests_by_student
appears in the "Functions" tab. When you try to use the new query right now, you’ll receive an error: “Function X not implemented yet…”. So, let’s do it.project_tests_by_student
. It does exactly what we need.Query(
Lambda(
["projectID"],
Let({ project: Ref(Collection("Project"), Var("projectID")), match: Match(Index("project_tests_by_project"), Var("project")), data: Paginate(Var("match"))},
Map(Var("data"), Lambda("ref", Get(Var("ref")))))))
Query(
Lambda(
["projectID", "size", "after", "before"],
Let(
{
...
data: If(
And(IsNull(Var("after")), IsNull(Var("before"))),
Paginate(Var("match"), { size: Var("size") }),
If(
IsNull(Var("before")),
Paginate(Var("match"), { after: Var("after"), size: Var("size") }),
Paginate(Var("match"), { before: Var("before"), size: Var("size") })
)
)
},
...
)
)
)
/ AllProjects.js
import React, { useContext } from "react";
import { useQuery } from "react-query";
import { gql } from "graphql-request";
import Table from "./Table";
import { GraphqlClientContext } from "./App";
export default function AllProjects() {
const { data, isLoading } = useProjects();
if (isLoading) {
return <span>Loading...</span>;
}
return <Table columns={columns} data={data} />;
}
function useProjects() {
const graphqlClient = useContext(GraphqlClientContext);
return useQuery("projects", async () => {
const {
projects: { data },
} = await graphqlClient.request(
gql`
query {
projects {
data {
_id
name
}
}
}
`
);
return projects;
});
}
const columns = [
{
Header: "ID",
accessor: "_id",
},
{
Header: "Name",
accessor: "name",
},
];
// Table.js
import { useTable } from "react-table";
import "./Table.scss";
export default function Table({ columns, data }) {
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
} = useTable({
columns,
data,
});
return (
<table {...getTableProps()}>
<thead>
{headerGroups.map((headerGroup) => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map((column) => (
<th {...column.getHeaderProps()}>{column.render("Header")}</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{rows.map((row, i) => {
prepareRow(row);
return (
<tr {...row.getRowProps()}>
{row.cells.map((cell) => {
return <td {...cell.getCellProps()}>{cell.render("Cell")}</td>;
})}
</tr>
);
})}
</tbody>
</table>
);
}
// App.js
import React from "react";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import { QueryClient, QueryClientProvider } from "react-query";
import { GraphQLClient } from "graphql-request";
import AllProjects from "./AllProjects";
const queryClient = new QueryClient();
const graphQLClient = new GraphQLClient(`https://graphql.fauna.com/graphql`, {
headers: {
authorization: "Bearer <fauna_secret>",
},
});
export const GraphqlClientContext = React.createContext();
function Main() {
return (
<Router>
<Switch>
<Route path="/projects">
<AllProjects />
</Route>
</Switch>
</Router>
);
}
function App() {
return (
<GraphqlClientContext.Provider value={graphQLClient}>
<QueryClientProvider client={queryClient}>
<Main />
</QueryClientProvider>
</GraphqlClientContext.Provider>
);
}
export default App;
// Table.js
import React from "react";
import { useTable, usePagination } from "react-table";
import "./Table.scss";
const pageSizeVariants = [50, 75, 100];
export default function Table({
columns,
data,
fetchData,
loading,
initialPageSize,
pageCount: controlledPageCount,
}) {
const {
getTableProps,
getTableBodyProps,
headerGroups,
prepareRow,
page,
canPreviousPage,
canNextPage,
nextPage,
previousPage,
setPageSize,
// Get the state from the instance
state: { pageIndex, pageSize },
} = useTable(
{
columns,
data,
initialState: { pageIndex: 0, pageSize: initialPageSize },
// We will be handling pagination by sending paginated request,
// not default client side, hence the manualPagination option
manualPagination: true,
pageCount: controlledPageCount,
},
usePagination
);
function changeSize(e) {
setPageSize(Number(e.target.value));
}
React.useEffect(() => {
fetchData({ pageIndex, pageSize });
}, [fetchData, pageIndex, pageSize]);
return (
<>
<table {...getTableProps()}>
<thead>{headerGroups.map(renderHeaderGroup)}</thead>
<tbody {...getTableBodyProps()}>
{page.map(renderPage(prepareRow))}
</tbody>
</table>
<div>
<button onClick={previousPage} disabled={!canPreviousPage}>
{"<"}
</button>{" "}
<button onClick={nextPage} disabled={!canNextPage}>
{">"}
</button>{" "}
<select value={pageSize} onChange={changeSize}>
{pageSizeVariants.map(renderOption)}
</select>
</div>
</>
);
}
function renderHeaderGroup(headerGroup) {
return (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map((column) => (
<th {...column.getHeaderProps()}>{column.render("Header")}</th>
))}
</tr>
);
}
function renderPage(prepareRow) {
return function (row, i) {
prepareRow(row);
return (
<tr {...row.getRowProps()}>
{row.cells.map((cell) => {
return <td {...cell.getCellProps()}>{cell.render("Cell")}</td>;
})}
</tr>
);
};
}
function renderOption(val) {
return (
<option key={val} value={val}>
Show {val}
</option>
);
}
fetchData
- function that calls API to get data on every page/size changeinitialPageSize
- sets number of documents to display on first renderpageCount
- initially, it indicates how many pages of data are available, we will not be able to get that information but we have to use it to control whether there is more data to display or not. react-table blocks pagination if current number of pages are the same as the pages count. We will increase the pageCount by one if there is more data, or keep the same if not.getTestsByProject
. We need to define some query variables.query($id: ID, $size: Int, $cursor: String) {
getTestsByProject(id: $id, _size: $size, _cursor: $cursor) {
data {
id: _id
name
student {
id: _id
}
}
after
before
}
}
}
useTests()
// useTests
function useTests(projectID) {
// react-table will send us the page index if user go back or next
const [page, setPage] = React.useState({ index: 0, cursor: null, size: 25 });
// we'll be using the GraphlClient to send requests
const graphqlClient = useContext(GraphqlClientContext);
const query = useQuery(
[key, page.size, page.cursor, projectID],
fetchProjects(graphqlClient)({ size: page.size, cursor: page.cursor, id: projectID })
);
return query
}
const fetchProjects = (client) => (variables) => async () => {
const { tests } = await client.request(
gql`
query($id: ID, $size: Int, $cursor: String) {
tests: getTestsByProject(id: $id, _size: $size, _cursor: $cursor) {
data {
id: _id
name
student {
name
}
}
after
before
}
}
`,
variables
);
return tests;
};
// useTests.js
// useTests.js
function useTests(projectID) {
...
// under query.data we have all the results from `tests` query
// query.data -> { data, after, before }
const tests = query.data?.data || [];
const nextPageCursor = query.data?.after;
const prevPageCursor = query.data?.before;
const canNextPage = !!nextPageCursor;
function nextPage() {
if (!nextPageCursor) return;
setPage((page) => ({
...page,
index: page.index + 1,
cursor: nextPageCursor,
}));
}
const prevPageCursor = data?.before;
function prevPage() {
if (!prevPageCursor) return;
setPage((page) => ({
...page,
index: page.index - 1,
cursor: prevPageCursor,
}));
}
function changeSize(size) {
if (size === page.size) return;
setPage((page) => ({ index: page.index, cursor: null, size }));
}
function updateData({ pageIndex, pageSize }) {
if (pageSize !== page.size) changeSize(pageSize);
else if (pageIndex === page.index) return;
else if (pageIndex > page.index) nextPage();
else prevPage();
}
const canNextPage = !!nextPageCursor;
return {
...query,
data: tests,
size: page.size,
updateData,
// page + 1 gives actual number of pages (page is an index started from 0)
// Number(canNextPage) increase the pageCount by 1 if canNextPage == true
pageCount: page.index + 1 + Number(canNextPage),
};
}
prevPage()
if only change size then changeSize()
method. This logic lives inside the updateData()
which will be fired after any page/size change.// Project.js
...
import { useParams } from "react-router-dom";
export default function Project() {
const { id } = useParams();
const { data, isLoading, pageCount, size, updateData } = useTests(id);
if (isLoading) {
return <span>Loading...</span>;
}
return (
<Table
columns={columns}
data={data}
fetchData={updateData}
pageCount={pageCount}
initialPageSize={size}
/>
);
}
const columns = [
{
Header: "ID",
accessor: "_id",
},
{
Header: "Name",
accessor: "name",
},
{
Header: "Student",
accessor: "student.name",
},
];
// App.js
...
<Router>
<Switch>
<Route path="/projects/:id">
<Project />
</Route>
<Route path="/projects">
<AllProjects />
</Route>
</Switch>
</Router>
// AllProjects.js
import { Link } from "react-router-dom";
...
const columns = [
{
Header: "ID",
accessor: ({ _id }) => <Link to={`/projects/${_id}`}>{_id}</Link>,
},
{
Header: "Name",
accessor: "name",
},
];
query {
tests(filter: {
student: ["286712490662822407", "286712490702668289"],
project: ["286712490727835143"] }) {
data {
id: _id
name
student {
id: _id
}
}
after
before
}
}
}
Paginate()
the data. Example resolver with schema:schema.graphql
.# schema.graphql
#...
input TestFilters {
project: [ID]
student: [ID]
}
type Query {
# ...
tests(filter: TestFilters): [Test] @resolver(name: "get_tests", paginated: true)
#...
}
// get_tests.fql
Query(
Lambda(
["filters", "size", "after", "before"],
Let(
{
baseMatch: Match(Index("tests")),
// creates match for every id in in filter.project array
matchByProjects: Map(
Select("project", Var("filters"), []),
Lambda(
"id",
Match(
Index("project_tests_by_project"),
Ref(Collection("Project"), Var("id"))
)
)
),
// creates match for every id in in filter.student array
matchByStudents: Map(
Select("student", Var("filters"), []),
Lambda(
"id",
Match(
Index("student_tests_by_student"),
Ref(Collection("Student"), Var("id"))
)
)
),
// combines all matches into one array
// end up with [baseMatch, Union([projects]), Union([students])]
match: Reduce(
Lambda(
["acc", "curr"],
If(
IsArray(Var("curr")),
If(
// skips if empty
IsEmpty(Var("curr")),
Var("acc"),
Append(Union(Var("curr")), Var("acc"))
),
If(
IsNull(Var("curr")),
Var("acc"),
Append([Var("curr")], Var("acc")),
)
)
),
[],
[
Var("baseMatch"),
Var("matchByProjects"),
Var("matchByStudents")
]
),
intersectionMatch: Intersection(Var("match")),
item: If(
Equals(Var("before"), null),
If(
Equals(Var("after"), null),
Paginate(Var("intersectionMatch"), { size: Var("size") }),
Paginate(Var("intersectionMatch"), {
after: Var("after"),
size: Var("size")
})
),
Paginate(Var("intersectionMatch"), {
before: Var("before"),
size: Var("size")
})
)
},
Map(Var("item"), Lambda("ref", Get(Var("ref"))))
)
)
)
26