25
loading...
This website collects cookies to deliver better user experience
const fetchPokemonData = async (page, pageSize) => {
const offset = page * pageSize;
try {
const response = await fetch(
`https://pokeapi.co/api/v2/pokemon?offset=${offset}&limit=${pageSize}`
);
const data = await response.json();
return data;
} catch (e) {
throw new Error(`API error:${e?.message}`);
}
};
const { isLoading, error, data, isSuccess } = useQuery(
['pokemons', queryPageIndex, queryPageSize],
() => fetchPokemonData(queryPageIndex, queryPageSize),
{
keepPreviousData: true,
staleTime: Infinity,
}
);
staleTime
is marked as infinity as we don't want to burden the Pokémon API with too many hits.const {
getTableProps,
getTableBodyProps,
headerGroups,
prepareRow,
page,
canPreviousPage,
canNextPage,
pageOptions,
pageCount,
gotoPage,
nextPage,
previousPage,
setPageSize,
// Get the state from the instance
state: { pageIndex, pageSize },
} = useTable(
{
columns,
data: isSuccess ? trimData(data.results) : [],
initialState: {
pageIndex: queryPageIndex,
pageSize: queryPageSize,
},
manualPagination: true, // Tell the usePagination
// hook that we'll handle our own data fetching
// This means we'll also have to provide our own
// pageCount.
pageCount: isSuccess ? Math.ceil(totalCount / queryPageSize) : null,
},
usePagination
);
queryPageIndex
and queryPageSize
as the initial state. When the fetch query is isSuccess
, we pass on the data
and the pageCount
. Let's look at our local state now.const initialState = {
queryPageIndex: 0,
queryPageSize: 10,
totalCount: null,
};
const PAGE_CHANGED = 'PAGE_CHANGED';
const PAGE_SIZE_CHANGED = 'PAGE_SIZE_CHANGED';
const TOTAL_COUNT_CHANGED = 'TOTAL_COUNT_CHANGED';
const reducer = (state, { type, payload }) => {
switch (type) {
case PAGE_CHANGED:
return {
...state,
queryPageIndex: payload,
};
case PAGE_SIZE_CHANGED:
return {
...state,
queryPageSize: payload,
};
case TOTAL_COUNT_CHANGED:
return {
...state,
totalCount: payload,
};
default:
throw new Error(`Unhandled action type: ${type}`);
}
};
const [{ queryPageIndex, queryPageSize, totalCount }, dispatch] =
React.useReducer(reducer, initialState);
useReducer
in this case. As queryPageIndex
and queryPageSize
are being used in the useQuery
keys, the fetchPokemonData
is invoked when we either move to a new page or change to a new pageSize. Since we are using staleTime: Infinity
, already visited pages with a particular page size is served from the cache for infinite time.React.useEffect(() => {
dispatch({ type: PAGE_CHANGED, payload: pageIndex });
}, [pageIndex]);
React.useEffect(() => {
dispatch({ type: PAGE_SIZE_CHANGED, payload: pageSize });
gotoPage(0);
}, [pageSize, gotoPage]);
React.useEffect(() => {
if (data?.count) {
dispatch({
type: TOTAL_COUNT_CHANGED,
payload: data.count,
});
}
}, [data?.count]);
pageIndex
and pageSize
of react-table state changes in the useEffect and dispatch to keep a copy in our local state. This is clearly duplicating them in favour of using useQuery
in its declarative nature. There is another option to imperatively use react-query's fetchQuery
and keep the data in the local state but you will miss the status and all other automagical stuff of useQuery
. If you want to explore more on this topic, you can follow the reference links given at the bottom.import React from 'react';
import styled from 'styled-components';
import { useTable, usePagination } from 'react-table';
import { useQuery } from 'react-query';
import { fetchPokemonData } from './fetchData';
const TableContainer = styled.div`
padding: 1rem;
table {
border-spacing: 0;
border: 1px solid black;
tr {
:last-child {
td {
border-bottom: 0;
}
}
}
th,
td {
margin: 0;
padding: 0.5rem;
border-bottom: 1px solid black;
border-right: 1px solid black;
:last-child {
border-right: 0;
}
}
}
.pagination {
padding: 0.5rem;
}
`;
const columns = [
{
Header: 'Name',
accessor: 'name',
},
{
Header: 'Url',
accessor: 'url',
},
];
const trimData = (data = []) =>
data.map(({ name, url }) => ({
name,
url,
}));
const initialState = {
queryPageIndex: 0,
queryPageSize: 10,
totalCount: null,
};
const PAGE_CHANGED = 'PAGE_CHANGED';
const PAGE_SIZE_CHANGED = 'PAGE_SIZE_CHANGED';
const TOTAL_COUNT_CHANGED = 'TOTAL_COUNT_CHANGED';
const reducer = (state, { type, payload }) => {
switch (type) {
case PAGE_CHANGED:
return {
...state,
queryPageIndex: payload,
};
case PAGE_SIZE_CHANGED:
return {
...state,
queryPageSize: payload,
};
case TOTAL_COUNT_CHANGED:
return {
...state,
totalCount: payload,
};
default:
throw new Error(`Unhandled action type: ${type}`);
}
};
function PokemonTable() {
const [{ queryPageIndex, queryPageSize, totalCount }, dispatch] =
React.useReducer(reducer, initialState);
const { isLoading, error, data, isSuccess } = useQuery(
['pokemons', queryPageIndex, queryPageSize],
() => fetchPokemonData(queryPageIndex, queryPageSize),
{
keepPreviousData: true,
staleTime: Infinity,
}
);
const {
getTableProps,
getTableBodyProps,
headerGroups,
prepareRow,
page,
canPreviousPage,
canNextPage,
pageOptions,
pageCount,
gotoPage,
nextPage,
previousPage,
setPageSize,
// Get the state from the instance
state: { pageIndex, pageSize },
} = useTable(
{
columns,
data: isSuccess ? trimData(data.results) : [],
initialState: {
pageIndex: queryPageIndex,
pageSize: queryPageSize,
},
manualPagination: true, // Tell the usePagination
// hook that we'll handle our own data fetching
// This means we'll also have to provide our own
// pageCount.
pageCount: isSuccess ? Math.ceil(totalCount / queryPageSize) : null,
},
usePagination
);
React.useEffect(() => {
dispatch({ type: PAGE_CHANGED, payload: pageIndex });
}, [pageIndex]);
React.useEffect(() => {
dispatch({ type: PAGE_SIZE_CHANGED, payload: pageSize });
gotoPage(0);
}, [pageSize, gotoPage]);
React.useEffect(() => {
if (data?.count) {
dispatch({
type: TOTAL_COUNT_CHANGED,
payload: data.count,
});
}
}, [data?.count]);
if (error) {
return <p>Error</p>;
}
if (isLoading) {
return <p>Loading...</p>;
}
return (
<TableContainer>
{isSuccess ? (
<>
<table {...getTableProps()}>
<thead>
{headerGroups.map((headerGroup) => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map((column) => (
<th {...column.getHeaderProps()}>
{column.render('Header')}
</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{page.map((row) => {
prepareRow(row);
return (
<tr {...row.getRowProps()}>
{row.cells.map((cell) => (
<td {...cell.getCellProps()}>{cell.render('Cell')}</td>
))}
</tr>
);
})}
</tbody>
</table>
<div className="pagination">
<button onClick={() => gotoPage(0)} disabled={!canPreviousPage}>
{'<<'}
</button>{' '}
<button onClick={() => previousPage()} disabled={!canPreviousPage}>
{'<'}
</button>{' '}
<button onClick={() => nextPage()} disabled={!canNextPage}>
{'>'}
</button>{' '}
<button
onClick={() => gotoPage(pageCount - 1)}
disabled={!canNextPage}
>
{'>>'}
</button>{' '}
<span>
Page{' '}
<strong>
{pageIndex + 1} of {pageOptions.length}
</strong>{' '}
</span>
<span>
| Go to page:{' '}
<input
type="number"
value={pageIndex + 1}
onChange={(e) => {
const page = e.target.value ? Number(e.target.value) - 1 : 0;
gotoPage(page);
}}
style={{ width: '100px' }}
/>
</span>{' '}
<select
value={pageSize}
onChange={(e) => {
setPageSize(Number(e.target.value));
}}
>
{[10, 20, 30, 40, 50].map((pageSize) => (
<option key={pageSize} value={pageSize}>
Show {pageSize}
</option>
))}
</select>
</div>
</>
) : null}
</TableContainer>
);
}
export default PokemonTable;